volute 0.30.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -22
- package/dist/{accept-E3PAH3QJ.js → accept-74M7I4RZ.js} +5 -4
- package/dist/{activity-events-BKBPPUBP.js → activity-events-HETAODOK.js} +3 -2
- package/dist/{ai-service-VAJT5UBS.js → ai-service-ZIPCV3MX.js} +20 -5
- package/dist/api.d.ts +341 -397
- package/dist/{archive-WWDBWYN2.js → archive-INXYFVCW.js} +3 -2
- package/dist/auth-6DMGES3I.js +44 -0
- package/dist/{bridge-RO37CUFM.js → bridge-BVCBTGPF.js} +5 -4
- package/dist/{chat-TCUNPFGO.js → chat-XT4OBJBU.js} +8 -8
- package/dist/{chunk-P7VFDSSG.js → chunk-2FLJ63GU.js} +2 -2
- package/dist/{chunk-ZWKTUQEL.js → chunk-2NGTS5UU.js} +1 -1
- package/dist/{chunk-JGFRDMR6.js → chunk-ALEF47VT.js} +1 -1
- package/dist/{chunk-MDPCSXZ4.js → chunk-D5G5YOPL.js} +163 -15
- package/dist/{chunk-VGWJSNHS.js → chunk-G53F3JA4.js} +1 -35
- package/dist/{chunk-A6TUJJ3L.js → chunk-G6BSYHPK.js} +2 -2
- package/dist/{chunk-DTC6EH5I.js → chunk-I5KY25PQ.js} +1 -9
- package/dist/{chunk-NSBFETWP.js → chunk-IYDIE3HG.js} +64 -26
- package/dist/{chunk-W5OOPLNP.js → chunk-JJ7W6WSB.js} +3 -3
- package/dist/{chunk-G3GBKZGG.js → chunk-LGB6JBHI.js} +54 -2
- package/dist/chunk-LRCG2JLP.js +251 -0
- package/dist/{chunk-FXHXHI2A.js → chunk-LSGWR54X.js} +3 -6
- package/dist/{chunk-S5LR3XYJ.js → chunk-M7UL5S3Q.js} +1 -1
- package/dist/chunk-PB65JZK2.js +85 -0
- package/dist/chunk-PVY5W6QN.js +41 -0
- package/dist/{chunk-QVAQ5454.js → chunk-QBQ424EM.js} +3007 -2126
- package/dist/{chunk-P27RV5WM.js → chunk-QZANELPX.js} +6 -2
- package/dist/{chunk-FSM45XD5.js → chunk-R7E6CRVQ.js} +1 -1
- package/dist/{chunk-HHTXM4JT.js → chunk-RPZZSXV3.js} +39 -195
- package/dist/{chunk-UPA6COHU.js → chunk-RSX4OPZY.js} +5 -5
- package/dist/{chunk-2C2VXEBB.js → chunk-S6NFERDC.js} +21 -57
- package/dist/chunk-SKLSMHXO.js +208 -0
- package/dist/{chunk-IKHDUZRH.js → chunk-SX5TKJBZ.js} +2 -2
- package/dist/chunk-TDRYEPH4.js +185 -0
- package/dist/chunk-TSXLLQZW.js +46 -0
- package/dist/{chunk-EFVHR7KH.js → chunk-UKVWJRKN.js} +24 -5
- package/dist/{chunk-2NDZC3S7.js → chunk-WKF5FEFK.js} +688 -389
- package/dist/cli.js +93 -24
- package/dist/{clock-G3ALCMLJ.js → clock-2UOZ6JPU.js} +11 -8
- package/dist/{cloud-sync-JV4LJOK3.js → cloud-sync-JN3NWKEM.js} +16 -14
- package/dist/config-H2H4UIF7.js +72 -0
- package/dist/connectors/discord-bridge.js +1 -1
- package/dist/connectors/slack-bridge.js +1 -1
- package/dist/connectors/telegram-bridge.js +1 -1
- package/dist/{conversations-7KVQV7EZ.js → conversations-3O5O6AS3.js} +8 -7
- package/dist/{create-JTLS7GX3.js → create-RNLNCORE.js} +5 -4
- package/dist/{create-VQSQHJQW.js → create-WBBYI6V7.js} +6 -2
- package/dist/daemon-client-6QXHZ7US.js +12 -0
- package/dist/{daemon-restart-4JGBHEJ4.js → daemon-restart-NGFHFAUF.js} +7 -7
- package/dist/daemon.js +2446 -1999
- package/dist/{db-HMFPIRO2.js → db-F34YLV7D.js} +2 -1
- package/dist/db-RA45JBFG.js +16 -0
- package/dist/{delete-JESHKE7F.js → delete-QTGWEDBI.js} +1 -1
- package/dist/delivery-manager-SDVXFD4W.js +28 -0
- package/dist/delivery-router-FL45JL7N.js +21 -0
- package/dist/down-TB3ESMNP.js +14 -0
- package/dist/{env-CLXXT7M2.js → env-RLYQBOOP.js} +5 -4
- package/dist/{export-EGA5M5PB.js → export-SUYRLI5Q.js} +4 -3
- package/dist/{extension-WZ4SUPJB.js → extension-FQ5D3NCC.js} +6 -6
- package/dist/{extensions-ECO4RPFQ.js → extensions-GDYWQXC4.js} +9 -7
- package/dist/{files-4VEJDASH.js → files-EAMPO2SJ.js} +6 -5
- package/dist/{history-EJMMLXDO.js → history-FO5PHBQ5.js} +9 -4
- package/dist/{import-YCGPMBSI.js → import-DDUFE7AY.js} +4 -3
- package/dist/{join-2GBJKZEN.js → join-I5QEE3LG.js} +1 -1
- package/dist/{list-Q6O7FGAN.js → list-DW2VRTOZ.js} +5 -4
- package/dist/{login-RL6AU2SM.js → login-7CHPW2PN.js} +5 -4
- package/dist/{login-RET5WESK.js → login-RIJF2F4G.js} +3 -2
- package/dist/{logout-CGAGJN3L.js → logout-5MLHZALK.js} +3 -2
- package/dist/{logout-JRPBEMMR.js → logout-UZJRGY4Z.js} +3 -2
- package/dist/message-delivery-2FIM7QKO.js +32 -0
- package/dist/{mind-LUWRQUQ5.js → mind-2B6M7Y25.js} +18 -18
- package/dist/{mind-activity-tracker-VYN2ZZ2M.js → mind-activity-tracker-NZZT2NTT.js} +4 -3
- package/dist/{mind-list-V5WW5DUA.js → mind-list-WUPMQDYQ.js} +3 -2
- package/dist/mind-manager-BNCMGYXW.js +28 -0
- package/dist/mind-service-AV273WT4.js +34 -0
- package/dist/{mind-sleep-R6PTNNW4.js → mind-sleep-B7BHJLH7.js} +5 -4
- package/dist/{mind-status-I4ISFJ6I.js → mind-status-L3EFFRPR.js} +3 -2
- package/dist/{mind-wake-67ZQEWAV.js → mind-wake-GY3RFX7Y.js} +5 -4
- package/dist/{package-OYUD4ZJ4.js → package-PK6JUFL3.js} +3 -3
- package/dist/read-5AMJRO3D.js +75 -0
- package/dist/{register-NZDSTLP3.js → register-V2JZZKFK.js} +5 -4
- package/dist/{registry-ODSALQQL.js → registry-PJ4S5PHQ.js} +8 -1
- package/dist/{reject-2HZOJEIJ.js → reject-33HEZMZ4.js} +5 -4
- package/dist/{restart-QHS3NT64.js → restart-3UCMRUVC.js} +5 -4
- package/dist/{sandbox-O5FUSF43.js → sandbox-JANNTX6U.js} +4 -3
- package/dist/schema-PA3M5ZKH.js +32 -0
- package/dist/seed-ALUQ55FF.js +112 -0
- package/dist/{send-OAN3RYYY.js → send-3MI36LEF.js} +58 -69
- package/dist/{setup-QMDK5RZX.js → setup-SZIARWI6.js} +5 -4
- package/dist/{setup-XJH3E7YM.js → setup-WENLVPVP.js} +9 -9
- package/dist/{skill-FZIN4W4Q.js → skill-TUVOTW4Z.js} +5 -4
- package/dist/skills/dreaming/SKILL.md +6 -4
- package/dist/skills/dreaming/references/INSTALL.md +4 -5
- package/dist/skills/dreaming/scripts/dream.ts +5 -27
- package/dist/skills/dreaming/scripts/wake-context-dreams.sh +1 -1
- package/dist/skills/imagegen/SKILL.md +6 -5
- package/dist/skills/imagegen/references/INSTALL.md +1 -1
- package/dist/skills/resonance/SKILL.md +4 -1
- package/dist/skills/resonance/references/INSTALL.md +2 -2
- package/dist/skills/resonance/scripts/resonance-hook.sh +2 -0
- package/dist/skills/resonance/scripts/resonance.ts +35 -5
- package/dist/skills/volute-admin/SKILL.md +83 -0
- package/dist/skills/volute-mind/SKILL.md +12 -12
- package/dist/skills-XNZK6P4K.js +61 -0
- package/dist/sleep-manager-53DZOWW7.js +32 -0
- package/dist/spirit-N4W4UQRH.js +217 -0
- package/dist/{split-EXYGGGQN.js → split-STOROBYJ.js} +1 -1
- package/dist/{sprout-AXQ6H5DB.js → sprout-L2GFOVF7.js} +9 -8
- package/dist/{start-MTOVL6SY.js → start-K2NCUUCG.js} +5 -4
- package/dist/{status-ZRO37MWR.js → status-TCUMUO6M.js} +5 -5
- package/dist/{stop-OK5WEPVC.js → stop-H26JZDXF.js} +5 -4
- package/dist/system-chat-NPYFYZVI.js +32 -0
- package/dist/{systems-W3BBMSOZ.js → systems-DHBKVYEY.js} +6 -5
- package/dist/{tailscale-BM72RXCJ.js → tailscale-XHQBZROW.js} +2 -1
- package/dist/{template-hash-3HOR4UAJ.js → template-hash-A6VVKOXJ.js} +2 -1
- package/dist/up-6I6BHRTO.js +17 -0
- package/dist/{update-PLPHMMZ2.js → update-QVPRF6GR.js} +5 -5
- package/dist/{update-check-CVCN7MF6.js → update-check-ZD6OOIYQ.js} +3 -2
- package/dist/{upgrade-I6NPCYUU.js → upgrade-O4Q7WJM3.js} +12 -14
- package/dist/{version-notify-2NTWVEHL.js → version-notify-TCKWBZZG.js} +22 -23
- package/dist/web-assets/assets/index-Bui7U9Uu.css +1 -0
- package/dist/web-assets/assets/index-e36DIo1b.js +73 -0
- package/dist/web-assets/ext-theme.css +94 -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/0004_spirits.sql +5 -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/0004_snapshot.json +3 -406
- package/drizzle/meta/_journal.json +10 -101
- package/package.json +3 -3
- package/packages/extensions/notes/dist/ui/assets/index-8jWEv9SA.js +61 -0
- package/packages/extensions/notes/dist/ui/assets/index-DkaB7Ytd.css +1 -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 +17 -44
- package/templates/_base/.init/.config/hooks/pre-prompt/session-activity.ts +40 -0
- package/templates/_base/.init/.local/bin/volute +27 -0
- package/templates/_base/.init/.local/hooks/pre-prompt/session-activity.ts +40 -0
- package/templates/_base/.init/.local/hooks/startup-context.ts +58 -0
- package/templates/_base/home/.config/routes.json +1 -1
- package/templates/_base/src/lib/auto-commit.ts +82 -43
- package/templates/_base/src/lib/daemon-client.ts +40 -36
- package/templates/_base/src/lib/format-prefix.ts +1 -0
- package/templates/_base/src/lib/hook-loader.ts +155 -0
- package/templates/_base/src/lib/router.ts +17 -1
- package/templates/_base/src/lib/startup.ts +17 -12
- package/templates/_base/src/lib/transparency.ts +2 -2
- package/templates/_base/src/lib/volute-server.ts +2 -5
- package/templates/claude/.init/.claude/settings.json +1 -1
- package/templates/claude/.init/.config/routes.json +2 -2
- package/templates/claude/src/agent.ts +97 -14
- package/templates/claude/src/lib/hooks/auto-commit.ts +7 -3
- package/templates/claude/src/lib/message-channel.ts +7 -2
- package/templates/claude/src/server.ts +0 -9
- package/templates/codex/.init/.config/routes.json +11 -0
- package/templates/codex/.init/AGENTS.md +29 -0
- package/templates/codex/home/.config/config.json.tmpl +7 -0
- package/templates/codex/package.json.tmpl +20 -0
- package/templates/codex/src/agent.ts +553 -0
- package/templates/codex/src/lib/content.ts +16 -0
- package/templates/codex/src/lib/session-store.ts +56 -0
- package/templates/codex/src/server.ts +59 -0
- package/templates/codex/volute-template.json +8 -0
- package/templates/pi/.init/.config/routes.json +2 -2
- package/templates/pi/package.json.tmpl +1 -1
- package/templates/pi/src/agent.ts +63 -9
- package/templates/pi/src/lib/event-handler.ts +6 -4
- package/templates/pi/src/lib/reply-instructions-extension.ts +32 -11
- package/dist/chunk-7D47T4RB.js +0 -84
- package/dist/chunk-CVH6Y2YG.js +0 -59
- package/dist/chunk-EFP3PE6C.js +0 -232
- 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/pages-watcher-Z3PKNROC.js +0 -21
- package/dist/read-WQMPTSN2.js +0 -46
- package/dist/seed-WUQMPLDM.js +0 -71
- package/dist/skills/sessions/SKILL.md +0 -49
- 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/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-DgawVO5g.css +0 -1
- 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/.init/.config/hooks/startup-context.sh +0 -46
- package/templates/_base/.init/.config/scripts/session-reader.ts +0 -59
- package/templates/_base/home/public/.gitkeep +0 -0
- package/templates/_base/src/lib/session-monitor.ts +0 -400
- package/templates/claude/src/lib/hooks/session-context.ts +0 -32
- package/templates/pi/src/lib/session-context-extension.ts +0 -35
- /package/templates/_base/.init/{.config → .local}/hooks/wake-context.sh +0 -0
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
getAllSites,
|
|
4
|
+
getPublishedPages,
|
|
5
|
+
getRecentPages,
|
|
6
|
+
initDb,
|
|
7
|
+
syncPublishedPages
|
|
8
|
+
} from "./chunk-PB65JZK2.js";
|
|
9
|
+
import {
|
|
10
|
+
getUser,
|
|
11
|
+
getUserByUsername
|
|
12
|
+
} from "./chunk-TDRYEPH4.js";
|
|
13
|
+
import {
|
|
4
14
|
publish
|
|
5
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-QZANELPX.js";
|
|
6
16
|
import {
|
|
7
17
|
hashSkillDir,
|
|
8
18
|
importSkillFromDir,
|
|
9
19
|
sharedSkillsDir
|
|
10
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-D5G5YOPL.js";
|
|
11
21
|
import {
|
|
12
22
|
logger_default
|
|
13
23
|
} from "./chunk-YUIHSKR6.js";
|
|
14
24
|
import {
|
|
15
25
|
getDb,
|
|
16
26
|
mindDir,
|
|
17
|
-
users,
|
|
18
27
|
voluteHome,
|
|
19
|
-
voluteSystemDir
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
voluteSystemDir
|
|
29
|
+
} from "./chunk-LRCG2JLP.js";
|
|
30
|
+
import {
|
|
31
|
+
turns
|
|
32
|
+
} from "./chunk-RPZZSXV3.js";
|
|
22
33
|
|
|
23
34
|
// src/lib/extensions.ts
|
|
24
|
-
import { existsSync as
|
|
25
|
-
import { dirname, resolve as
|
|
35
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
36
|
+
import { dirname, resolve as resolve6 } from "path";
|
|
26
37
|
|
|
27
38
|
// packages/extensions/notes/src/index.ts
|
|
28
39
|
import { resolve } from "path";
|
|
@@ -40,47 +51,6 @@ function createExtension(manifest) {
|
|
|
40
51
|
return manifest;
|
|
41
52
|
}
|
|
42
53
|
|
|
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
54
|
// packages/extensions/notes/src/notes.ts
|
|
85
55
|
function slugify(text) {
|
|
86
56
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -319,7 +289,199 @@ async function resolveNoteId(db, getUserByUsername2, authorSlug) {
|
|
|
319
289
|
return row?.id ?? null;
|
|
320
290
|
}
|
|
321
291
|
|
|
292
|
+
// packages/extensions/notes/src/commands.ts
|
|
293
|
+
function getFlag(args, flag) {
|
|
294
|
+
const idx = args.indexOf(flag);
|
|
295
|
+
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
function createCommands() {
|
|
299
|
+
return {
|
|
300
|
+
write: {
|
|
301
|
+
description: "Write a new note",
|
|
302
|
+
usage: 'volute notes write "title" "content" [--reply-to author/slug]',
|
|
303
|
+
handler: async (args, ctx) => {
|
|
304
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
305
|
+
const mindName = ctx.mindName;
|
|
306
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
307
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
308
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
309
|
+
const title = args[0];
|
|
310
|
+
const content = args[1];
|
|
311
|
+
if (!title || !content)
|
|
312
|
+
return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
|
|
313
|
+
let replyToId;
|
|
314
|
+
const replyTo = getFlag(args, "--reply-to");
|
|
315
|
+
if (replyTo) {
|
|
316
|
+
const id = await resolveNoteId(ctx.db, ctx.getUserByUsername, replyTo);
|
|
317
|
+
if (id === null) return { error: `Reply target not found: ${replyTo}` };
|
|
318
|
+
replyToId = id;
|
|
319
|
+
}
|
|
320
|
+
const note = await createNote(ctx.db, ctx.getUser, user.id, title, content, replyToId);
|
|
321
|
+
ctx.publishActivity({
|
|
322
|
+
type: "note_created",
|
|
323
|
+
mind: user.username,
|
|
324
|
+
summary: `${user.username} wrote "${title}"`,
|
|
325
|
+
metadata: { author: user.username, slug: note.slug, bodyHtml: content.slice(0, 500) }
|
|
326
|
+
});
|
|
327
|
+
return { output: `Published: ${note.author_username}/${note.slug}` };
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
list: {
|
|
331
|
+
description: "List notes",
|
|
332
|
+
usage: "volute notes list [--author name] [--limit N]",
|
|
333
|
+
handler: async (args, ctx) => {
|
|
334
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
335
|
+
const author = getFlag(args, "--author");
|
|
336
|
+
const limit = parseInt(getFlag(args, "--limit") ?? "10", 10);
|
|
337
|
+
const notes = await listNotes(ctx.db, ctx.getUser, ctx.getUserByUsername, {
|
|
338
|
+
authorUsername: author,
|
|
339
|
+
limit
|
|
340
|
+
});
|
|
341
|
+
if (notes.length === 0) return { output: "No notes found." };
|
|
342
|
+
const lines = notes.map((n) => {
|
|
343
|
+
const date = new Date(n.created_at).toLocaleDateString();
|
|
344
|
+
return ` ${n.author_username}/${n.slug} "${n.title}" (${date})`;
|
|
345
|
+
});
|
|
346
|
+
return { output: lines.join("\n") };
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
read: {
|
|
350
|
+
description: "Read a note",
|
|
351
|
+
usage: "volute notes read <author/slug>",
|
|
352
|
+
handler: async (args, ctx) => {
|
|
353
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
354
|
+
const ref = args[0];
|
|
355
|
+
if (!ref || !ref.includes("/")) return { error: "Usage: volute notes read <author/slug>" };
|
|
356
|
+
const [author, slug] = ref.split("/", 2);
|
|
357
|
+
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
358
|
+
if (!note) return { error: "Note not found" };
|
|
359
|
+
const lines = [
|
|
360
|
+
`# ${note.title}
|
|
361
|
+
`,
|
|
362
|
+
`By ${note.author_username} \u2014 ${new Date(note.created_at).toLocaleString()}
|
|
363
|
+
`,
|
|
364
|
+
note.content
|
|
365
|
+
];
|
|
366
|
+
if (note.reactions?.length) {
|
|
367
|
+
lines.push(
|
|
368
|
+
`
|
|
369
|
+
Reactions: ${note.reactions.map((r) => `${r.emoji} (${r.count})`).join(" ")}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (note.comments?.length) {
|
|
373
|
+
lines.push(`
|
|
374
|
+
Comments (${note.comments.length}):`);
|
|
375
|
+
for (const c of note.comments) {
|
|
376
|
+
lines.push(` ${c.author_username}: ${c.content}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { output: lines.join("\n") };
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
comment: {
|
|
383
|
+
description: "Comment on a note",
|
|
384
|
+
usage: 'volute notes comment <author/slug> "content"',
|
|
385
|
+
handler: async (args, ctx) => {
|
|
386
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
387
|
+
const mindName = ctx.mindName;
|
|
388
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
389
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
390
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
391
|
+
const ref = args[0];
|
|
392
|
+
const content = args[1];
|
|
393
|
+
if (!ref || !ref.includes("/") || !content) {
|
|
394
|
+
return { error: 'Usage: volute notes comment <author/slug> "content"' };
|
|
395
|
+
}
|
|
396
|
+
const [author, slug] = ref.split("/", 2);
|
|
397
|
+
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
398
|
+
if (!note) return { error: "Note not found" };
|
|
399
|
+
await addComment(ctx.db, ctx.getUser, note.id, user.id, content);
|
|
400
|
+
return { output: "Comment added." };
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
react: {
|
|
404
|
+
description: "React to a note",
|
|
405
|
+
usage: 'volute notes react <author/slug> "emoji"',
|
|
406
|
+
handler: async (args, ctx) => {
|
|
407
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
408
|
+
const mindName = ctx.mindName;
|
|
409
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
410
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
411
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
412
|
+
const ref = args[0];
|
|
413
|
+
const emoji = args[1];
|
|
414
|
+
if (!ref || !ref.includes("/") || !emoji) {
|
|
415
|
+
return { error: 'Usage: volute notes react <author/slug> "emoji"' };
|
|
416
|
+
}
|
|
417
|
+
const [author, slug] = ref.split("/", 2);
|
|
418
|
+
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
419
|
+
if (!note) return { error: "Note not found" };
|
|
420
|
+
const result = toggleReaction(ctx.db, note.id, user.id, emoji);
|
|
421
|
+
return { output: result.added ? "Reaction added." : "Reaction removed." };
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
delete: {
|
|
425
|
+
description: "Delete your own note",
|
|
426
|
+
usage: "volute notes delete <author/slug>",
|
|
427
|
+
handler: async (args, ctx) => {
|
|
428
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
429
|
+
const mindName = ctx.mindName;
|
|
430
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
431
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
432
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
433
|
+
const ref = args[0];
|
|
434
|
+
if (!ref || !ref.includes("/"))
|
|
435
|
+
return { error: "Usage: volute notes delete <author/slug>" };
|
|
436
|
+
const [author, slug] = ref.split("/", 2);
|
|
437
|
+
const deleted = await deleteNote(ctx.db, ctx.getUserByUsername, author, slug, user.id);
|
|
438
|
+
if (!deleted) return { error: "Note not found or not authorized" };
|
|
439
|
+
return { output: "Note deleted." };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// packages/extensions/notes/src/db.ts
|
|
446
|
+
function initDb2(db) {
|
|
447
|
+
db.exec(`
|
|
448
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
449
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
450
|
+
author_id INTEGER NOT NULL,
|
|
451
|
+
title TEXT NOT NULL,
|
|
452
|
+
slug TEXT NOT NULL,
|
|
453
|
+
content TEXT NOT NULL,
|
|
454
|
+
reply_to_id INTEGER,
|
|
455
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
456
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
457
|
+
);
|
|
458
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
|
|
459
|
+
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
|
|
460
|
+
CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
|
|
461
|
+
|
|
462
|
+
CREATE TABLE IF NOT EXISTS note_comments (
|
|
463
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
464
|
+
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
465
|
+
author_id INTEGER NOT NULL,
|
|
466
|
+
content TEXT NOT NULL,
|
|
467
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
468
|
+
);
|
|
469
|
+
CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
|
|
470
|
+
|
|
471
|
+
CREATE TABLE IF NOT EXISTS note_reactions (
|
|
472
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
473
|
+
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
474
|
+
user_id INTEGER NOT NULL,
|
|
475
|
+
emoji TEXT NOT NULL,
|
|
476
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
477
|
+
);
|
|
478
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
|
|
479
|
+
CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
|
|
480
|
+
`);
|
|
481
|
+
}
|
|
482
|
+
|
|
322
483
|
// packages/extensions/notes/src/routes.ts
|
|
484
|
+
import { Hono } from "hono";
|
|
323
485
|
async function parseJson(c) {
|
|
324
486
|
try {
|
|
325
487
|
return await c.req.json();
|
|
@@ -366,12 +528,19 @@ function createRoutes(ctx) {
|
|
|
366
528
|
replyToId = id;
|
|
367
529
|
}
|
|
368
530
|
const note = await createNote(db, getUser2, actor.id, body.title, body.content, replyToId);
|
|
369
|
-
ctx.publishActivity(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
531
|
+
ctx.publishActivity(
|
|
532
|
+
{
|
|
533
|
+
type: "note_created",
|
|
534
|
+
mind: actor.username,
|
|
535
|
+
summary: `${actor.username} wrote "${body.title}"`,
|
|
536
|
+
metadata: {
|
|
537
|
+
author: actor.username,
|
|
538
|
+
slug: note.slug,
|
|
539
|
+
bodyHtml: body.content.slice(0, 500)
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
c
|
|
543
|
+
);
|
|
375
544
|
return c.json(note, 201);
|
|
376
545
|
}).get("/:author/:slug", async (c) => {
|
|
377
546
|
const { author, slug } = c.req.param();
|
|
@@ -459,8 +628,10 @@ var src_default = createExtension({
|
|
|
459
628
|
name: "Notes",
|
|
460
629
|
version: "0.1.0",
|
|
461
630
|
description: "Public notes for sharing thoughts, reflections, and ideas",
|
|
631
|
+
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>',
|
|
462
632
|
routes: (ctx) => createRoutes(ctx),
|
|
463
|
-
|
|
633
|
+
commands: createCommands(),
|
|
634
|
+
initDb: initDb2,
|
|
464
635
|
skillsDir,
|
|
465
636
|
standardSkill: true,
|
|
466
637
|
ui: {
|
|
@@ -474,12 +645,221 @@ var src_default = createExtension({
|
|
|
474
645
|
});
|
|
475
646
|
|
|
476
647
|
// packages/extensions/pages/src/index.ts
|
|
477
|
-
import { resolve as
|
|
648
|
+
import { resolve as resolve4 } from "path";
|
|
649
|
+
|
|
650
|
+
// packages/extensions/pages/src/commands.ts
|
|
651
|
+
import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
652
|
+
import { relative, resolve as resolve2 } from "path";
|
|
653
|
+
function createCommands2() {
|
|
654
|
+
return {
|
|
655
|
+
publish: {
|
|
656
|
+
description: "Publish all pages (copy to public snapshot)",
|
|
657
|
+
usage: "volute pages publish [--remote]",
|
|
658
|
+
handler: async (args, ctx) => {
|
|
659
|
+
const mindName = ctx.mindName;
|
|
660
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
661
|
+
const remote = args.includes("--remote");
|
|
662
|
+
const mindDir2 = ctx.getMindDir(mindName);
|
|
663
|
+
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
664
|
+
const sourceDir = resolve2(mindDir2, "home", "public", "pages");
|
|
665
|
+
if (!existsSync(sourceDir))
|
|
666
|
+
return { error: "No pages directory found (home/public/pages/)" };
|
|
667
|
+
const db = ctx.db;
|
|
668
|
+
if (!db) return { error: "Database not available" };
|
|
669
|
+
const snapshotDir = resolve2(ctx.dataDir, "sites", mindName);
|
|
670
|
+
try {
|
|
671
|
+
if (existsSync(snapshotDir)) rmSync(snapshotDir, { recursive: true });
|
|
672
|
+
cpSync(sourceDir, snapshotDir, { recursive: true });
|
|
673
|
+
} catch (err) {
|
|
674
|
+
return { error: `Failed to publish snapshot: ${err.message}` };
|
|
675
|
+
}
|
|
676
|
+
const htmlFiles = collectHtmlFiles(snapshotDir, snapshotDir);
|
|
677
|
+
let diff;
|
|
678
|
+
try {
|
|
679
|
+
diff = syncPublishedPages(db, mindName, htmlFiles);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
return { error: `Failed to update page database: ${err.message}` };
|
|
682
|
+
}
|
|
683
|
+
for (const file of diff.added) {
|
|
684
|
+
ctx.publishActivity({
|
|
685
|
+
type: "page_published",
|
|
686
|
+
mind: mindName,
|
|
687
|
+
summary: `${mindName} published ${file}`,
|
|
688
|
+
metadata: { file, iframeUrl: `/ext/pages/public/${mindName}/${file}` }
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
for (const file of diff.removed) {
|
|
692
|
+
ctx.publishActivity({
|
|
693
|
+
type: "page_removed",
|
|
694
|
+
mind: mindName,
|
|
695
|
+
summary: `${mindName} removed ${file}`,
|
|
696
|
+
metadata: { file }
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
let output = `Published ${htmlFiles.length} files`;
|
|
700
|
+
const parts = [];
|
|
701
|
+
if (diff.added.length > 0) parts.push(`${diff.added.length} new`);
|
|
702
|
+
if (diff.updated.length > 0) parts.push(`${diff.updated.length} updated`);
|
|
703
|
+
if (diff.removed.length > 0) parts.push(`${diff.removed.length} removed`);
|
|
704
|
+
if (parts.length > 0) output += ` (${parts.join(", ")})`;
|
|
705
|
+
if (remote) {
|
|
706
|
+
const config = ctx.getSystemsConfig();
|
|
707
|
+
if (!config)
|
|
708
|
+
return {
|
|
709
|
+
error: "Not connected to volute.systems. Run volute systems register or login first."
|
|
710
|
+
};
|
|
711
|
+
const allFiles = collectAllFiles(snapshotDir, snapshotDir);
|
|
712
|
+
const files = {};
|
|
713
|
+
for (const f of allFiles) {
|
|
714
|
+
const fp = resolve2(snapshotDir, f);
|
|
715
|
+
files[f] = readFileSync(fp).toString("base64");
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const res = await fetch(`${config.apiUrl}/api/pages/publish/${mindName}`, {
|
|
719
|
+
method: "PUT",
|
|
720
|
+
headers: {
|
|
721
|
+
"Content-Type": "application/json",
|
|
722
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
723
|
+
},
|
|
724
|
+
body: JSON.stringify({ files })
|
|
725
|
+
});
|
|
726
|
+
const data = await res.json().catch(() => ({}));
|
|
727
|
+
if (!res.ok) {
|
|
728
|
+
const errMsg = data.error || `HTTP ${res.status}`;
|
|
729
|
+
output += `
|
|
730
|
+
Warning: remote publish failed: ${errMsg}`;
|
|
731
|
+
} else if (data.url) {
|
|
732
|
+
output += `
|
|
733
|
+
Remote: ${data.url}`;
|
|
734
|
+
}
|
|
735
|
+
} catch (err) {
|
|
736
|
+
output += `
|
|
737
|
+
Warning: remote publish failed: ${err.message}`;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return { output };
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
list: {
|
|
744
|
+
description: "List pages with publish status",
|
|
745
|
+
usage: "volute pages list [--all]",
|
|
746
|
+
handler: async (args, ctx) => {
|
|
747
|
+
const mindName = ctx.mindName;
|
|
748
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
749
|
+
const db = ctx.db;
|
|
750
|
+
if (!db) return { error: "Database not available" };
|
|
751
|
+
const allFlag = args.includes("--all");
|
|
752
|
+
const port = process.env.VOLUTE_DAEMON_PORT || "1618";
|
|
753
|
+
if (allFlag) {
|
|
754
|
+
const { getAllSites: getAllSites2 } = await import("./db-RA45JBFG.js");
|
|
755
|
+
const sites = getAllSites2(db);
|
|
756
|
+
const lines2 = [];
|
|
757
|
+
for (const site of sites) {
|
|
758
|
+
for (const f of site.files) {
|
|
759
|
+
const url = `http://localhost:${port}/ext/pages/public/${site.mind}/${f.file}`;
|
|
760
|
+
lines2.push(`${site.mind.padEnd(15)} ${f.file.padEnd(25)} ${url}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return { output: lines2.length > 0 ? lines2.join("\n") : "No published pages found." };
|
|
764
|
+
}
|
|
765
|
+
const mindDir2 = ctx.getMindDir(mindName);
|
|
766
|
+
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
767
|
+
const sourceDir = resolve2(mindDir2, "home", "public", "pages");
|
|
768
|
+
const published = new Set(getPublishedPages(db, mindName).map((p) => p.file));
|
|
769
|
+
const draftFiles = existsSync(sourceDir) ? collectHtmlFiles(sourceDir, sourceDir) : [];
|
|
770
|
+
const allFiles = /* @__PURE__ */ new Set([...published, ...draftFiles]);
|
|
771
|
+
if (allFiles.size === 0) return { output: "No pages found." };
|
|
772
|
+
const lines = [...allFiles].sort().map((file) => {
|
|
773
|
+
const isPublished = published.has(file);
|
|
774
|
+
const status = isPublished ? "published" : "draft";
|
|
775
|
+
const url = isPublished ? `http://localhost:${port}/ext/pages/public/${mindName}/${file}` : "";
|
|
776
|
+
return `${status.padEnd(11)} ${file.padEnd(25)} ${url}`;
|
|
777
|
+
});
|
|
778
|
+
return { output: lines.join("\n") };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function collectHtmlFiles(dir, baseDir) {
|
|
784
|
+
const files = [];
|
|
785
|
+
let items;
|
|
786
|
+
try {
|
|
787
|
+
items = readdirSync(dir);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
|
|
790
|
+
return files;
|
|
791
|
+
}
|
|
792
|
+
for (const item of items) {
|
|
793
|
+
if (item.startsWith(".")) continue;
|
|
794
|
+
const fullPath = resolve2(dir, item);
|
|
795
|
+
try {
|
|
796
|
+
const s = statSync(fullPath);
|
|
797
|
+
if (s.isFile() && item.endsWith(".html")) {
|
|
798
|
+
files.push(relative(baseDir, fullPath));
|
|
799
|
+
} else if (s.isDirectory()) {
|
|
800
|
+
files.push(...collectHtmlFiles(fullPath, baseDir));
|
|
801
|
+
}
|
|
802
|
+
} catch (err) {
|
|
803
|
+
console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return files.sort();
|
|
807
|
+
}
|
|
808
|
+
function collectAllFiles(dir, baseDir) {
|
|
809
|
+
const files = [];
|
|
810
|
+
let items;
|
|
811
|
+
try {
|
|
812
|
+
items = readdirSync(dir);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
|
|
815
|
+
return files;
|
|
816
|
+
}
|
|
817
|
+
for (const item of items) {
|
|
818
|
+
if (item.startsWith(".")) continue;
|
|
819
|
+
const fullPath = resolve2(dir, item);
|
|
820
|
+
try {
|
|
821
|
+
const s = statSync(fullPath);
|
|
822
|
+
if (s.isFile()) {
|
|
823
|
+
files.push(relative(baseDir, fullPath));
|
|
824
|
+
} else if (s.isDirectory()) {
|
|
825
|
+
files.push(...collectAllFiles(fullPath, baseDir));
|
|
826
|
+
}
|
|
827
|
+
} catch (err) {
|
|
828
|
+
console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return files.sort();
|
|
832
|
+
}
|
|
478
833
|
|
|
479
834
|
// packages/extensions/pages/src/routes.ts
|
|
480
835
|
import { readFile, stat } from "fs/promises";
|
|
481
|
-
import { extname, resolve as
|
|
836
|
+
import { extname, resolve as resolve3 } from "path";
|
|
482
837
|
import { Hono as Hono2 } from "hono";
|
|
838
|
+
|
|
839
|
+
// packages/extensions/pages/src/cache.ts
|
|
840
|
+
function getSites(db) {
|
|
841
|
+
const sites = getAllSites(db);
|
|
842
|
+
return sites.map((site) => ({
|
|
843
|
+
name: site.mind,
|
|
844
|
+
label: site.mind,
|
|
845
|
+
pages: site.files.map((f) => ({
|
|
846
|
+
file: f.file,
|
|
847
|
+
modified: f.updated_at,
|
|
848
|
+
url: `/ext/pages/public/${site.mind}/${f.file}`
|
|
849
|
+
}))
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
function getRecentPagesList(db, opts) {
|
|
853
|
+
const rows = getRecentPages(db, opts);
|
|
854
|
+
return rows.map((r) => ({
|
|
855
|
+
mind: r.mind,
|
|
856
|
+
file: r.file,
|
|
857
|
+
modified: r.updated_at,
|
|
858
|
+
url: `/ext/pages/public/${r.mind}/${r.file}`
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// packages/extensions/pages/src/routes.ts
|
|
483
863
|
var MIME_TYPES = {
|
|
484
864
|
".html": "text/html",
|
|
485
865
|
".js": "application/javascript",
|
|
@@ -496,28 +876,20 @@ var MIME_TYPES = {
|
|
|
496
876
|
".txt": "text/plain",
|
|
497
877
|
".xml": "application/xml"
|
|
498
878
|
};
|
|
499
|
-
var _pagesWatcher = null;
|
|
500
|
-
async function getPagesWatcher() {
|
|
501
|
-
if (_pagesWatcher) return _pagesWatcher;
|
|
502
|
-
const mod = await import("./pages-watcher-Z3PKNROC.js");
|
|
503
|
-
_pagesWatcher = mod;
|
|
504
|
-
return _pagesWatcher;
|
|
505
|
-
}
|
|
506
879
|
function createRoutes2(ctx) {
|
|
507
880
|
return new Hono2().get("/", async (c) => {
|
|
508
|
-
|
|
509
|
-
const sites =
|
|
510
|
-
const recentPages =
|
|
881
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
882
|
+
const sites = getSites(ctx.db);
|
|
883
|
+
const recentPages = getRecentPagesList(ctx.db);
|
|
511
884
|
return c.json({ sites, recentPages });
|
|
512
885
|
}).get("/feed", async (c) => {
|
|
513
|
-
|
|
514
|
-
let recentPages = await pw.getCachedRecentPages();
|
|
886
|
+
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
515
887
|
const mind = c.req.query("mind");
|
|
516
|
-
if (mind) recentPages = recentPages.filter((p) => p.mind === mind);
|
|
517
888
|
const rawLimit = c.req.query("limit");
|
|
518
|
-
const limit = rawLimit ? parseInt(rawLimit, 10) : 8;
|
|
889
|
+
const limit = rawLimit ? parseInt(rawLimit, 10) || 8 : 8;
|
|
890
|
+
const recentPages = getRecentPagesList(ctx.db, { mind: mind || void 0, limit });
|
|
519
891
|
return c.json(
|
|
520
|
-
recentPages.
|
|
892
|
+
recentPages.map((p) => ({
|
|
521
893
|
id: `page-${p.mind}-${p.file}`,
|
|
522
894
|
title: `${p.mind}/${p.file}`,
|
|
523
895
|
url: p.url ?? `/minds/${p.mind}/pages/${p.file}`,
|
|
@@ -576,66 +948,70 @@ function createRoutes2(ctx) {
|
|
|
576
948
|
var _voluteHome = null;
|
|
577
949
|
async function getVoluteHome() {
|
|
578
950
|
if (_voluteHome) return _voluteHome();
|
|
579
|
-
const mod = await import("./registry-
|
|
951
|
+
const mod = await import("./registry-PJ4S5PHQ.js");
|
|
580
952
|
_voluteHome = mod.voluteHome;
|
|
581
953
|
return _voluteHome();
|
|
582
954
|
}
|
|
583
955
|
function createPublicRoutes(ctx) {
|
|
584
956
|
return new Hono2().get("/:name/*", async (c) => {
|
|
585
957
|
const name = c.req.param("name");
|
|
958
|
+
if (name.includes("/") || name.includes("\\") || name === "." || name === "..")
|
|
959
|
+
return c.text("Not found", 404);
|
|
586
960
|
let pagesRoot;
|
|
587
961
|
if (name === "_system") {
|
|
588
962
|
const home = await getVoluteHome();
|
|
589
|
-
pagesRoot =
|
|
963
|
+
pagesRoot = resolve3(home, "shared", "pages");
|
|
590
964
|
} else {
|
|
591
|
-
|
|
592
|
-
if (!mindDirPath) return c.text("Not found", 404);
|
|
593
|
-
pagesRoot = resolve2(mindDirPath, "home", "public", "pages");
|
|
965
|
+
pagesRoot = resolve3(ctx.dataDir, "sites", name);
|
|
594
966
|
}
|
|
595
967
|
const prefix = `/public/${name}`;
|
|
596
968
|
const idx = c.req.path.indexOf(prefix);
|
|
597
969
|
const wildcard = idx >= 0 ? c.req.path.slice(idx + prefix.length) : "/";
|
|
598
|
-
const requestedPath =
|
|
599
|
-
if (requestedPath !== pagesRoot && !requestedPath.startsWith(pagesRoot
|
|
970
|
+
const requestedPath = resolve3(pagesRoot, wildcard.slice(1));
|
|
971
|
+
if (requestedPath !== pagesRoot && !requestedPath.startsWith(`${pagesRoot}/`))
|
|
600
972
|
return c.text("Forbidden", 403);
|
|
973
|
+
let fileToServe = requestedPath;
|
|
601
974
|
let fileStat = await stat(requestedPath).catch(() => null);
|
|
602
975
|
if (fileStat?.isDirectory()) {
|
|
603
|
-
const indexPath =
|
|
976
|
+
const indexPath = resolve3(requestedPath, "index.html");
|
|
604
977
|
fileStat = await stat(indexPath).catch(() => null);
|
|
605
978
|
if (fileStat?.isFile()) {
|
|
606
|
-
|
|
607
|
-
|
|
979
|
+
fileToServe = indexPath;
|
|
980
|
+
} else {
|
|
981
|
+
return c.text("Not found", 404);
|
|
608
982
|
}
|
|
983
|
+
} else if (!fileStat?.isFile()) {
|
|
609
984
|
return c.text("Not found", 404);
|
|
610
985
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const body = await readFile(
|
|
986
|
+
const ext = extname(fileToServe);
|
|
987
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
988
|
+
try {
|
|
989
|
+
const body = await readFile(fileToServe);
|
|
615
990
|
return c.body(body, 200, { "Content-Type": mime });
|
|
991
|
+
} catch (err) {
|
|
992
|
+
const code = err.code;
|
|
993
|
+
if (code === "EACCES") return c.text("Forbidden", 403);
|
|
994
|
+
if (code === "ENOENT") return c.text("Not found", 404);
|
|
995
|
+
return c.text("Internal server error", 500);
|
|
616
996
|
}
|
|
617
|
-
return c.text("Not found", 404);
|
|
618
997
|
});
|
|
619
998
|
}
|
|
620
999
|
|
|
621
1000
|
// packages/extensions/pages/src/index.ts
|
|
622
|
-
var assetsDir2 =
|
|
623
|
-
var skillsDir2 =
|
|
624
|
-
var _watcher = null;
|
|
625
|
-
async function getWatcher() {
|
|
626
|
-
if (_watcher) return _watcher;
|
|
627
|
-
_watcher = await import("./pages-watcher-Z3PKNROC.js");
|
|
628
|
-
return _watcher;
|
|
629
|
-
}
|
|
1001
|
+
var assetsDir2 = resolve4(import.meta.dirname, "../dist/ui");
|
|
1002
|
+
var skillsDir2 = resolve4(import.meta.dirname, "../skills");
|
|
630
1003
|
var src_default2 = createExtension({
|
|
631
1004
|
id: "pages",
|
|
632
1005
|
name: "Pages",
|
|
633
1006
|
version: "0.1.0",
|
|
634
1007
|
description: "Publish and serve web pages from mind directories",
|
|
1008
|
+
initDb,
|
|
635
1009
|
routes: (ctx) => createRoutes2(ctx),
|
|
636
1010
|
publicRoutes: (ctx) => createPublicRoutes(ctx),
|
|
1011
|
+
commands: createCommands2(),
|
|
637
1012
|
skillsDir: skillsDir2,
|
|
638
1013
|
standardSkill: true,
|
|
1014
|
+
icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="2" width="14" height="12" rx="1.5"/><path d="M1 5h14"/><circle cx="3" cy="3.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="5" cy="3.5" r="0.5" fill="currentColor" stroke="none"/></svg>',
|
|
639
1015
|
ui: {
|
|
640
1016
|
assetsDir: assetsDir2,
|
|
641
1017
|
systemSection: {
|
|
@@ -647,194 +1023,129 @@ var src_default2 = createExtension({
|
|
|
647
1023
|
feedSource: {
|
|
648
1024
|
endpoint: "/api/ext/pages/feed"
|
|
649
1025
|
}
|
|
650
|
-
},
|
|
651
|
-
onDaemonStart: () => {
|
|
652
|
-
getWatcher().then((w) => w.startSystemWatcher()).catch(
|
|
653
|
-
(err) => console.error("[pages] failed to start system watcher:", err.message)
|
|
654
|
-
);
|
|
655
|
-
},
|
|
656
|
-
onDaemonStop: () => {
|
|
657
|
-
getWatcher().then((w) => w.stopAllWatchers()).catch((err) => console.error("[pages] failed to stop watchers:", err.message));
|
|
658
|
-
},
|
|
659
|
-
onMindStart: (mindName) => {
|
|
660
|
-
getWatcher().then((w) => w.startWatcher(mindName)).catch(
|
|
661
|
-
(err) => console.error(`[pages] failed to start watcher for ${mindName}:`, err.message)
|
|
662
|
-
);
|
|
663
|
-
},
|
|
664
|
-
onMindStop: (mindName) => {
|
|
665
|
-
getWatcher().then((w) => w.stopWatcher(mindName)).catch(
|
|
666
|
-
(err) => console.error(`[pages] failed to stop watcher for ${mindName}:`, err.message)
|
|
667
|
-
);
|
|
668
1026
|
}
|
|
669
1027
|
});
|
|
670
1028
|
|
|
671
|
-
// src/lib/
|
|
672
|
-
import {
|
|
673
|
-
import {
|
|
674
|
-
var
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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;
|
|
1029
|
+
// src/lib/daemon/turn-tracker.ts
|
|
1030
|
+
import { randomUUID } from "crypto";
|
|
1031
|
+
import { eq } from "drizzle-orm";
|
|
1032
|
+
var tlog = logger_default.child("turn-tracker");
|
|
1033
|
+
var activeTurns = /* @__PURE__ */ new Map();
|
|
1034
|
+
function key(mind, session) {
|
|
1035
|
+
return `${mind}:${session ?? "*"}`;
|
|
1036
|
+
}
|
|
1037
|
+
async function createTurn(mind) {
|
|
1038
|
+
const k = key(mind);
|
|
1039
|
+
const existing = activeTurns.get(k);
|
|
1040
|
+
if (existing) return existing.turnId;
|
|
1041
|
+
const turnId = randomUUID();
|
|
1042
|
+
const entry = { turnId, lastToolUseEventId: void 0 };
|
|
1043
|
+
activeTurns.set(k, entry);
|
|
727
1044
|
try {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
password_hash: "!mind",
|
|
731
|
-
role: "user",
|
|
732
|
-
user_type: "mind"
|
|
733
|
-
}).returning(userSelectFields);
|
|
734
|
-
return result;
|
|
1045
|
+
const db = await getDb();
|
|
1046
|
+
await db.insert(turns).values({ id: turnId, mind, status: "active" });
|
|
735
1047
|
} catch (err) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
throw err;
|
|
1048
|
+
tlog.error(`failed to create turn for ${mind}`, logger_default.errorData(err));
|
|
1049
|
+
if (activeTurns.get(k) === entry) activeTurns.delete(k);
|
|
1050
|
+
return void 0;
|
|
741
1051
|
}
|
|
1052
|
+
return turnId;
|
|
742
1053
|
}
|
|
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;
|
|
1054
|
+
function getActiveTurnId(mind, session) {
|
|
1055
|
+
return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
|
|
755
1056
|
}
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
1057
|
+
function trackToolUse(mind, session, eventId) {
|
|
1058
|
+
const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
|
|
1059
|
+
if (entry) entry.lastToolUseEventId = eventId;
|
|
759
1060
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.role, "admin"));
|
|
763
|
-
return value;
|
|
1061
|
+
function getLastToolUseEventId(mind, session) {
|
|
1062
|
+
return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
|
|
764
1063
|
}
|
|
765
|
-
async function
|
|
766
|
-
const
|
|
767
|
-
const
|
|
768
|
-
if (!
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1064
|
+
async function assignSession(mind, turnId, session) {
|
|
1065
|
+
const wildcardKey = key(mind);
|
|
1066
|
+
const entry = activeTurns.get(wildcardKey);
|
|
1067
|
+
if (!entry || entry.turnId !== turnId) {
|
|
1068
|
+
tlog.warn(`assignSession: no matching turn for ${mind} (turnId=${turnId}, session=${session})`);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
const db = await getDb();
|
|
1073
|
+
await db.update(turns).set({ session }).where(eq(turns.id, turnId));
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
tlog.error(`failed to assign session to turn ${turnId}`, logger_default.errorData(err));
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
activeTurns.delete(wildcardKey);
|
|
1079
|
+
activeTurns.set(key(mind, session), entry);
|
|
1080
|
+
}
|
|
1081
|
+
async function completeTurn(mind, session) {
|
|
1082
|
+
const k = key(mind, session);
|
|
1083
|
+
const wildcardKey = key(mind);
|
|
1084
|
+
const entry = activeTurns.get(k) ?? activeTurns.get(wildcardKey);
|
|
1085
|
+
if (!entry) return void 0;
|
|
1086
|
+
try {
|
|
1087
|
+
const db = await getDb();
|
|
1088
|
+
await db.update(turns).set({ status: "complete" }).where(eq(turns.id, entry.turnId));
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
tlog.error(`failed to complete turn ${entry.turnId}`, logger_default.errorData(err));
|
|
1091
|
+
return void 0;
|
|
1092
|
+
}
|
|
1093
|
+
activeTurns.delete(k);
|
|
1094
|
+
activeTurns.delete(wildcardKey);
|
|
1095
|
+
return entry.turnId;
|
|
776
1096
|
}
|
|
777
|
-
async function
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1097
|
+
async function setSummaryEventId(turnId, summaryEventId) {
|
|
1098
|
+
try {
|
|
1099
|
+
const db = await getDb();
|
|
1100
|
+
await db.update(turns).set({ summary_event_id: summaryEventId }).where(eq(turns.id, turnId));
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
tlog.error(`failed to set summary event for turn ${turnId}`, logger_default.errorData(err));
|
|
1103
|
+
}
|
|
782
1104
|
}
|
|
783
|
-
async function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
await db.update(users).set(newProfile).where(eq(users.id, user.id));
|
|
794
|
-
broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
|
|
1105
|
+
async function completeOrphanedTurns() {
|
|
1106
|
+
try {
|
|
1107
|
+
const db = await getDb();
|
|
1108
|
+
const active = await db.select({ id: turns.id }).from(turns).where(eq(turns.status, "active"));
|
|
1109
|
+
if (active.length === 0) return;
|
|
1110
|
+
await db.update(turns).set({ status: "complete" }).where(eq(turns.status, "active"));
|
|
1111
|
+
tlog.info(`completed ${active.length} orphaned active turn(s) from previous daemon session`);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
tlog.error("failed to complete orphaned turns on startup", logger_default.errorData(err));
|
|
1114
|
+
}
|
|
795
1115
|
}
|
|
796
|
-
async function
|
|
797
|
-
const
|
|
798
|
-
|
|
1116
|
+
async function clearMind(mind) {
|
|
1117
|
+
const toDelete = [];
|
|
1118
|
+
const turnIds = [];
|
|
1119
|
+
for (const [k, entry] of activeTurns.entries()) {
|
|
1120
|
+
if (k.startsWith(`${mind}:`)) {
|
|
1121
|
+
turnIds.push(entry.turnId);
|
|
1122
|
+
toDelete.push(k);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
for (const k of toDelete) activeTurns.delete(k);
|
|
1126
|
+
if (turnIds.length > 0) {
|
|
1127
|
+
try {
|
|
1128
|
+
const db = await getDb();
|
|
1129
|
+
for (const id of turnIds) {
|
|
1130
|
+
await db.update(turns).set({ status: "complete" }).where(eq(turns.id, id));
|
|
1131
|
+
}
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
tlog.error(`failed to complete orphaned turns for ${mind}`, logger_default.errorData(err));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
799
1136
|
}
|
|
800
1137
|
|
|
801
1138
|
// src/lib/systems-config.ts
|
|
802
|
-
import {
|
|
803
|
-
|
|
804
|
-
mkdirSync,
|
|
805
|
-
readFileSync,
|
|
806
|
-
renameSync,
|
|
807
|
-
unlinkSync,
|
|
808
|
-
writeFileSync
|
|
809
|
-
} from "fs";
|
|
810
|
-
import { resolve as resolve4 } from "path";
|
|
1139
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
1140
|
+
import { resolve as resolve5 } from "path";
|
|
811
1141
|
var DEFAULT_API_URL = "https://volute.systems";
|
|
812
1142
|
function configPath() {
|
|
813
|
-
return
|
|
814
|
-
}
|
|
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
|
-
}
|
|
1143
|
+
return resolve5(voluteSystemDir(), "systems.json");
|
|
832
1144
|
}
|
|
833
1145
|
function readSystemsConfig() {
|
|
834
|
-
migrateIfNeeded();
|
|
835
1146
|
const path = configPath();
|
|
836
|
-
if (!
|
|
837
|
-
const raw =
|
|
1147
|
+
if (!existsSync2(path)) return null;
|
|
1148
|
+
const raw = readFileSync2(path, "utf-8");
|
|
838
1149
|
let data;
|
|
839
1150
|
try {
|
|
840
1151
|
data = JSON.parse(raw);
|
|
@@ -870,19 +1181,19 @@ function deleteSystemsConfig() {
|
|
|
870
1181
|
var VALID_EXTENSION_ID2 = /^[a-z0-9][a-z0-9_-]*$/;
|
|
871
1182
|
var loaded = [];
|
|
872
1183
|
function extensionsBaseDir() {
|
|
873
|
-
return
|
|
1184
|
+
return resolve6(voluteHome(), "extensions");
|
|
874
1185
|
}
|
|
875
1186
|
function extensionDataDir(id) {
|
|
876
|
-
return
|
|
1187
|
+
return resolve6(voluteSystemDir(), "extension-data", id);
|
|
877
1188
|
}
|
|
878
1189
|
function extensionsConfigPath() {
|
|
879
|
-
return
|
|
1190
|
+
return resolve6(voluteHome(), "system", "extensions.json");
|
|
880
1191
|
}
|
|
881
1192
|
function readExtensionsConfig() {
|
|
882
1193
|
const configPath2 = extensionsConfigPath();
|
|
883
|
-
if (!
|
|
1194
|
+
if (!existsSync3(configPath2)) return [];
|
|
884
1195
|
try {
|
|
885
|
-
const data = JSON.parse(
|
|
1196
|
+
const data = JSON.parse(readFileSync3(configPath2, "utf-8"));
|
|
886
1197
|
return Array.isArray(data) ? data : [];
|
|
887
1198
|
} catch (err) {
|
|
888
1199
|
logger_default.warn("failed to read extensions config, ignoring installed extensions", {
|
|
@@ -900,73 +1211,10 @@ async function getLibsqlDatabase() {
|
|
|
900
1211
|
return _LibsqlDatabase;
|
|
901
1212
|
}
|
|
902
1213
|
async function openExtensionDb(_id, dataDir) {
|
|
903
|
-
const dbPath =
|
|
1214
|
+
const dbPath = resolve6(dataDir, "data.db");
|
|
904
1215
|
const Database = await getLibsqlDatabase();
|
|
905
1216
|
return new Database(dbPath);
|
|
906
1217
|
}
|
|
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
1218
|
async function buildContext(manifest, dataDir, authMw) {
|
|
971
1219
|
let db = null;
|
|
972
1220
|
if (manifest.initDb) {
|
|
@@ -977,9 +1225,6 @@ async function buildContext(manifest, dataDir, authMw) {
|
|
|
977
1225
|
realDb.close();
|
|
978
1226
|
throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
|
|
979
1227
|
}
|
|
980
|
-
if (manifest.id === "notes") {
|
|
981
|
-
await migrateNotesFromCoreDb(realDb);
|
|
982
|
-
}
|
|
983
1228
|
db = realDb;
|
|
984
1229
|
}
|
|
985
1230
|
return {
|
|
@@ -992,15 +1237,22 @@ async function buildContext(manifest, dataDir, authMw) {
|
|
|
992
1237
|
},
|
|
993
1238
|
getUser: async (id) => getUser(id),
|
|
994
1239
|
getUserByUsername: async (username) => getUserByUsername(username),
|
|
995
|
-
publishActivity: (event) => {
|
|
996
|
-
|
|
1240
|
+
publishActivity: (event, sessionOrContext) => {
|
|
1241
|
+
const session = typeof sessionOrContext === "string" ? sessionOrContext : sessionOrContext?.get("mindSession");
|
|
1242
|
+
const turnId = getActiveTurnId(event.mind, session);
|
|
1243
|
+
const sourceEventId = getLastToolUseEventId(event.mind, session);
|
|
1244
|
+
publish({
|
|
1245
|
+
...event,
|
|
1246
|
+
turn_id: turnId,
|
|
1247
|
+
source_event_id: sourceEventId
|
|
1248
|
+
}).catch(
|
|
997
1249
|
(err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
|
|
998
1250
|
);
|
|
999
1251
|
},
|
|
1000
1252
|
getMindDir: (name) => {
|
|
1001
1253
|
try {
|
|
1002
1254
|
const dir = mindDir(name);
|
|
1003
|
-
return
|
|
1255
|
+
return existsSync3(dir) ? dir : null;
|
|
1004
1256
|
} catch (err) {
|
|
1005
1257
|
logger_default.warn(
|
|
1006
1258
|
`extension ${manifest.id}: failed to resolve mind dir for ${name}`,
|
|
@@ -1030,19 +1282,48 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1030
1282
|
const publicApp = manifest.publicRoutes(context);
|
|
1031
1283
|
app.route(`/ext/${manifest.id}/public`, publicApp);
|
|
1032
1284
|
}
|
|
1285
|
+
if (manifest.commands) {
|
|
1286
|
+
for (const [cmdName, cmd] of Object.entries(manifest.commands)) {
|
|
1287
|
+
app.post(`${extApiPath}/commands/${cmdName}`, async (c) => {
|
|
1288
|
+
let body;
|
|
1289
|
+
try {
|
|
1290
|
+
body = await c.req.json();
|
|
1291
|
+
} catch {
|
|
1292
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
1293
|
+
}
|
|
1294
|
+
const user = c.get("user");
|
|
1295
|
+
const mindName = body.mind || user?.username;
|
|
1296
|
+
const session = c.get("mindSession");
|
|
1297
|
+
try {
|
|
1298
|
+
const result = await cmd.handler(body.args ?? [], {
|
|
1299
|
+
...context,
|
|
1300
|
+
// Bind publishActivity to the session so command handlers
|
|
1301
|
+
// don't need to pass it explicitly
|
|
1302
|
+
publishActivity: (event, sc) => context.publishActivity(event, sc ?? session),
|
|
1303
|
+
mindName,
|
|
1304
|
+
session
|
|
1305
|
+
});
|
|
1306
|
+
return c.json(result);
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
|
|
1309
|
+
return c.json({ error: err.message }, 500);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1033
1314
|
let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
|
|
1034
|
-
if (resolvedAssetsDir && !
|
|
1315
|
+
if (resolvedAssetsDir && !existsSync3(resolvedAssetsDir)) {
|
|
1035
1316
|
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
1036
1317
|
for (let i = 0; i < 5; i++) {
|
|
1037
|
-
const candidate =
|
|
1038
|
-
if (
|
|
1318
|
+
const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
|
|
1319
|
+
if (existsSync3(candidate)) {
|
|
1039
1320
|
resolvedAssetsDir = candidate;
|
|
1040
1321
|
break;
|
|
1041
1322
|
}
|
|
1042
1323
|
searchDir = dirname(searchDir);
|
|
1043
1324
|
}
|
|
1044
1325
|
}
|
|
1045
|
-
if (resolvedAssetsDir &&
|
|
1326
|
+
if (resolvedAssetsDir && existsSync3(resolvedAssetsDir)) {
|
|
1046
1327
|
const assetsDir3 = resolvedAssetsDir;
|
|
1047
1328
|
const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
|
|
1048
1329
|
const { extname: ext } = await import("path");
|
|
@@ -1059,11 +1340,11 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1059
1340
|
".woff2": "font/woff2"
|
|
1060
1341
|
};
|
|
1061
1342
|
const prefix = `/ext/${manifest.id}`;
|
|
1062
|
-
const indexPath =
|
|
1343
|
+
const indexPath = resolve6(assetsDir3, "index.html");
|
|
1063
1344
|
const serveExtAssets = async (c) => {
|
|
1064
1345
|
const urlPath = new URL(c.req.url).pathname;
|
|
1065
1346
|
const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
|
|
1066
|
-
const filePath =
|
|
1347
|
+
const filePath = resolve6(assetsDir3, relativePath);
|
|
1067
1348
|
if (filePath !== assetsDir3 && !filePath.startsWith(assetsDir3 + "/"))
|
|
1068
1349
|
return c.text("Forbidden", 403);
|
|
1069
1350
|
const s = await fsStat(filePath).catch(() => null);
|
|
@@ -1072,7 +1353,7 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1072
1353
|
const body = await readFile2(filePath);
|
|
1073
1354
|
return c.body(body, 200, { "Content-Type": mime });
|
|
1074
1355
|
}
|
|
1075
|
-
if (
|
|
1356
|
+
if (existsSync3(indexPath)) {
|
|
1076
1357
|
const body = await readFile2(indexPath, "utf-8");
|
|
1077
1358
|
return c.html(body);
|
|
1078
1359
|
}
|
|
@@ -1085,7 +1366,7 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1085
1366
|
if (skillsDir3) {
|
|
1086
1367
|
let entries;
|
|
1087
1368
|
try {
|
|
1088
|
-
entries =
|
|
1369
|
+
entries = readdirSync2(skillsDir3, { withFileTypes: true });
|
|
1089
1370
|
} catch (err) {
|
|
1090
1371
|
logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
|
|
1091
1372
|
entries = [];
|
|
@@ -1093,10 +1374,10 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1093
1374
|
for (const entry of entries) {
|
|
1094
1375
|
if (!entry.isDirectory()) continue;
|
|
1095
1376
|
try {
|
|
1096
|
-
const skillPath =
|
|
1377
|
+
const skillPath = resolve6(skillsDir3, entry.name);
|
|
1097
1378
|
const sourceHash = hashSkillDir(skillPath);
|
|
1098
|
-
const destDir =
|
|
1099
|
-
if (
|
|
1379
|
+
const destDir = resolve6(sharedSkillsDir(), entry.name);
|
|
1380
|
+
if (existsSync3(destDir)) {
|
|
1100
1381
|
const destHash = hashSkillDir(destDir);
|
|
1101
1382
|
if (sourceHash === destHash) continue;
|
|
1102
1383
|
}
|
|
@@ -1120,11 +1401,11 @@ function resolveSkillsDir(manifest) {
|
|
|
1120
1401
|
if (!manifest.skillsDir) return null;
|
|
1121
1402
|
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
1122
1403
|
for (let i = 0; i < 5; i++) {
|
|
1123
|
-
const candidate =
|
|
1124
|
-
if (
|
|
1404
|
+
const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "skills");
|
|
1405
|
+
if (existsSync3(candidate)) return candidate;
|
|
1125
1406
|
searchDir = dirname(searchDir);
|
|
1126
1407
|
}
|
|
1127
|
-
if (
|
|
1408
|
+
if (existsSync3(manifest.skillsDir)) return manifest.skillsDir;
|
|
1128
1409
|
logger_default.warn(`skills dir not found for extension ${manifest.id}: ${manifest.skillsDir}`);
|
|
1129
1410
|
return null;
|
|
1130
1411
|
}
|
|
@@ -1134,14 +1415,14 @@ function discoverBuiltinExtensions() {
|
|
|
1134
1415
|
async function discoverInstalledExtensions() {
|
|
1135
1416
|
const manifests = [];
|
|
1136
1417
|
const packages = readExtensionsConfig();
|
|
1137
|
-
const npmDir =
|
|
1418
|
+
const npmDir = resolve6(voluteHome(), "extensions", "_npm");
|
|
1138
1419
|
const { createRequire } = await import("module");
|
|
1139
1420
|
for (const pkg of packages) {
|
|
1140
1421
|
try {
|
|
1141
1422
|
let resolved = pkg;
|
|
1142
|
-
const npmPkgDir =
|
|
1143
|
-
if (
|
|
1144
|
-
const require2 = createRequire(
|
|
1423
|
+
const npmPkgDir = resolve6(npmDir, "node_modules", pkg);
|
|
1424
|
+
if (existsSync3(npmPkgDir)) {
|
|
1425
|
+
const require2 = createRequire(resolve6(npmDir, "noop.js"));
|
|
1145
1426
|
resolved = require2.resolve(pkg);
|
|
1146
1427
|
}
|
|
1147
1428
|
const mod = await import(resolved);
|
|
@@ -1184,19 +1465,19 @@ function validateManifest(manifest, source) {
|
|
|
1184
1465
|
}
|
|
1185
1466
|
async function discoverLocalExtensions() {
|
|
1186
1467
|
const baseDir = extensionsBaseDir();
|
|
1187
|
-
if (!
|
|
1468
|
+
if (!existsSync3(baseDir)) return [];
|
|
1188
1469
|
const manifests = [];
|
|
1189
1470
|
let entries;
|
|
1190
1471
|
try {
|
|
1191
|
-
entries =
|
|
1472
|
+
entries = readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
|
|
1192
1473
|
} catch (err) {
|
|
1193
1474
|
logger_default.error("failed to read local extensions directory", logger_default.errorData(err));
|
|
1194
1475
|
return [];
|
|
1195
1476
|
}
|
|
1196
1477
|
for (const dir of entries) {
|
|
1197
|
-
const extDir =
|
|
1198
|
-
const candidates = [
|
|
1199
|
-
const entryPoint = candidates.find((p) =>
|
|
1478
|
+
const extDir = resolve6(baseDir, dir);
|
|
1479
|
+
const candidates = [resolve6(extDir, "src", "index.js"), resolve6(extDir, "index.js")];
|
|
1480
|
+
const entryPoint = candidates.find((p) => existsSync3(p));
|
|
1200
1481
|
if (!entryPoint) continue;
|
|
1201
1482
|
try {
|
|
1202
1483
|
const mod = await import(entryPoint);
|
|
@@ -1228,17 +1509,43 @@ async function loadAllExtensions(app, authMw) {
|
|
|
1228
1509
|
logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
|
|
1229
1510
|
}
|
|
1230
1511
|
}
|
|
1512
|
+
app.get("/api/extensions/commands", (c) => {
|
|
1513
|
+
const result = {};
|
|
1514
|
+
for (const { manifest } of loaded) {
|
|
1515
|
+
if (!manifest.commands) continue;
|
|
1516
|
+
const cmds = {};
|
|
1517
|
+
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1518
|
+
cmds[name] = { description: cmd.description, ...cmd.usage ? { usage: cmd.usage } : {} };
|
|
1519
|
+
}
|
|
1520
|
+
result[manifest.id] = { commands: cmds };
|
|
1521
|
+
}
|
|
1522
|
+
return c.json(result);
|
|
1523
|
+
});
|
|
1231
1524
|
}
|
|
1232
1525
|
function getLoadedExtensions() {
|
|
1233
|
-
return loaded.map(({ manifest }) =>
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1526
|
+
return loaded.map(({ manifest }) => {
|
|
1527
|
+
let commands;
|
|
1528
|
+
if (manifest.commands) {
|
|
1529
|
+
commands = {};
|
|
1530
|
+
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1531
|
+
commands[name] = {
|
|
1532
|
+
description: cmd.description,
|
|
1533
|
+
...cmd.usage ? { usage: cmd.usage } : {}
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
id: manifest.id,
|
|
1539
|
+
name: manifest.name,
|
|
1540
|
+
version: manifest.version,
|
|
1541
|
+
description: manifest.description,
|
|
1542
|
+
icon: manifest.icon,
|
|
1543
|
+
systemSection: manifest.ui?.systemSection,
|
|
1544
|
+
mindSections: manifest.ui?.mindSections,
|
|
1545
|
+
feedSource: manifest.ui?.feedSource,
|
|
1546
|
+
commands
|
|
1547
|
+
};
|
|
1548
|
+
});
|
|
1242
1549
|
}
|
|
1243
1550
|
function getExtensionStandardSkills() {
|
|
1244
1551
|
const skills = [];
|
|
@@ -1247,7 +1554,7 @@ function getExtensionStandardSkills() {
|
|
|
1247
1554
|
const dir = resolveSkillsDir(manifest);
|
|
1248
1555
|
if (!dir) continue;
|
|
1249
1556
|
try {
|
|
1250
|
-
for (const entry of
|
|
1557
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1251
1558
|
if (entry.isDirectory()) skills.push(entry.name);
|
|
1252
1559
|
}
|
|
1253
1560
|
} catch (err) {
|
|
@@ -1300,23 +1607,15 @@ function notifyExtensionsMindStop(mindName) {
|
|
|
1300
1607
|
}
|
|
1301
1608
|
|
|
1302
1609
|
export {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
changePassword,
|
|
1313
|
-
approveUser,
|
|
1314
|
-
countAdmins,
|
|
1315
|
-
setUserRole,
|
|
1316
|
-
deleteUser,
|
|
1317
|
-
updateUserProfile,
|
|
1318
|
-
syncMindProfile,
|
|
1319
|
-
migrateMindRoles,
|
|
1610
|
+
createTurn,
|
|
1611
|
+
getActiveTurnId,
|
|
1612
|
+
trackToolUse,
|
|
1613
|
+
getLastToolUseEventId,
|
|
1614
|
+
assignSession,
|
|
1615
|
+
completeTurn,
|
|
1616
|
+
setSummaryEventId,
|
|
1617
|
+
completeOrphanedTurns,
|
|
1618
|
+
clearMind,
|
|
1320
1619
|
readSystemsConfig,
|
|
1321
1620
|
writeSystemsConfig,
|
|
1322
1621
|
deleteSystemsConfig,
|