volute 0.34.0 → 0.35.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 +7 -6
- package/dist/accept-ZBDVVCEU.js +42 -0
- package/dist/{activity-events-BN7V6KCC.js → activity-events-ZW4SDL2C.js} +4 -4
- package/dist/{ai-service-PSILB5WD.js → ai-service-LURBEDDB.js} +5 -5
- package/dist/{api-client-XUXOB7LI.js → api-client-3A77HMH7.js} +1 -1
- package/dist/api.d.ts +1 -5618
- package/dist/{archive-C2VEMQOR.js → archive-ESU2FUN4.js} +3 -3
- package/dist/{auth-ZFZXJZDQ.js → auth-WX4TESEI.js} +5 -5
- package/dist/bridge-PXIO6PS2.js +206 -0
- package/dist/chat-QXAJF3FU.js +51 -0
- package/dist/{chunk-7F2SW2KD.js → chunk-2TGZJFAT.js} +3 -3
- package/dist/{chunk-6LXAAQ43.js → chunk-33ODGMFZ.js} +1 -1
- package/dist/{chunk-4JSR7YO7.js → chunk-5N7Y5WAM.js} +1 -1
- package/dist/{chunk-FYCALD4Q.js → chunk-5T5YMX6S.js} +1 -1
- package/dist/{chunk-B2BVAIZ4.js → chunk-5XJYUFZH.js} +21 -15
- package/dist/{chunk-M3K5AARV.js → chunk-A2ZLHBHG.js} +2 -2
- package/dist/{chunk-U5BTYSAL.js → chunk-AN2W47GW.js} +2 -2
- package/dist/{chunk-G53F3JA4.js → chunk-AOB6GVRM.js} +1 -1
- package/dist/{chunk-N7BLAHNE.js → chunk-BDYXIWA5.js} +5 -5
- package/dist/{chunk-YUIHSKR6.js → chunk-BKF4WQCY.js} +2 -2
- package/dist/{chunk-6OWJXUAR.js → chunk-BMZQYACC.js} +2 -2
- package/dist/{chunk-NAOW2CLO.js → chunk-BTY4WNFE.js} +1 -1
- package/dist/{chunk-MLOQKQNB.js → chunk-BV65KRHM.js} +2 -2
- package/dist/{chunk-XWXBJQBE.js → chunk-CORXD635.js} +4 -4
- package/dist/{chunk-PVY5W6QN.js → chunk-F7ZNLYKZ.js} +2 -2
- package/dist/{chunk-BFWHBQK4.js → chunk-FT5KETXZ.js} +3 -3
- package/dist/{chunk-N3DNFPVA.js → chunk-IJHIXLVN.js} +8 -8
- package/dist/{chunk-V6ZCNULL.js → chunk-J6CJQDWI.js} +37 -28
- package/dist/{chunk-4RQBJWQX.js → chunk-LOPXTW6H.js} +1 -1
- package/dist/{chunk-47ZPNLF4.js → chunk-MDJGMOSD.js} +8 -137
- package/dist/{chunk-BTWAGDV5.js → chunk-N446KRP7.js} +3 -3
- package/dist/{chunk-6WAWMWR5.js → chunk-N5LMGYXX.js} +2 -2
- package/dist/{chunk-G6BSYHPK.js → chunk-NJK5SDGR.js} +1 -1
- package/dist/{chunk-D424ZQGI.js → chunk-O7IGP7ZW.js} +11 -3
- package/dist/{chunk-2IOP6PHB.js → chunk-OTC67N2Z.js} +2 -2
- package/dist/{chunk-V45JXOWY.js → chunk-PWQ2ITYG.js} +4 -4
- package/dist/{chunk-KTLFDYPT.js → chunk-QCH6K235.js} +1 -1
- package/dist/chunk-QHG4OMZL.js +145 -0
- package/dist/{chunk-IS7WJ56Q.js → chunk-QWTR6AWZ.js} +3 -3
- package/dist/chunk-TXSA4Q3V.js +116 -0
- package/dist/{chunk-BDK73LK6.js → chunk-VHJRZM2S.js} +2 -2
- package/dist/{chunk-SSI47XP2.js → chunk-VHWGEJ4V.js} +1 -1
- package/dist/chunk-VY3RB2V7.js +164 -0
- package/dist/chunk-WJPROOU5.js +8314 -0
- package/dist/{chunk-E5C7OWZ2.js → chunk-WZRZFFCL.js} +8 -8
- package/dist/{chunk-BM474GX6.js → chunk-XRQSAMX2.js} +4 -4
- package/dist/{chunk-OYAKCAVY.js → chunk-ZSR72JB3.js} +1 -1
- package/dist/{chunk-PLDWHR4D.js → chunk-ZX7EAV5J.js} +17 -7
- package/dist/cli.js +90 -29
- package/dist/clock-HSEKS5AR.js +289 -0
- package/dist/{cloud-sync-TG3TIX5H.js → cloud-sync-6JL4C24T.js} +21 -22
- package/dist/config-UTS7QULS.js +76 -0
- package/dist/connectors/discord-bridge.js +3 -3
- package/dist/connectors/slack-bridge.js +3 -3
- package/dist/connectors/telegram-bridge.js +3 -3
- package/dist/{conversations-HL2JP5GI.js → conversations-2PW57WO2.js} +5 -5
- package/dist/create-5BPOOJAN.js +75 -0
- package/dist/create-UVCK2CS6.js +50 -0
- package/dist/daemon-client-RVIKXGFQ.js +12 -0
- package/dist/daemon-restart-HSZ3BCX5.js +65 -0
- package/dist/daemon.js +845 -1766
- package/dist/{db-PLEDCBHZ.js → db-BDMH4SZ2.js} +7 -3
- package/dist/{db-RYX3SS2W.js → db-BVBJ57TU.js} +2 -2
- package/dist/delete-L5PAVDGQ.js +42 -0
- package/dist/delivery-manager-H5ZVBMCQ.js +31 -0
- package/dist/{delivery-router-D5ELDMS2.js → delivery-router-HEJSJAHQ.js} +4 -4
- package/dist/down-74VXM45A.js +17 -0
- package/dist/env-E4XHO2BI.js +223 -0
- package/dist/{exec-DVLXKRIO.js → exec-PY7THYH4.js} +4 -4
- package/dist/export-OAS6QVBN.js +113 -0
- package/dist/{extension-PM42QCID.js → extension-D74CNM7G.js} +25 -33
- package/dist/{extensions-BBGVL5JC.js → extensions-XDDFY72A.js} +22 -11
- package/dist/files-CWTK6V3H.js +53 -0
- package/dist/import-5A3T7QV4.js +143 -0
- package/dist/{isolation-62MKDZN3.js → isolation-TK5RX2WM.js} +3 -3
- package/dist/join-DF5XSJAC.js +67 -0
- package/dist/list-PDMQM7ZV.js +53 -0
- package/dist/login-7TE6CIZF.js +60 -0
- package/dist/login-GOTAYLXP.js +51 -0
- package/dist/logout-6KIA74EV.js +29 -0
- package/dist/logout-T4XS6LRU.js +50 -0
- package/dist/message-delivery-GRC4W6P7.js +41 -0
- package/dist/mind-5IEYKV7I.js +97 -0
- package/dist/{mind-activity-tracker-2ACNHA7B.js → mind-activity-tracker-QBLIV7ZJ.js} +5 -5
- package/dist/{mind-history-WOYFLQAI.js → mind-history-IE2QH7U5.js} +82 -71
- package/dist/mind-list-GEWHWAL4.js +38 -0
- package/dist/mind-manager-HFLB5653.js +31 -0
- package/dist/mind-profile-DCBDVF5B.js +53 -0
- package/dist/mind-service-X2CAA6W6.js +37 -0
- package/dist/mind-sleep-ITCF6OQA.js +47 -0
- package/dist/mind-status-X4SX3YUG.js +65 -0
- package/dist/mind-wake-KXMKMGWX.js +42 -0
- package/dist/{package-V2WHWVG6.js → package-D2FSVFAX.js} +5 -5
- package/dist/read-67VRP2DO.js +91 -0
- package/dist/{read-stdin-PIRM6A2Y.js → read-stdin-3X5VYKNS.js} +1 -1
- package/dist/register-SB7NXCOE.js +51 -0
- package/dist/{registry-UYV5S6QT.js → registry-GBSNW3HG.js} +2 -2
- package/dist/reject-MUR2KWJ4.js +40 -0
- package/dist/restart-5EGG4JXU.js +42 -0
- package/dist/{sandbox-SI5HMBP3.js → sandbox-R37VIU36.js} +5 -5
- package/dist/scheduler-Y7O4CJXL.js +31 -0
- package/dist/{schema-ETMABTW4.js → schema-XVZ2CLKW.js} +1 -1
- package/dist/{seed-WNGI6PNW.js → seed-EQORWX77.js} +2 -2
- package/dist/seed-check-KJNTL72M.js +35 -0
- package/dist/seed-cmd-ZM2XGVU2.js +30 -0
- package/dist/seed-create-DRWGGHEI.js +113 -0
- package/dist/seed-sprout-JYXGXOP3.js +148 -0
- package/dist/send-JBJJQ7CA.js +409 -0
- package/dist/service-WNPCNHOX.js +121 -0
- package/dist/{setup-Z3DEVWV7.js → setup-BJ4YAY26.js} +153 -127
- package/dist/{setup-GGMKENLN.js → setup-RHJRFURI.js} +3 -3
- package/dist/skill-TAAKEYBV.js +389 -0
- package/dist/skills/volute-mind/SKILL.md +3 -7
- package/dist/skills/volute-mind/references/extensions.md +8 -11
- package/dist/{skills-Q6VZ2UGD.js → skills-EKMCQ46K.js} +7 -7
- package/dist/sleep-manager-7KFK3USC.js +35 -0
- package/dist/spirit-ZFRDXMG7.js +23 -0
- package/dist/split-AWVOYOPZ.js +64 -0
- package/dist/{sprout-E3HJIV2Z.js → sprout-HE4TITMK.js} +2 -2
- package/dist/start-3UXOPXQG.js +39 -0
- package/dist/status-ZK34WYIM.js +125 -0
- package/dist/stop-3XYIBGFM.js +41 -0
- package/dist/system-chat-IDPHYHY4.js +35 -0
- package/dist/systems-O43WGQY6.js +52 -0
- package/dist/{tailscale-ZEUK7GKZ.js → tailscale-ZIZ2HWJ5.js} +4 -4
- package/dist/{template-hash-EJRTKE36.js → template-hash-A7FNHTB7.js} +2 -2
- package/dist/up-77ICEDEW.js +19 -0
- package/dist/update-ANE5ZM7F.js +225 -0
- package/dist/{update-check-X3YG4WVP.js → update-check-UV55CBEP.js} +3 -3
- package/dist/upgrade-ZMDGC7M2.js +74 -0
- package/dist/variant-QWL2WSRI.js +62 -0
- package/dist/{version-notify-YCH4UVQ2.js → version-notify-FXSEMXWW.js} +28 -27
- package/dist/{volute-config-WBKYJGYQ.js → volute-config-D2XVS2YI.js} +1 -1
- package/dist/web-assets/assets/index-BhxWKvbB.css +1 -0
- package/dist/web-assets/assets/index-CHVKJ9II.js +75 -0
- package/dist/web-assets/index.html +2 -2
- package/dist/web-assets/sw.js +117 -0
- package/package.json +5 -5
- package/packages/extensions/pages/dist/ui/assets/index-DKZLNMED.js +2 -0
- package/packages/extensions/pages/dist/ui/index.html +1 -1
- package/packages/extensions/pages/skills/pages/SKILL.md +84 -9
- package/templates/_base/src/lib/auto-commit.ts +8 -8
- package/templates/_base/src/lib/volute-server.ts +6 -0
- package/templates/claude/src/agent.ts +8 -1
- package/dist/accept-TW6V4WI4.js +0 -42
- package/dist/bridge-O753D5F4.js +0 -207
- package/dist/chat-BHYX7DJ4.js +0 -68
- package/dist/chunk-47XDEWWV.js +0 -156
- package/dist/chunk-CVL5IGIR.js +0 -2084
- package/dist/chunk-PB65JZK2.js +0 -85
- package/dist/chunk-TAHX36HZ.js +0 -3679
- package/dist/clock-3X4DSC2N.js +0 -281
- package/dist/config-OROA5DUA.js +0 -72
- package/dist/create-3SEKKI6P.js +0 -71
- package/dist/create-UOSOQ2HN.js +0 -44
- package/dist/daemon-client-WOAQXXBM.js +0 -12
- package/dist/daemon-restart-5ABHNXJZ.js +0 -52
- package/dist/delete-KYOVWR23.js +0 -35
- package/dist/delivery-manager-2BR5NZKF.js +0 -32
- package/dist/down-QVFN4UPK.js +0 -15
- package/dist/env-R34DT7XL.js +0 -195
- package/dist/export-6ZXAXATG.js +0 -112
- package/dist/files-VQV2VZQO.js +0 -47
- package/dist/import-MK2I2T6F.js +0 -23
- package/dist/join-DGYHTJUH.js +0 -66
- package/dist/list-C644WTHV.js +0 -49
- package/dist/login-IIGEQPHL.js +0 -47
- package/dist/login-KZQLMAWE.js +0 -47
- package/dist/logout-AGTZVRGP.js +0 -40
- package/dist/logout-KD6GXIJJ.js +0 -21
- package/dist/message-delivery-V3R6NXJP.js +0 -42
- package/dist/mind-BI4EPBVZ.js +0 -108
- package/dist/mind-list-6VPM7GUQ.js +0 -30
- package/dist/mind-manager-MWW3BTS4.js +0 -32
- package/dist/mind-profile-WPG42U5Y.js +0 -47
- package/dist/mind-service-VIKZJK2M.js +0 -38
- package/dist/mind-sleep-XDISJY74.js +0 -42
- package/dist/mind-status-7FTZWPZF.js +0 -56
- package/dist/mind-wake-KIIKEI3A.js +0 -37
- package/dist/read-H5C26YO7.js +0 -85
- package/dist/register-J27WP33N.js +0 -47
- package/dist/reject-OEANJYIA.js +0 -40
- package/dist/restart-V5EGYBJG.js +0 -33
- package/dist/scheduler-AGG3L2FO.js +0 -32
- package/dist/seed-check-PXTH7YXS.js +0 -32
- package/dist/seed-cmd-VENFTGS3.js +0 -36
- package/dist/seed-create-663ALOKH.js +0 -112
- package/dist/seed-sprout-EH3AGKAI.js +0 -132
- package/dist/send-7FUUUZZH.js +0 -386
- package/dist/skill-DKNYJS4P.js +0 -362
- package/dist/skills/shared-files/SKILL.md +0 -44
- package/dist/skills/shared-files/scripts/merge.ts +0 -72
- package/dist/skills/shared-files/scripts/pull.ts +0 -52
- package/dist/sleep-manager-BJK2ROPX.js +0 -36
- package/dist/spirit-4JP4TY4C.js +0 -23
- package/dist/split-3YPMS2CL.js +0 -63
- package/dist/start-W3TPKX4D.js +0 -33
- package/dist/status-4OVFXFEJ.js +0 -115
- package/dist/stop-GTT6YWYO.js +0 -32
- package/dist/system-channel-DXD2JBOU.js +0 -36
- package/dist/system-chat-TYLOL7SX.js +0 -36
- package/dist/systems-AYLO727G.js +0 -61
- package/dist/up-PA7F2CXE.js +0 -18
- package/dist/update-HG4LCUSG.js +0 -215
- package/dist/upgrade-YGNIDICG.js +0 -67
- package/dist/variant-MZUMRTQO.js +0 -41
- package/dist/web-assets/assets/index-DiiwC-CZ.css +0 -1
- package/dist/web-assets/assets/index-d6y5b9Ij.js +0 -75
- package/packages/extensions/pages/dist/ui/assets/index-tLTROSk5.js +0 -2
package/dist/chunk-CVL5IGIR.js
DELETED
|
@@ -1,2084 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
hashSkillDir,
|
|
4
|
-
importSkillFromDir,
|
|
5
|
-
removeSharedSkill,
|
|
6
|
-
sharedSkillsDir
|
|
7
|
-
} from "./chunk-N3DNFPVA.js";
|
|
8
|
-
import {
|
|
9
|
-
getAllSites,
|
|
10
|
-
getPublishedPages,
|
|
11
|
-
getRecentPages,
|
|
12
|
-
initDb,
|
|
13
|
-
syncPublishedPages
|
|
14
|
-
} from "./chunk-PB65JZK2.js";
|
|
15
|
-
import {
|
|
16
|
-
getUser,
|
|
17
|
-
getUserByUsername
|
|
18
|
-
} from "./chunk-BM474GX6.js";
|
|
19
|
-
import {
|
|
20
|
-
publish
|
|
21
|
-
} from "./chunk-XWXBJQBE.js";
|
|
22
|
-
import {
|
|
23
|
-
logger_default
|
|
24
|
-
} from "./chunk-YUIHSKR6.js";
|
|
25
|
-
import {
|
|
26
|
-
readGlobalConfig,
|
|
27
|
-
writeGlobalConfig
|
|
28
|
-
} from "./chunk-6OWJXUAR.js";
|
|
29
|
-
import {
|
|
30
|
-
mindDir,
|
|
31
|
-
voluteHome,
|
|
32
|
-
voluteSystemDir
|
|
33
|
-
} from "./chunk-N7BLAHNE.js";
|
|
34
|
-
|
|
35
|
-
// src/lib/extensions.ts
|
|
36
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
37
|
-
import { dirname, resolve as resolve7 } from "path";
|
|
38
|
-
|
|
39
|
-
// packages/extensions/notes/src/index.ts
|
|
40
|
-
import { resolve } from "path";
|
|
41
|
-
|
|
42
|
-
// packages/extensions/sdk/src/index.ts
|
|
43
|
-
var VALID_EXTENSION_ID = /^[a-z0-9][a-z0-9_-]*$/;
|
|
44
|
-
function createExtension(manifest) {
|
|
45
|
-
if (!manifest.id) throw new Error("Extension manifest requires an id");
|
|
46
|
-
if (!VALID_EXTENSION_ID.test(manifest.id))
|
|
47
|
-
throw new Error(
|
|
48
|
-
"Extension id must be lowercase alphanumeric with hyphens/underscores, starting with a letter or digit"
|
|
49
|
-
);
|
|
50
|
-
if (typeof manifest.routes !== "function")
|
|
51
|
-
throw new Error("Extension manifest requires a routes function");
|
|
52
|
-
return manifest;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// packages/extensions/notes/src/notes.ts
|
|
56
|
-
function slugify(text) {
|
|
57
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
58
|
-
}
|
|
59
|
-
async function createNote(db, getUser2, authorId, title, content, replyToId) {
|
|
60
|
-
let slug = slugify(title) || "untitled";
|
|
61
|
-
const existing = db.prepare("SELECT slug FROM notes WHERE author_id = ?").all(authorId);
|
|
62
|
-
const existingSlugs = new Set(existing.map((r) => r.slug));
|
|
63
|
-
if (existingSlugs.has(slug)) {
|
|
64
|
-
let i = 2;
|
|
65
|
-
while (existingSlugs.has(`${slug}-${i}`)) i++;
|
|
66
|
-
slug = `${slug}-${i}`;
|
|
67
|
-
}
|
|
68
|
-
const row = db.prepare(
|
|
69
|
-
`INSERT INTO notes (author_id, title, slug, content, reply_to_id)
|
|
70
|
-
VALUES (?, ?, ?, ?, ?)
|
|
71
|
-
RETURNING *`
|
|
72
|
-
).get(authorId, title, slug, content, replyToId ?? null);
|
|
73
|
-
const author = await getUser2(authorId);
|
|
74
|
-
return {
|
|
75
|
-
...row,
|
|
76
|
-
author_username: author?.username ?? "unknown",
|
|
77
|
-
author_display_name: author?.display_name ?? null,
|
|
78
|
-
comment_count: 0
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
async function getNote(db, getUser2, getUserByUsername2, authorUsername, slug) {
|
|
82
|
-
const author = await getUserByUsername2(authorUsername);
|
|
83
|
-
if (!author) return null;
|
|
84
|
-
const row = db.prepare("SELECT * FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
85
|
-
if (!row) return null;
|
|
86
|
-
const comments = await getComments(db, getUser2, row.id);
|
|
87
|
-
const reactions = await getReactions(db, getUser2, row.id);
|
|
88
|
-
let reply_to = null;
|
|
89
|
-
if (row.reply_to_id) {
|
|
90
|
-
const parent = db.prepare("SELECT * FROM notes WHERE id = ?").get(row.reply_to_id);
|
|
91
|
-
if (parent) {
|
|
92
|
-
const parentAuthor = await getUser2(parent.author_id);
|
|
93
|
-
reply_to = {
|
|
94
|
-
author_username: parentAuthor?.username ?? "unknown",
|
|
95
|
-
slug: parent.slug,
|
|
96
|
-
title: parent.title
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const replyRows = db.prepare("SELECT * FROM notes WHERE reply_to_id = ? ORDER BY created_at").all(row.id);
|
|
101
|
-
const replies = [];
|
|
102
|
-
for (const r of replyRows) {
|
|
103
|
-
const replyAuthor = await getUser2(r.author_id);
|
|
104
|
-
replies.push({
|
|
105
|
-
author_username: replyAuthor?.username ?? "unknown",
|
|
106
|
-
slug: r.slug,
|
|
107
|
-
title: r.title,
|
|
108
|
-
created_at: r.created_at
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
...row,
|
|
113
|
-
author_username: authorUsername,
|
|
114
|
-
author_display_name: author.display_name ?? null,
|
|
115
|
-
comment_count: comments.length,
|
|
116
|
-
comments,
|
|
117
|
-
reactions,
|
|
118
|
-
reply_to,
|
|
119
|
-
replies
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
async function listNotes(db, getUser2, getUserByUsername2, opts) {
|
|
123
|
-
const limit = opts?.limit ?? 50;
|
|
124
|
-
const offset = opts?.offset ?? 0;
|
|
125
|
-
let authorId;
|
|
126
|
-
if (opts?.authorUsername) {
|
|
127
|
-
const author = await getUserByUsername2(opts.authorUsername);
|
|
128
|
-
if (!author) return [];
|
|
129
|
-
authorId = author.id;
|
|
130
|
-
}
|
|
131
|
-
const rows = authorId ? db.prepare(
|
|
132
|
-
"SELECT * FROM notes WHERE author_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
133
|
-
).all(authorId, limit, offset) : db.prepare("SELECT * FROM notes ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
134
|
-
if (rows.length === 0) return [];
|
|
135
|
-
const noteIds = rows.map((r) => r.id);
|
|
136
|
-
const commentCounts = db.prepare(
|
|
137
|
-
`SELECT note_id, COUNT(*) as count FROM note_comments
|
|
138
|
-
WHERE note_id IN (${noteIds.map(() => "?").join(",")})
|
|
139
|
-
GROUP BY note_id`
|
|
140
|
-
).all(...noteIds);
|
|
141
|
-
const countMap = new Map(commentCounts.map((r) => [r.note_id, r.count]));
|
|
142
|
-
const allReactions = db.prepare(
|
|
143
|
-
`SELECT note_id, emoji, COUNT(*) as count FROM note_reactions
|
|
144
|
-
WHERE note_id IN (${noteIds.map(() => "?").join(",")})
|
|
145
|
-
GROUP BY note_id, emoji`
|
|
146
|
-
).all(...noteIds);
|
|
147
|
-
const reactionMap = /* @__PURE__ */ new Map();
|
|
148
|
-
for (const r of allReactions) {
|
|
149
|
-
if (!reactionMap.has(r.note_id)) reactionMap.set(r.note_id, []);
|
|
150
|
-
reactionMap.get(r.note_id).push({ emoji: r.emoji, count: r.count });
|
|
151
|
-
}
|
|
152
|
-
const replyToIds = [
|
|
153
|
-
...new Set(rows.filter((r) => r.reply_to_id).map((r) => r.reply_to_id))
|
|
154
|
-
];
|
|
155
|
-
const replyToMap = /* @__PURE__ */ new Map();
|
|
156
|
-
if (replyToIds.length > 0) {
|
|
157
|
-
const parents = db.prepare(`SELECT * FROM notes WHERE id IN (${replyToIds.map(() => "?").join(",")})`).all(...replyToIds);
|
|
158
|
-
for (const parent of parents) {
|
|
159
|
-
const parentAuthor = await getUser2(parent.author_id);
|
|
160
|
-
replyToMap.set(parent.id, {
|
|
161
|
-
author_username: parentAuthor?.username ?? "unknown",
|
|
162
|
-
slug: parent.slug,
|
|
163
|
-
title: parent.title
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const authorCache = /* @__PURE__ */ new Map();
|
|
168
|
-
const result = [];
|
|
169
|
-
for (const r of rows) {
|
|
170
|
-
if (!authorCache.has(r.author_id)) {
|
|
171
|
-
const u = await getUser2(r.author_id);
|
|
172
|
-
authorCache.set(r.author_id, {
|
|
173
|
-
username: u?.username ?? "unknown",
|
|
174
|
-
display_name: u?.display_name ?? null
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
const authorInfo = authorCache.get(r.author_id);
|
|
178
|
-
const reactions = reactionMap.get(r.id);
|
|
179
|
-
const topReactions = reactions ? reactions.sort((a, b) => b.count - a.count).slice(0, 3).map((rx) => ({ ...rx, usernames: [] })) : void 0;
|
|
180
|
-
result.push({
|
|
181
|
-
...r,
|
|
182
|
-
author_username: authorInfo.username,
|
|
183
|
-
author_display_name: authorInfo.display_name,
|
|
184
|
-
comment_count: countMap.get(r.id) ?? 0,
|
|
185
|
-
reactions: topReactions,
|
|
186
|
-
reply_to: r.reply_to_id ? replyToMap.get(r.reply_to_id) ?? null : null
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
async function updateNote(db, getUser2, getUserByUsername2, authorUsername, slug, updates) {
|
|
192
|
-
const author = await getUserByUsername2(authorUsername);
|
|
193
|
-
if (!author) return null;
|
|
194
|
-
const existing = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
195
|
-
if (!existing) return null;
|
|
196
|
-
const sets = ["updated_at = datetime('now')"];
|
|
197
|
-
const params = [];
|
|
198
|
-
if (updates.title !== void 0) {
|
|
199
|
-
sets.push("title = ?");
|
|
200
|
-
params.push(updates.title);
|
|
201
|
-
}
|
|
202
|
-
if (updates.content !== void 0) {
|
|
203
|
-
sets.push("content = ?");
|
|
204
|
-
params.push(updates.content);
|
|
205
|
-
}
|
|
206
|
-
params.push(existing.id);
|
|
207
|
-
db.prepare(`UPDATE notes SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
208
|
-
const full = await getNote(db, getUser2, getUserByUsername2, authorUsername, slug);
|
|
209
|
-
if (!full) return null;
|
|
210
|
-
const { comments, replies, ...note } = full;
|
|
211
|
-
return note;
|
|
212
|
-
}
|
|
213
|
-
async function deleteNote(db, getUserByUsername2, authorUsername, slug, authorId) {
|
|
214
|
-
const author = await getUserByUsername2(authorUsername);
|
|
215
|
-
if (!author) return false;
|
|
216
|
-
const existing = db.prepare("SELECT id, author_id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
217
|
-
if (!existing || existing.author_id !== authorId) return false;
|
|
218
|
-
db.prepare("DELETE FROM notes WHERE id = ?").run(existing.id);
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
async function addComment(db, getUser2, noteId, authorId, content) {
|
|
222
|
-
const row = db.prepare(`INSERT INTO note_comments (note_id, author_id, content) VALUES (?, ?, ?) RETURNING *`).get(noteId, authorId, content);
|
|
223
|
-
const author = await getUser2(authorId);
|
|
224
|
-
return {
|
|
225
|
-
...row,
|
|
226
|
-
author_username: author?.username ?? "unknown",
|
|
227
|
-
author_display_name: author?.display_name ?? null
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
async function getComments(db, getUser2, noteId) {
|
|
231
|
-
const rows = db.prepare("SELECT * FROM note_comments WHERE note_id = ? ORDER BY created_at").all(noteId);
|
|
232
|
-
const result = [];
|
|
233
|
-
for (const row of rows) {
|
|
234
|
-
const author = await getUser2(row.author_id);
|
|
235
|
-
result.push({
|
|
236
|
-
...row,
|
|
237
|
-
author_username: author?.username ?? "unknown",
|
|
238
|
-
author_display_name: author?.display_name ?? null
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
async function deleteComment(db, commentId, authorId) {
|
|
244
|
-
const existing = db.prepare("SELECT id, author_id FROM note_comments WHERE id = ?").get(commentId);
|
|
245
|
-
if (!existing || existing.author_id !== authorId) return false;
|
|
246
|
-
db.prepare("DELETE FROM note_comments WHERE id = ?").run(existing.id);
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
function toggleReaction(db, noteId, userId, emoji) {
|
|
250
|
-
const existing = db.prepare("SELECT id FROM note_reactions WHERE note_id = ? AND user_id = ? AND emoji = ?").get(noteId, userId, emoji);
|
|
251
|
-
if (existing) {
|
|
252
|
-
db.prepare("DELETE FROM note_reactions WHERE id = ?").run(existing.id);
|
|
253
|
-
return { added: false };
|
|
254
|
-
}
|
|
255
|
-
db.prepare("INSERT INTO note_reactions (note_id, user_id, emoji) VALUES (?, ?, ?)").run(
|
|
256
|
-
noteId,
|
|
257
|
-
userId,
|
|
258
|
-
emoji
|
|
259
|
-
);
|
|
260
|
-
return { added: true };
|
|
261
|
-
}
|
|
262
|
-
async function getReactions(db, getUser2, noteId) {
|
|
263
|
-
const rows = db.prepare("SELECT * FROM note_reactions WHERE note_id = ? ORDER BY emoji").all(noteId);
|
|
264
|
-
const userCache = /* @__PURE__ */ new Map();
|
|
265
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
266
|
-
for (const r of rows) {
|
|
267
|
-
if (!grouped.has(r.emoji)) grouped.set(r.emoji, []);
|
|
268
|
-
grouped.get(r.emoji).push(r.user_id);
|
|
269
|
-
}
|
|
270
|
-
const result = [];
|
|
271
|
-
for (const [emoji, userIds] of grouped) {
|
|
272
|
-
const usernames = [];
|
|
273
|
-
for (const uid of userIds) {
|
|
274
|
-
if (!userCache.has(uid)) {
|
|
275
|
-
const u = await getUser2(uid);
|
|
276
|
-
userCache.set(uid, u?.username ?? "unknown");
|
|
277
|
-
}
|
|
278
|
-
usernames.push(userCache.get(uid));
|
|
279
|
-
}
|
|
280
|
-
result.push({ emoji, count: userIds.length, usernames });
|
|
281
|
-
}
|
|
282
|
-
return result;
|
|
283
|
-
}
|
|
284
|
-
async function resolveNoteId(db, getUserByUsername2, authorSlug) {
|
|
285
|
-
const [authorName, slug] = authorSlug.split("/", 2);
|
|
286
|
-
if (!authorName || !slug) return null;
|
|
287
|
-
const author = await getUserByUsername2(authorName);
|
|
288
|
-
if (!author) return null;
|
|
289
|
-
const row = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
290
|
-
return row?.id ?? null;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// packages/extensions/notes/src/commands.ts
|
|
294
|
-
function getFlag(args, flag) {
|
|
295
|
-
const idx = args.indexOf(flag);
|
|
296
|
-
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
297
|
-
return void 0;
|
|
298
|
-
}
|
|
299
|
-
function createCommands() {
|
|
300
|
-
return {
|
|
301
|
-
write: {
|
|
302
|
-
description: "Write a new note",
|
|
303
|
-
usage: 'volute notes write "title" ["content"] [--reply-to author/slug] (content can be piped via stdin)',
|
|
304
|
-
handler: async (args, ctx) => {
|
|
305
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
306
|
-
const mindName = ctx.mindName;
|
|
307
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
308
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
309
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
310
|
-
const title = args[0];
|
|
311
|
-
const content = args[1] ?? ctx.stdin;
|
|
312
|
-
if (!title || !content)
|
|
313
|
-
return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
|
|
314
|
-
let replyToId;
|
|
315
|
-
const replyTo = getFlag(args, "--reply-to");
|
|
316
|
-
if (replyTo) {
|
|
317
|
-
const id = await resolveNoteId(ctx.db, ctx.getUserByUsername, replyTo);
|
|
318
|
-
if (id === null) return { error: `Reply target not found: ${replyTo}` };
|
|
319
|
-
replyToId = id;
|
|
320
|
-
}
|
|
321
|
-
const note = await createNote(ctx.db, ctx.getUser, user.id, title, content, replyToId);
|
|
322
|
-
ctx.publishActivity({
|
|
323
|
-
type: "note_created",
|
|
324
|
-
mind: user.username,
|
|
325
|
-
summary: `${user.username} wrote "${title}"`,
|
|
326
|
-
metadata: { author: user.username, slug: note.slug, bodyHtml: content.slice(0, 500) }
|
|
327
|
-
});
|
|
328
|
-
return { output: `Published: ${note.author_username}/${note.slug}` };
|
|
329
|
-
}
|
|
330
|
-
},
|
|
331
|
-
list: {
|
|
332
|
-
description: "List notes",
|
|
333
|
-
usage: "volute notes list [--author name] [--limit N]",
|
|
334
|
-
handler: async (args, ctx) => {
|
|
335
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
336
|
-
const author = getFlag(args, "--author");
|
|
337
|
-
const limit = parseInt(getFlag(args, "--limit") ?? "10", 10);
|
|
338
|
-
const notes = await listNotes(ctx.db, ctx.getUser, ctx.getUserByUsername, {
|
|
339
|
-
authorUsername: author,
|
|
340
|
-
limit
|
|
341
|
-
});
|
|
342
|
-
if (notes.length === 0) return { output: "No notes found." };
|
|
343
|
-
const lines = notes.map((n) => {
|
|
344
|
-
const date = new Date(n.created_at).toLocaleDateString();
|
|
345
|
-
return ` ${n.author_username}/${n.slug} "${n.title}" (${date})`;
|
|
346
|
-
});
|
|
347
|
-
return { output: lines.join("\n") };
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
read: {
|
|
351
|
-
description: "Read a note",
|
|
352
|
-
usage: "volute notes read <author/slug>",
|
|
353
|
-
handler: async (args, ctx) => {
|
|
354
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
355
|
-
const ref = args[0];
|
|
356
|
-
if (!ref || !ref.includes("/")) return { error: "Usage: volute notes read <author/slug>" };
|
|
357
|
-
const [author, slug] = ref.split("/", 2);
|
|
358
|
-
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
359
|
-
if (!note) return { error: "Note not found" };
|
|
360
|
-
const lines = [
|
|
361
|
-
`# ${note.title}
|
|
362
|
-
`,
|
|
363
|
-
`By ${note.author_username} \u2014 ${new Date(note.created_at).toLocaleString()}
|
|
364
|
-
`,
|
|
365
|
-
note.content
|
|
366
|
-
];
|
|
367
|
-
if (note.reactions?.length) {
|
|
368
|
-
lines.push(
|
|
369
|
-
`
|
|
370
|
-
Reactions: ${note.reactions.map((r) => `${r.emoji} (${r.count})`).join(" ")}`
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
if (note.comments?.length) {
|
|
374
|
-
lines.push(`
|
|
375
|
-
Comments (${note.comments.length}):`);
|
|
376
|
-
for (const c of note.comments) {
|
|
377
|
-
lines.push(` ${c.author_username}: ${c.content}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return { output: lines.join("\n") };
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
comment: {
|
|
384
|
-
description: "Comment on a note",
|
|
385
|
-
usage: 'volute notes comment <author/slug> ["content"] (content can be piped via stdin)',
|
|
386
|
-
handler: async (args, ctx) => {
|
|
387
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
388
|
-
const mindName = ctx.mindName;
|
|
389
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
390
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
391
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
392
|
-
const ref = args[0];
|
|
393
|
-
const content = args[1] ?? ctx.stdin;
|
|
394
|
-
if (!ref || !ref.includes("/") || !content) {
|
|
395
|
-
return { error: 'Usage: volute notes comment <author/slug> "content"' };
|
|
396
|
-
}
|
|
397
|
-
const [author, slug] = ref.split("/", 2);
|
|
398
|
-
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
399
|
-
if (!note) return { error: "Note not found" };
|
|
400
|
-
await addComment(ctx.db, ctx.getUser, note.id, user.id, content);
|
|
401
|
-
return { output: "Comment added." };
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
react: {
|
|
405
|
-
description: "React to a note",
|
|
406
|
-
usage: 'volute notes react <author/slug> "emoji"',
|
|
407
|
-
handler: async (args, ctx) => {
|
|
408
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
409
|
-
const mindName = ctx.mindName;
|
|
410
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
411
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
412
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
413
|
-
const ref = args[0];
|
|
414
|
-
const emoji = args[1];
|
|
415
|
-
if (!ref || !ref.includes("/") || !emoji) {
|
|
416
|
-
return { error: 'Usage: volute notes react <author/slug> "emoji"' };
|
|
417
|
-
}
|
|
418
|
-
const [author, slug] = ref.split("/", 2);
|
|
419
|
-
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
420
|
-
if (!note) return { error: "Note not found" };
|
|
421
|
-
const result = toggleReaction(ctx.db, note.id, user.id, emoji);
|
|
422
|
-
return { output: result.added ? "Reaction added." : "Reaction removed." };
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
delete: {
|
|
426
|
-
description: "Delete your own note",
|
|
427
|
-
usage: "volute notes delete <author/slug>",
|
|
428
|
-
handler: async (args, ctx) => {
|
|
429
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
430
|
-
const mindName = ctx.mindName;
|
|
431
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
432
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
433
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
434
|
-
const ref = args[0];
|
|
435
|
-
if (!ref || !ref.includes("/"))
|
|
436
|
-
return { error: "Usage: volute notes delete <author/slug>" };
|
|
437
|
-
const [author, slug] = ref.split("/", 2);
|
|
438
|
-
const deleted = await deleteNote(ctx.db, ctx.getUserByUsername, author, slug, user.id);
|
|
439
|
-
if (!deleted) return { error: "Note not found or not authorized" };
|
|
440
|
-
return { output: "Note deleted." };
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// packages/extensions/notes/src/db.ts
|
|
447
|
-
function initDb2(db) {
|
|
448
|
-
db.exec(`
|
|
449
|
-
CREATE TABLE IF NOT EXISTS notes (
|
|
450
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
451
|
-
author_id INTEGER NOT NULL,
|
|
452
|
-
title TEXT NOT NULL,
|
|
453
|
-
slug TEXT NOT NULL,
|
|
454
|
-
content TEXT NOT NULL,
|
|
455
|
-
reply_to_id INTEGER,
|
|
456
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
457
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
458
|
-
);
|
|
459
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
|
|
460
|
-
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
|
|
461
|
-
CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
|
|
462
|
-
|
|
463
|
-
CREATE TABLE IF NOT EXISTS note_comments (
|
|
464
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
465
|
-
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
466
|
-
author_id INTEGER NOT NULL,
|
|
467
|
-
content TEXT NOT NULL,
|
|
468
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
469
|
-
);
|
|
470
|
-
CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
|
|
471
|
-
|
|
472
|
-
CREATE TABLE IF NOT EXISTS note_reactions (
|
|
473
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
474
|
-
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
475
|
-
user_id INTEGER NOT NULL,
|
|
476
|
-
emoji TEXT NOT NULL,
|
|
477
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
478
|
-
);
|
|
479
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
|
|
480
|
-
CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
|
|
481
|
-
`);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// packages/extensions/notes/src/routes.ts
|
|
485
|
-
import { Hono } from "hono";
|
|
486
|
-
async function parseJson(c) {
|
|
487
|
-
try {
|
|
488
|
-
return await c.req.json();
|
|
489
|
-
} catch {
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
function resolveUserId(c) {
|
|
494
|
-
const user = c.get("user");
|
|
495
|
-
if (!user || user.id === 0) return null;
|
|
496
|
-
return { id: user.id, username: user.username };
|
|
497
|
-
}
|
|
498
|
-
function createRoutes(ctx) {
|
|
499
|
-
if (!ctx.db) throw new Error("Notes extension requires a database");
|
|
500
|
-
const db = ctx.db;
|
|
501
|
-
const { getUser: getUser2, getUserByUsername: getUserByUsername2 } = ctx;
|
|
502
|
-
const app = new Hono().get("/", async (c) => {
|
|
503
|
-
const author = c.req.query("author");
|
|
504
|
-
const rawLimit = c.req.query("limit");
|
|
505
|
-
const rawOffset = c.req.query("offset");
|
|
506
|
-
const limit = rawLimit ? parseInt(rawLimit, 10) : void 0;
|
|
507
|
-
const offset = rawOffset ? parseInt(rawOffset, 10) : void 0;
|
|
508
|
-
if (limit !== void 0 && Number.isNaN(limit) || offset !== void 0 && Number.isNaN(offset)) {
|
|
509
|
-
return c.json({ error: "Invalid limit or offset parameter" }, 400);
|
|
510
|
-
}
|
|
511
|
-
const result = await listNotes(db, getUser2, getUserByUsername2, {
|
|
512
|
-
authorUsername: author,
|
|
513
|
-
limit,
|
|
514
|
-
offset
|
|
515
|
-
});
|
|
516
|
-
return c.json(result);
|
|
517
|
-
}).post("/", async (c) => {
|
|
518
|
-
const actor = resolveUserId(c);
|
|
519
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
520
|
-
const body = await parseJson(c);
|
|
521
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
522
|
-
if (!body.title || !body.content) {
|
|
523
|
-
return c.json({ error: "title and content are required" }, 400);
|
|
524
|
-
}
|
|
525
|
-
let replyToId;
|
|
526
|
-
if (body.reply_to) {
|
|
527
|
-
const id = await resolveNoteId(db, getUserByUsername2, body.reply_to);
|
|
528
|
-
if (id === null) return c.json({ error: `Reply target not found: ${body.reply_to}` }, 404);
|
|
529
|
-
replyToId = id;
|
|
530
|
-
}
|
|
531
|
-
const note = await createNote(db, getUser2, actor.id, body.title, body.content, replyToId);
|
|
532
|
-
ctx.publishActivity({
|
|
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
|
-
return c.json(note, 201);
|
|
543
|
-
}).get("/:author/:slug", async (c) => {
|
|
544
|
-
const { author, slug } = c.req.param();
|
|
545
|
-
const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
|
|
546
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
547
|
-
return c.json(note);
|
|
548
|
-
}).put("/:author/:slug", async (c) => {
|
|
549
|
-
const actor = resolveUserId(c);
|
|
550
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
551
|
-
const { author, slug } = c.req.param();
|
|
552
|
-
if (actor.username !== author) return c.json({ error: "Forbidden" }, 403);
|
|
553
|
-
const body = await parseJson(c);
|
|
554
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
555
|
-
const note = await updateNote(db, getUser2, getUserByUsername2, author, slug, body);
|
|
556
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
557
|
-
return c.json(note);
|
|
558
|
-
}).delete("/:author/:slug", async (c) => {
|
|
559
|
-
const actor = resolveUserId(c);
|
|
560
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
561
|
-
const { author, slug } = c.req.param();
|
|
562
|
-
const deleted = await deleteNote(db, getUserByUsername2, author, slug, actor.id);
|
|
563
|
-
if (!deleted) return c.json({ error: "Note not found or not authorized" }, 404);
|
|
564
|
-
return c.json({ ok: true });
|
|
565
|
-
}).post("/:author/:slug/reactions", async (c) => {
|
|
566
|
-
const actor = resolveUserId(c);
|
|
567
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
568
|
-
const { author, slug } = c.req.param();
|
|
569
|
-
const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
|
|
570
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
571
|
-
const body = await parseJson(c);
|
|
572
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
573
|
-
if (!body.emoji) return c.json({ error: "emoji is required" }, 400);
|
|
574
|
-
const result = toggleReaction(db, note.id, actor.id, body.emoji);
|
|
575
|
-
const reactions = await getReactions(db, getUser2, note.id);
|
|
576
|
-
return c.json({ ...result, reactions });
|
|
577
|
-
}).post("/:author/:slug/comments", async (c) => {
|
|
578
|
-
const actor = resolveUserId(c);
|
|
579
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
580
|
-
const { author, slug } = c.req.param();
|
|
581
|
-
const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
|
|
582
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
583
|
-
const body = await parseJson(c);
|
|
584
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
585
|
-
if (!body.content) return c.json({ error: "content is required" }, 400);
|
|
586
|
-
const comment = await addComment(db, getUser2, note.id, actor.id, body.content);
|
|
587
|
-
return c.json(comment, 201);
|
|
588
|
-
}).delete("/:author/:slug/comments/:id", async (c) => {
|
|
589
|
-
const actor = resolveUserId(c);
|
|
590
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
591
|
-
const commentId = parseInt(c.req.param("id"), 10);
|
|
592
|
-
if (Number.isNaN(commentId)) return c.json({ error: "Invalid comment ID" }, 400);
|
|
593
|
-
const deleted = await deleteComment(db, commentId, actor.id);
|
|
594
|
-
if (!deleted) return c.json({ error: "Comment not found or not authorized" }, 404);
|
|
595
|
-
return c.json({ ok: true });
|
|
596
|
-
}).get("/feed", async (c) => {
|
|
597
|
-
const rawFeedLimit = c.req.query("limit");
|
|
598
|
-
const limit = rawFeedLimit ? parseInt(rawFeedLimit, 10) : 8;
|
|
599
|
-
if (Number.isNaN(limit)) return c.json({ error: "Invalid limit parameter" }, 400);
|
|
600
|
-
const mind = c.req.query("mind");
|
|
601
|
-
const notes = await listNotes(db, getUser2, getUserByUsername2, {
|
|
602
|
-
limit,
|
|
603
|
-
...mind ? { authorUsername: mind } : {}
|
|
604
|
-
});
|
|
605
|
-
return c.json(
|
|
606
|
-
notes.map((n) => ({
|
|
607
|
-
id: `note-${n.author_username}-${n.slug}`,
|
|
608
|
-
title: n.title,
|
|
609
|
-
url: `/minds/${n.author_username}/notes/${n.slug}`,
|
|
610
|
-
date: n.created_at,
|
|
611
|
-
author: n.author_username,
|
|
612
|
-
bodyHtml: n.content,
|
|
613
|
-
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>',
|
|
614
|
-
color: "yellow"
|
|
615
|
-
}))
|
|
616
|
-
);
|
|
617
|
-
});
|
|
618
|
-
return app;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// packages/extensions/notes/src/index.ts
|
|
622
|
-
var assetsDir = resolve(import.meta.dirname, "../dist/ui");
|
|
623
|
-
var skillsDir = resolve(import.meta.dirname, "../skills");
|
|
624
|
-
var src_default = createExtension({
|
|
625
|
-
id: "notes",
|
|
626
|
-
name: "Notes",
|
|
627
|
-
version: "0.1.0",
|
|
628
|
-
description: "Public notes for sharing thoughts, reflections, and ideas",
|
|
629
|
-
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>',
|
|
630
|
-
color: "yellow",
|
|
631
|
-
routes: (ctx) => createRoutes(ctx),
|
|
632
|
-
commands: createCommands(),
|
|
633
|
-
initDb: initDb2,
|
|
634
|
-
skillsDir,
|
|
635
|
-
standardSkill: true,
|
|
636
|
-
ui: {
|
|
637
|
-
assetsDir,
|
|
638
|
-
systemSection: { id: "notes", label: "Notes", urlPatterns: ["/notes", "/notes/:author/:slug"] },
|
|
639
|
-
mindSections: [{ id: "notes", label: "Notes" }],
|
|
640
|
-
feedSource: {
|
|
641
|
-
endpoint: "/api/ext/notes/feed"
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
// packages/extensions/pages/src/index.ts
|
|
647
|
-
import { resolve as resolve4 } from "path";
|
|
648
|
-
|
|
649
|
-
// packages/extensions/pages/src/commands.ts
|
|
650
|
-
import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
651
|
-
import { relative, resolve as resolve2 } from "path";
|
|
652
|
-
function createCommands2() {
|
|
653
|
-
return {
|
|
654
|
-
publish: {
|
|
655
|
-
description: "Publish all pages (copy to public snapshot)",
|
|
656
|
-
usage: "volute pages publish [--remote]",
|
|
657
|
-
handler: async (args, ctx) => {
|
|
658
|
-
const mindName = ctx.mindName;
|
|
659
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
660
|
-
const remote = args.includes("--remote");
|
|
661
|
-
const mindDir2 = ctx.getMindDir(mindName);
|
|
662
|
-
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
663
|
-
const sourceDir = resolve2(mindDir2, "home", "public", "pages");
|
|
664
|
-
if (!existsSync(sourceDir))
|
|
665
|
-
return { error: "No pages directory found (home/public/pages/)" };
|
|
666
|
-
const db = ctx.db;
|
|
667
|
-
if (!db) return { error: "Database not available" };
|
|
668
|
-
const snapshotDir = resolve2(ctx.dataDir, "sites", mindName);
|
|
669
|
-
try {
|
|
670
|
-
if (existsSync(snapshotDir)) rmSync(snapshotDir, { recursive: true });
|
|
671
|
-
cpSync(sourceDir, snapshotDir, { recursive: true });
|
|
672
|
-
} catch (err) {
|
|
673
|
-
return { error: `Failed to publish snapshot: ${err.message}` };
|
|
674
|
-
}
|
|
675
|
-
const htmlFiles = collectHtmlFiles(snapshotDir, snapshotDir);
|
|
676
|
-
let diff;
|
|
677
|
-
try {
|
|
678
|
-
diff = syncPublishedPages(db, mindName, htmlFiles);
|
|
679
|
-
} catch (err) {
|
|
680
|
-
return { error: `Failed to update page database: ${err.message}` };
|
|
681
|
-
}
|
|
682
|
-
for (const file of diff.added) {
|
|
683
|
-
ctx.publishActivity({
|
|
684
|
-
type: "page_published",
|
|
685
|
-
mind: mindName,
|
|
686
|
-
summary: `${mindName} published ${file}`,
|
|
687
|
-
metadata: { file, iframeUrl: `/ext/pages/public/${mindName}/${file}` }
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
for (const file of diff.removed) {
|
|
691
|
-
ctx.publishActivity({
|
|
692
|
-
type: "page_removed",
|
|
693
|
-
mind: mindName,
|
|
694
|
-
summary: `${mindName} removed ${file}`,
|
|
695
|
-
metadata: { file }
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
let output = `Published ${htmlFiles.length} files`;
|
|
699
|
-
const parts = [];
|
|
700
|
-
if (diff.added.length > 0) parts.push(`${diff.added.length} new`);
|
|
701
|
-
if (diff.updated.length > 0) parts.push(`${diff.updated.length} updated`);
|
|
702
|
-
if (diff.removed.length > 0) parts.push(`${diff.removed.length} removed`);
|
|
703
|
-
if (parts.length > 0) output += ` (${parts.join(", ")})`;
|
|
704
|
-
if (remote) {
|
|
705
|
-
const config = ctx.getSystemsConfig();
|
|
706
|
-
if (!config)
|
|
707
|
-
return {
|
|
708
|
-
error: "Not connected to volute.systems. Run volute systems register or login first."
|
|
709
|
-
};
|
|
710
|
-
const allFiles = collectAllFiles(snapshotDir, snapshotDir);
|
|
711
|
-
const files = {};
|
|
712
|
-
for (const f of allFiles) {
|
|
713
|
-
const fp = resolve2(snapshotDir, f);
|
|
714
|
-
files[f] = readFileSync(fp).toString("base64");
|
|
715
|
-
}
|
|
716
|
-
try {
|
|
717
|
-
const res = await fetch(`${config.apiUrl}/api/pages/publish/${mindName}`, {
|
|
718
|
-
method: "PUT",
|
|
719
|
-
headers: {
|
|
720
|
-
"Content-Type": "application/json",
|
|
721
|
-
Authorization: `Bearer ${config.apiKey}`
|
|
722
|
-
},
|
|
723
|
-
body: JSON.stringify({ files })
|
|
724
|
-
});
|
|
725
|
-
const data = await res.json().catch(() => ({}));
|
|
726
|
-
if (!res.ok) {
|
|
727
|
-
const errMsg = data.error || `HTTP ${res.status}`;
|
|
728
|
-
output += `
|
|
729
|
-
Warning: remote publish failed: ${errMsg}`;
|
|
730
|
-
} else if (data.url) {
|
|
731
|
-
output += `
|
|
732
|
-
Remote: ${data.url}`;
|
|
733
|
-
}
|
|
734
|
-
} catch (err) {
|
|
735
|
-
output += `
|
|
736
|
-
Warning: remote publish failed: ${err.message}`;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
return { output };
|
|
740
|
-
}
|
|
741
|
-
},
|
|
742
|
-
list: {
|
|
743
|
-
description: "List pages with publish status",
|
|
744
|
-
usage: "volute pages list [--all]",
|
|
745
|
-
handler: async (args, ctx) => {
|
|
746
|
-
const mindName = ctx.mindName;
|
|
747
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
748
|
-
const db = ctx.db;
|
|
749
|
-
if (!db) return { error: "Database not available" };
|
|
750
|
-
const allFlag = args.includes("--all");
|
|
751
|
-
const port = process.env.VOLUTE_DAEMON_PORT || "1618";
|
|
752
|
-
if (allFlag) {
|
|
753
|
-
const { getAllSites: getAllSites2 } = await import("./db-PLEDCBHZ.js");
|
|
754
|
-
const sites = getAllSites2(db);
|
|
755
|
-
const lines2 = [];
|
|
756
|
-
for (const site of sites) {
|
|
757
|
-
for (const f of site.files) {
|
|
758
|
-
const url = `http://localhost:${port}/ext/pages/public/${site.mind}/${f.file}`;
|
|
759
|
-
lines2.push(`${site.mind.padEnd(15)} ${f.file.padEnd(25)} ${url}`);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
return { output: lines2.length > 0 ? lines2.join("\n") : "No published pages found." };
|
|
763
|
-
}
|
|
764
|
-
const mindDir2 = ctx.getMindDir(mindName);
|
|
765
|
-
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
766
|
-
const sourceDir = resolve2(mindDir2, "home", "public", "pages");
|
|
767
|
-
const published = new Set(getPublishedPages(db, mindName).map((p) => p.file));
|
|
768
|
-
const draftFiles = existsSync(sourceDir) ? collectHtmlFiles(sourceDir, sourceDir) : [];
|
|
769
|
-
const allFiles = /* @__PURE__ */ new Set([...published, ...draftFiles]);
|
|
770
|
-
if (allFiles.size === 0) return { output: "No pages found." };
|
|
771
|
-
const lines = [...allFiles].sort().map((file) => {
|
|
772
|
-
const isPublished = published.has(file);
|
|
773
|
-
const status = isPublished ? "published" : "draft";
|
|
774
|
-
const url = isPublished ? `http://localhost:${port}/ext/pages/public/${mindName}/${file}` : "";
|
|
775
|
-
return `${status.padEnd(11)} ${file.padEnd(25)} ${url}`;
|
|
776
|
-
});
|
|
777
|
-
return { output: lines.join("\n") };
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
function collectHtmlFiles(dir, baseDir) {
|
|
783
|
-
const files = [];
|
|
784
|
-
let items;
|
|
785
|
-
try {
|
|
786
|
-
items = readdirSync(dir);
|
|
787
|
-
} catch (err) {
|
|
788
|
-
console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
|
|
789
|
-
return files;
|
|
790
|
-
}
|
|
791
|
-
for (const item of items) {
|
|
792
|
-
if (item.startsWith(".")) continue;
|
|
793
|
-
const fullPath = resolve2(dir, item);
|
|
794
|
-
try {
|
|
795
|
-
const s = statSync(fullPath);
|
|
796
|
-
if (s.isFile() && item.endsWith(".html")) {
|
|
797
|
-
files.push(relative(baseDir, fullPath));
|
|
798
|
-
} else if (s.isDirectory()) {
|
|
799
|
-
files.push(...collectHtmlFiles(fullPath, baseDir));
|
|
800
|
-
}
|
|
801
|
-
} catch (err) {
|
|
802
|
-
console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
return files.sort();
|
|
806
|
-
}
|
|
807
|
-
function collectAllFiles(dir, baseDir) {
|
|
808
|
-
const files = [];
|
|
809
|
-
let items;
|
|
810
|
-
try {
|
|
811
|
-
items = readdirSync(dir);
|
|
812
|
-
} catch (err) {
|
|
813
|
-
console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
|
|
814
|
-
return files;
|
|
815
|
-
}
|
|
816
|
-
for (const item of items) {
|
|
817
|
-
if (item.startsWith(".")) continue;
|
|
818
|
-
const fullPath = resolve2(dir, item);
|
|
819
|
-
try {
|
|
820
|
-
const s = statSync(fullPath);
|
|
821
|
-
if (s.isFile()) {
|
|
822
|
-
files.push(relative(baseDir, fullPath));
|
|
823
|
-
} else if (s.isDirectory()) {
|
|
824
|
-
files.push(...collectAllFiles(fullPath, baseDir));
|
|
825
|
-
}
|
|
826
|
-
} catch (err) {
|
|
827
|
-
console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
return files.sort();
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// packages/extensions/pages/src/routes.ts
|
|
834
|
-
import { readFile, stat } from "fs/promises";
|
|
835
|
-
import { extname, resolve as resolve3 } from "path";
|
|
836
|
-
import { Hono as Hono2 } from "hono";
|
|
837
|
-
|
|
838
|
-
// packages/extensions/pages/src/cache.ts
|
|
839
|
-
function getSites(db) {
|
|
840
|
-
const sites = getAllSites(db);
|
|
841
|
-
return sites.map((site) => ({
|
|
842
|
-
name: site.mind,
|
|
843
|
-
label: site.mind,
|
|
844
|
-
pages: site.files.map((f) => ({
|
|
845
|
-
file: f.file,
|
|
846
|
-
modified: f.updated_at,
|
|
847
|
-
url: `/ext/pages/public/${site.mind}/${f.file}`
|
|
848
|
-
}))
|
|
849
|
-
}));
|
|
850
|
-
}
|
|
851
|
-
function getRecentPagesList(db, opts) {
|
|
852
|
-
const rows = getRecentPages(db, opts);
|
|
853
|
-
return rows.map((r) => ({
|
|
854
|
-
mind: r.mind,
|
|
855
|
-
file: r.file,
|
|
856
|
-
modified: r.updated_at,
|
|
857
|
-
url: `/ext/pages/public/${r.mind}/${r.file}`
|
|
858
|
-
}));
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// packages/extensions/pages/src/routes.ts
|
|
862
|
-
var MIME_TYPES = {
|
|
863
|
-
".html": "text/html",
|
|
864
|
-
".js": "application/javascript",
|
|
865
|
-
".css": "text/css",
|
|
866
|
-
".json": "application/json",
|
|
867
|
-
".svg": "image/svg+xml",
|
|
868
|
-
".png": "image/png",
|
|
869
|
-
".jpg": "image/jpeg",
|
|
870
|
-
".jpeg": "image/jpeg",
|
|
871
|
-
".gif": "image/gif",
|
|
872
|
-
".ico": "image/x-icon",
|
|
873
|
-
".woff": "font/woff",
|
|
874
|
-
".woff2": "font/woff2",
|
|
875
|
-
".txt": "text/plain",
|
|
876
|
-
".xml": "application/xml"
|
|
877
|
-
};
|
|
878
|
-
function createRoutes2(ctx) {
|
|
879
|
-
return new Hono2().get("/", async (c) => {
|
|
880
|
-
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
881
|
-
const sites = getSites(ctx.db);
|
|
882
|
-
const recentPages = getRecentPagesList(ctx.db);
|
|
883
|
-
return c.json({ sites, recentPages });
|
|
884
|
-
}).get("/feed", async (c) => {
|
|
885
|
-
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
886
|
-
const mind = c.req.query("mind");
|
|
887
|
-
const rawLimit = c.req.query("limit");
|
|
888
|
-
const limit = rawLimit ? parseInt(rawLimit, 10) || 8 : 8;
|
|
889
|
-
const recentPages = getRecentPagesList(ctx.db, { mind: mind || void 0, limit });
|
|
890
|
-
return c.json(
|
|
891
|
-
recentPages.map((p) => ({
|
|
892
|
-
id: `page-${p.mind}-${p.file}`,
|
|
893
|
-
title: `${p.mind}/${p.file}`,
|
|
894
|
-
url: p.url ?? `/minds/${p.mind}/pages/${p.file}`,
|
|
895
|
-
date: p.modified,
|
|
896
|
-
author: p.mind,
|
|
897
|
-
bodyHtml: `<p>Page updated</p>`,
|
|
898
|
-
iframeUrl: `/ext/pages/public/${p.mind}/${p.file}`,
|
|
899
|
-
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>',
|
|
900
|
-
color: "purple"
|
|
901
|
-
}))
|
|
902
|
-
);
|
|
903
|
-
}).put("/publish/:name", async (c) => {
|
|
904
|
-
const user = ctx.resolveUser(c);
|
|
905
|
-
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
906
|
-
const name = c.req.param("name");
|
|
907
|
-
if (user.role !== "admin" && user.username !== name) {
|
|
908
|
-
return c.json({ error: "Forbidden" }, 403);
|
|
909
|
-
}
|
|
910
|
-
const config = ctx.getSystemsConfig();
|
|
911
|
-
if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
|
|
912
|
-
const body = await c.req.text();
|
|
913
|
-
try {
|
|
914
|
-
const res = await fetch(`${config.apiUrl}/api/pages/publish/${name}`, {
|
|
915
|
-
method: "PUT",
|
|
916
|
-
headers: {
|
|
917
|
-
"Content-Type": "application/json",
|
|
918
|
-
Authorization: `Bearer ${config.apiKey}`
|
|
919
|
-
},
|
|
920
|
-
body
|
|
921
|
-
});
|
|
922
|
-
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
923
|
-
return c.json(data, res.status);
|
|
924
|
-
} catch (err) {
|
|
925
|
-
return c.json({ error: `Connection failed: ${err.message}` }, 502);
|
|
926
|
-
}
|
|
927
|
-
}).get("/status/:name", async (c) => {
|
|
928
|
-
const user = ctx.resolveUser(c);
|
|
929
|
-
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
930
|
-
const name = c.req.param("name");
|
|
931
|
-
if (user.role !== "admin" && user.username !== name) {
|
|
932
|
-
return c.json({ error: "Forbidden" }, 403);
|
|
933
|
-
}
|
|
934
|
-
const config = ctx.getSystemsConfig();
|
|
935
|
-
if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
|
|
936
|
-
try {
|
|
937
|
-
const res = await fetch(`${config.apiUrl}/api/pages/status/${name}`, {
|
|
938
|
-
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
939
|
-
});
|
|
940
|
-
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
941
|
-
return c.json(data, res.status);
|
|
942
|
-
} catch (err) {
|
|
943
|
-
return c.json({ error: `Connection failed: ${err.message}` }, 502);
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
var _voluteHome = null;
|
|
948
|
-
async function getVoluteHome() {
|
|
949
|
-
if (_voluteHome) return _voluteHome();
|
|
950
|
-
const mod = await import("./registry-UYV5S6QT.js");
|
|
951
|
-
_voluteHome = mod.voluteHome;
|
|
952
|
-
return _voluteHome();
|
|
953
|
-
}
|
|
954
|
-
function createPublicRoutes(ctx) {
|
|
955
|
-
return new Hono2().get("/:name/*", async (c) => {
|
|
956
|
-
const name = c.req.param("name");
|
|
957
|
-
if (name.includes("/") || name.includes("\\") || name === "." || name === "..")
|
|
958
|
-
return c.text("Not found", 404);
|
|
959
|
-
let pagesRoot;
|
|
960
|
-
if (name === "_system") {
|
|
961
|
-
const home = await getVoluteHome();
|
|
962
|
-
pagesRoot = resolve3(home, "shared", "pages");
|
|
963
|
-
} else {
|
|
964
|
-
pagesRoot = resolve3(ctx.dataDir, "sites", name);
|
|
965
|
-
}
|
|
966
|
-
const prefix = `/public/${name}`;
|
|
967
|
-
const idx = c.req.path.indexOf(prefix);
|
|
968
|
-
const wildcard = idx >= 0 ? c.req.path.slice(idx + prefix.length) : "/";
|
|
969
|
-
const requestedPath = resolve3(pagesRoot, wildcard.slice(1));
|
|
970
|
-
if (requestedPath !== pagesRoot && !requestedPath.startsWith(`${pagesRoot}/`))
|
|
971
|
-
return c.text("Forbidden", 403);
|
|
972
|
-
let fileToServe = requestedPath;
|
|
973
|
-
let fileStat = await stat(requestedPath).catch(() => null);
|
|
974
|
-
if (fileStat?.isDirectory()) {
|
|
975
|
-
const indexPath = resolve3(requestedPath, "index.html");
|
|
976
|
-
fileStat = await stat(indexPath).catch(() => null);
|
|
977
|
-
if (fileStat?.isFile()) {
|
|
978
|
-
fileToServe = indexPath;
|
|
979
|
-
} else {
|
|
980
|
-
return c.text("Not found", 404);
|
|
981
|
-
}
|
|
982
|
-
} else if (!fileStat?.isFile()) {
|
|
983
|
-
return c.text("Not found", 404);
|
|
984
|
-
}
|
|
985
|
-
const ext = extname(fileToServe);
|
|
986
|
-
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
987
|
-
try {
|
|
988
|
-
const body = await readFile(fileToServe);
|
|
989
|
-
return c.body(body, 200, { "Content-Type": mime });
|
|
990
|
-
} catch (err) {
|
|
991
|
-
const code = err.code;
|
|
992
|
-
if (code === "EACCES") return c.text("Forbidden", 403);
|
|
993
|
-
if (code === "ENOENT") return c.text("Not found", 404);
|
|
994
|
-
return c.text("Internal server error", 500);
|
|
995
|
-
}
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// packages/extensions/pages/src/index.ts
|
|
1000
|
-
var assetsDir2 = resolve4(import.meta.dirname, "../dist/ui");
|
|
1001
|
-
var skillsDir2 = resolve4(import.meta.dirname, "../skills");
|
|
1002
|
-
var src_default2 = createExtension({
|
|
1003
|
-
id: "pages",
|
|
1004
|
-
name: "Pages",
|
|
1005
|
-
version: "0.1.0",
|
|
1006
|
-
description: "Publish and serve web pages from mind directories",
|
|
1007
|
-
initDb,
|
|
1008
|
-
routes: (ctx) => createRoutes2(ctx),
|
|
1009
|
-
publicRoutes: (ctx) => createPublicRoutes(ctx),
|
|
1010
|
-
commands: createCommands2(),
|
|
1011
|
-
skillsDir: skillsDir2,
|
|
1012
|
-
standardSkill: true,
|
|
1013
|
-
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>',
|
|
1014
|
-
color: "purple",
|
|
1015
|
-
ui: {
|
|
1016
|
-
assetsDir: assetsDir2,
|
|
1017
|
-
systemSection: {
|
|
1018
|
-
id: "pages",
|
|
1019
|
-
label: "Pages",
|
|
1020
|
-
urlPatterns: ["/pages", "/pages/:site", "/pages/:site/:path"]
|
|
1021
|
-
},
|
|
1022
|
-
mindSections: [{ id: "pages", label: "Pages" }],
|
|
1023
|
-
feedSource: {
|
|
1024
|
-
endpoint: "/api/ext/pages/feed"
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
// packages/extensions/plan/src/index.ts
|
|
1030
|
-
import { resolve as resolve5 } from "path";
|
|
1031
|
-
|
|
1032
|
-
// packages/extensions/plan/src/plans.ts
|
|
1033
|
-
async function startPlan(db, getUser2, userId, title, description) {
|
|
1034
|
-
db.prepare(
|
|
1035
|
-
"UPDATE plans SET status = 'archived', completed_at = datetime('now') WHERE status = 'active'"
|
|
1036
|
-
).run();
|
|
1037
|
-
const row = db.prepare(
|
|
1038
|
-
`INSERT INTO plans (title, description, set_by)
|
|
1039
|
-
VALUES (?, ?, ?)
|
|
1040
|
-
RETURNING *`
|
|
1041
|
-
).get(title, description, userId);
|
|
1042
|
-
const user = await getUser2(userId);
|
|
1043
|
-
return {
|
|
1044
|
-
...row,
|
|
1045
|
-
set_by_username: user?.username ?? "unknown",
|
|
1046
|
-
set_by_display_name: user?.display_name ?? null
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
async function getActivePlan(db, getUser2) {
|
|
1050
|
-
const row = db.prepare("SELECT * FROM plans WHERE status = 'active' LIMIT 1").get();
|
|
1051
|
-
if (!row) return null;
|
|
1052
|
-
const user = await getUser2(row.set_by);
|
|
1053
|
-
const logs = db.prepare("SELECT * FROM plan_logs WHERE plan_id = ? ORDER BY created_at DESC LIMIT 20").all(row.id);
|
|
1054
|
-
const messages = db.prepare("SELECT * FROM plan_messages WHERE plan_id = ? ORDER BY id DESC LIMIT 10").all(row.id);
|
|
1055
|
-
const latestMessage = messages.length > 0 ? messages[0].content : null;
|
|
1056
|
-
return {
|
|
1057
|
-
...row,
|
|
1058
|
-
set_by_username: user?.username ?? "unknown",
|
|
1059
|
-
set_by_display_name: user?.display_name ?? null,
|
|
1060
|
-
logs,
|
|
1061
|
-
messages,
|
|
1062
|
-
latestMessage
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
function logProgress(db, planId, mindName, content) {
|
|
1066
|
-
return db.prepare(
|
|
1067
|
-
`INSERT INTO plan_logs (plan_id, mind_name, content)
|
|
1068
|
-
VALUES (?, ?, ?)
|
|
1069
|
-
RETURNING *`
|
|
1070
|
-
).get(planId, mindName, content);
|
|
1071
|
-
}
|
|
1072
|
-
function addPlanMessage(db, planId, content) {
|
|
1073
|
-
return db.prepare(
|
|
1074
|
-
`INSERT INTO plan_messages (plan_id, content)
|
|
1075
|
-
VALUES (?, ?)
|
|
1076
|
-
RETURNING *`
|
|
1077
|
-
).get(planId, content);
|
|
1078
|
-
}
|
|
1079
|
-
function finishPlan(db, planId, message) {
|
|
1080
|
-
const result = db.prepare(
|
|
1081
|
-
"UPDATE plans SET status = 'completed', completed_at = datetime('now'), finish_message = ? WHERE id = ? AND status = 'active'"
|
|
1082
|
-
).run(message ?? null, planId);
|
|
1083
|
-
return result.changes > 0;
|
|
1084
|
-
}
|
|
1085
|
-
async function listPlans(db, getUser2, opts) {
|
|
1086
|
-
const limit = opts?.limit ?? 20;
|
|
1087
|
-
const offset = opts?.offset ?? 0;
|
|
1088
|
-
const rows = opts?.status ? db.prepare("SELECT * FROM plans WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.status, limit, offset) : db.prepare("SELECT * FROM plans ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
1089
|
-
const userCache = /* @__PURE__ */ new Map();
|
|
1090
|
-
const result = [];
|
|
1091
|
-
for (const row of rows) {
|
|
1092
|
-
if (!userCache.has(row.set_by)) {
|
|
1093
|
-
const u = await getUser2(row.set_by);
|
|
1094
|
-
userCache.set(row.set_by, {
|
|
1095
|
-
username: u?.username ?? "unknown",
|
|
1096
|
-
display_name: u?.display_name ?? null
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
const userInfo = userCache.get(row.set_by);
|
|
1100
|
-
result.push({
|
|
1101
|
-
...row,
|
|
1102
|
-
set_by_username: userInfo.username,
|
|
1103
|
-
set_by_display_name: userInfo.display_name
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
return result;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// packages/extensions/plan/src/commands.ts
|
|
1110
|
-
function getFlag2(args, flag) {
|
|
1111
|
-
const idx = args.indexOf(flag);
|
|
1112
|
-
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
1113
|
-
return void 0;
|
|
1114
|
-
}
|
|
1115
|
-
var _announce = null;
|
|
1116
|
-
async function announceToSystem(text) {
|
|
1117
|
-
if (!_announce) {
|
|
1118
|
-
try {
|
|
1119
|
-
const mod = await import("./system-channel-DXD2JBOU.js");
|
|
1120
|
-
_announce = mod.announceToSystem;
|
|
1121
|
-
} catch {
|
|
1122
|
-
return false;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
try {
|
|
1126
|
-
await _announce(text);
|
|
1127
|
-
return true;
|
|
1128
|
-
} catch (err) {
|
|
1129
|
-
console.error("[plan] Failed to announce to system channel:", err);
|
|
1130
|
-
return false;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
function createCommands3() {
|
|
1134
|
-
return {
|
|
1135
|
-
start: {
|
|
1136
|
-
description: "Start a new system plan",
|
|
1137
|
-
usage: 'volute plan start "title" "description" (description can be piped via stdin)',
|
|
1138
|
-
handler: async (args, ctx) => {
|
|
1139
|
-
if (!ctx.db) return { error: "Plan extension requires a database" };
|
|
1140
|
-
const mindName = ctx.mindName;
|
|
1141
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
1142
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
1143
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
1144
|
-
const title = args[0];
|
|
1145
|
-
const description = args[1] ?? ctx.stdin ?? "";
|
|
1146
|
-
if (!title) return { error: 'Usage: volute plan start "title" "description"' };
|
|
1147
|
-
const plan = await startPlan(ctx.db, ctx.getUser, user.id, title, description);
|
|
1148
|
-
ctx.publishActivity({
|
|
1149
|
-
type: "plan_started",
|
|
1150
|
-
mind: user.username,
|
|
1151
|
-
summary: `${user.username} started plan: "${title}"`,
|
|
1152
|
-
metadata: { planId: plan.id, title }
|
|
1153
|
-
});
|
|
1154
|
-
return { output: `Plan started: ${plan.title}` };
|
|
1155
|
-
}
|
|
1156
|
-
},
|
|
1157
|
-
message: {
|
|
1158
|
-
description: "Post a message about the current plan (sent to #system)",
|
|
1159
|
-
usage: `volute plan message "today's focus: ..." (content can be piped via stdin)`,
|
|
1160
|
-
handler: async (args, ctx) => {
|
|
1161
|
-
if (!ctx.db) return { error: "Plan extension requires a database" };
|
|
1162
|
-
const mindName = ctx.mindName;
|
|
1163
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
1164
|
-
const content = args[0] ?? ctx.stdin;
|
|
1165
|
-
if (!content) return { error: 'Usage: volute plan message "your message"' };
|
|
1166
|
-
const plan = await getActivePlan(ctx.db, ctx.getUser);
|
|
1167
|
-
if (!plan) return { error: "No active plan" };
|
|
1168
|
-
const msg = addPlanMessage(ctx.db, plan.id, content);
|
|
1169
|
-
ctx.publishActivity({
|
|
1170
|
-
type: "plan_message",
|
|
1171
|
-
mind: mindName,
|
|
1172
|
-
summary: `Plan message: "${content.slice(0, 100)}"`,
|
|
1173
|
-
metadata: { planId: plan.id, messageId: msg.id }
|
|
1174
|
-
});
|
|
1175
|
-
const announced = await announceToSystem(`[Plan: ${plan.title}] ${content}`);
|
|
1176
|
-
return {
|
|
1177
|
-
output: announced ? "Message posted to #system." : "Message logged (system channel unavailable)."
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
},
|
|
1181
|
-
log: {
|
|
1182
|
-
description: "Log progress on the current plan",
|
|
1183
|
-
usage: 'volute plan log "progress update" (content can be piped via stdin)',
|
|
1184
|
-
handler: async (args, ctx) => {
|
|
1185
|
-
if (!ctx.db) return { error: "Plan extension requires a database" };
|
|
1186
|
-
const mindName = ctx.mindName;
|
|
1187
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
1188
|
-
const content = args[0] ?? ctx.stdin;
|
|
1189
|
-
if (!content) return { error: 'Usage: volute plan log "progress update"' };
|
|
1190
|
-
const plan = await getActivePlan(ctx.db, ctx.getUser);
|
|
1191
|
-
if (!plan) return { error: "No active plan" };
|
|
1192
|
-
const log = logProgress(ctx.db, plan.id, mindName, content);
|
|
1193
|
-
ctx.publishActivity({
|
|
1194
|
-
type: "plan_progress",
|
|
1195
|
-
mind: mindName,
|
|
1196
|
-
summary: `${mindName} logged progress: "${content.slice(0, 100)}"`,
|
|
1197
|
-
metadata: { planId: plan.id, logId: log.id }
|
|
1198
|
-
});
|
|
1199
|
-
return { output: "Progress logged." };
|
|
1200
|
-
}
|
|
1201
|
-
},
|
|
1202
|
-
current: {
|
|
1203
|
-
description: "Show the current active plan",
|
|
1204
|
-
usage: "volute plan current",
|
|
1205
|
-
handler: async (_args, ctx) => {
|
|
1206
|
-
if (!ctx.db) return { error: "Plan extension requires a database" };
|
|
1207
|
-
const plan = await getActivePlan(ctx.db, ctx.getUser);
|
|
1208
|
-
if (!plan) return { output: "No active plan." };
|
|
1209
|
-
const lines = [
|
|
1210
|
-
`# ${plan.title}`,
|
|
1211
|
-
"",
|
|
1212
|
-
`Set by ${plan.set_by_username} \u2014 ${new Date(plan.created_at).toLocaleString()}`
|
|
1213
|
-
];
|
|
1214
|
-
if (plan.description) {
|
|
1215
|
-
lines.push("", plan.description);
|
|
1216
|
-
}
|
|
1217
|
-
if (plan.latestMessage) {
|
|
1218
|
-
lines.push("", `## Latest message`, "", plan.latestMessage);
|
|
1219
|
-
}
|
|
1220
|
-
if (plan.logs.length > 0) {
|
|
1221
|
-
lines.push("", "## Progress");
|
|
1222
|
-
for (const log of plan.logs) {
|
|
1223
|
-
const date = new Date(log.created_at).toLocaleString();
|
|
1224
|
-
lines.push(` ${log.mind_name} (${date}): ${log.content}`);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
return { output: lines.join("\n") };
|
|
1228
|
-
}
|
|
1229
|
-
},
|
|
1230
|
-
history: {
|
|
1231
|
-
description: "List past plans",
|
|
1232
|
-
usage: "volute plan history [--limit N]",
|
|
1233
|
-
handler: async (args, ctx) => {
|
|
1234
|
-
if (!ctx.db) return { error: "Plan extension requires a database" };
|
|
1235
|
-
const limit = parseInt(getFlag2(args, "--limit") ?? "10", 10);
|
|
1236
|
-
const plans = await listPlans(ctx.db, ctx.getUser, { limit });
|
|
1237
|
-
if (plans.length === 0) return { output: "No plans found." };
|
|
1238
|
-
const lines = plans.map((p) => {
|
|
1239
|
-
const date = new Date(p.created_at).toLocaleDateString();
|
|
1240
|
-
const status = p.status === "active" ? " [active]" : "";
|
|
1241
|
-
return ` ${p.title} (${date}, by ${p.set_by_username})${status}`;
|
|
1242
|
-
});
|
|
1243
|
-
return { output: lines.join("\n") };
|
|
1244
|
-
}
|
|
1245
|
-
},
|
|
1246
|
-
finish: {
|
|
1247
|
-
description: "Finish the current plan with a closing message",
|
|
1248
|
-
usage: 'volute plan finish "closing message" (message can be piped via stdin)',
|
|
1249
|
-
handler: async (args, ctx) => {
|
|
1250
|
-
if (!ctx.db) return { error: "Plan extension requires a database" };
|
|
1251
|
-
const mindName = ctx.mindName;
|
|
1252
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
1253
|
-
const plan = await getActivePlan(ctx.db, ctx.getUser);
|
|
1254
|
-
if (!plan) return { error: "No active plan" };
|
|
1255
|
-
const message = args[0] ?? ctx.stdin ?? "";
|
|
1256
|
-
finishPlan(ctx.db, plan.id, message);
|
|
1257
|
-
ctx.publishActivity({
|
|
1258
|
-
type: "plan_finished",
|
|
1259
|
-
mind: mindName,
|
|
1260
|
-
summary: `${mindName} finished plan: "${plan.title}"`,
|
|
1261
|
-
metadata: { planId: plan.id }
|
|
1262
|
-
});
|
|
1263
|
-
const announcement = message ? `[Plan finished: ${plan.title}] ${message}` : `[Plan finished: ${plan.title}]`;
|
|
1264
|
-
const announced = await announceToSystem(announcement);
|
|
1265
|
-
const suffix = announced ? "" : " (system channel unavailable)";
|
|
1266
|
-
return { output: `Finished: ${plan.title}${suffix}` };
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
// packages/extensions/plan/src/db.ts
|
|
1273
|
-
function initDb3(db) {
|
|
1274
|
-
db.exec(`
|
|
1275
|
-
CREATE TABLE IF NOT EXISTS plans (
|
|
1276
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1277
|
-
title TEXT NOT NULL,
|
|
1278
|
-
description TEXT NOT NULL DEFAULT '',
|
|
1279
|
-
status TEXT NOT NULL DEFAULT 'active',
|
|
1280
|
-
set_by INTEGER NOT NULL,
|
|
1281
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1282
|
-
completed_at TEXT,
|
|
1283
|
-
finish_message TEXT
|
|
1284
|
-
);
|
|
1285
|
-
CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
|
|
1286
|
-
CREATE INDEX IF NOT EXISTS idx_plans_created_at ON plans(created_at);
|
|
1287
|
-
|
|
1288
|
-
CREATE TABLE IF NOT EXISTS plan_logs (
|
|
1289
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1290
|
-
plan_id INTEGER NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
|
1291
|
-
mind_name TEXT NOT NULL,
|
|
1292
|
-
content TEXT NOT NULL,
|
|
1293
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1294
|
-
);
|
|
1295
|
-
CREATE INDEX IF NOT EXISTS idx_plan_logs_plan_id ON plan_logs(plan_id);
|
|
1296
|
-
|
|
1297
|
-
CREATE TABLE IF NOT EXISTS plan_messages (
|
|
1298
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1299
|
-
plan_id INTEGER NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
|
1300
|
-
content TEXT NOT NULL,
|
|
1301
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1302
|
-
);
|
|
1303
|
-
CREATE INDEX IF NOT EXISTS idx_plan_messages_plan_id ON plan_messages(plan_id);
|
|
1304
|
-
`);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// packages/extensions/plan/src/routes.ts
|
|
1308
|
-
import { Hono as Hono3 } from "hono";
|
|
1309
|
-
function resolveUserId2(c) {
|
|
1310
|
-
const user = c.get("user");
|
|
1311
|
-
if (!user || user.id === 0) return null;
|
|
1312
|
-
return user;
|
|
1313
|
-
}
|
|
1314
|
-
async function parseJson2(c) {
|
|
1315
|
-
try {
|
|
1316
|
-
return await c.req.json();
|
|
1317
|
-
} catch {
|
|
1318
|
-
return null;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
function createRoutes3(ctx) {
|
|
1322
|
-
if (!ctx.db) throw new Error("Plan extension requires a database");
|
|
1323
|
-
const db = ctx.db;
|
|
1324
|
-
const { getUser: getUser2 } = ctx;
|
|
1325
|
-
const app = new Hono3().get("/current", async (c) => {
|
|
1326
|
-
const plan = await getActivePlan(db, getUser2);
|
|
1327
|
-
if (!plan) return c.json(null);
|
|
1328
|
-
return c.json(plan);
|
|
1329
|
-
}).get("/", async (c) => {
|
|
1330
|
-
const status = c.req.query("status");
|
|
1331
|
-
const rawLimit = c.req.query("limit");
|
|
1332
|
-
const rawOffset = c.req.query("offset");
|
|
1333
|
-
const limit = rawLimit ? parseInt(rawLimit, 10) : void 0;
|
|
1334
|
-
const offset = rawOffset ? parseInt(rawOffset, 10) : void 0;
|
|
1335
|
-
if (limit !== void 0 && Number.isNaN(limit) || offset !== void 0 && Number.isNaN(offset)) {
|
|
1336
|
-
return c.json({ error: "Invalid limit or offset parameter" }, 400);
|
|
1337
|
-
}
|
|
1338
|
-
const plans = await listPlans(db, getUser2, { status: status ?? void 0, limit, offset });
|
|
1339
|
-
return c.json(plans);
|
|
1340
|
-
}).post("/", async (c) => {
|
|
1341
|
-
const actor = resolveUserId2(c);
|
|
1342
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
1343
|
-
if (actor.role !== "admin" && actor.user_type !== "mind") {
|
|
1344
|
-
return c.json({ error: "Only spirit or admin can start plans" }, 403);
|
|
1345
|
-
}
|
|
1346
|
-
const body = await parseJson2(c);
|
|
1347
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
1348
|
-
if (!body.title) return c.json({ error: "title is required" }, 400);
|
|
1349
|
-
const plan = await startPlan(db, getUser2, actor.id, body.title, body.description ?? "");
|
|
1350
|
-
ctx.publishActivity({
|
|
1351
|
-
type: "plan_started",
|
|
1352
|
-
mind: actor.username,
|
|
1353
|
-
summary: `${actor.username} started plan: "${body.title}"`,
|
|
1354
|
-
metadata: { planId: plan.id, title: body.title }
|
|
1355
|
-
});
|
|
1356
|
-
return c.json(plan, 201);
|
|
1357
|
-
}).post("/:id{[0-9]+}/message", async (c) => {
|
|
1358
|
-
const actor = resolveUserId2(c);
|
|
1359
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
1360
|
-
const planId = parseInt(c.req.param("id"), 10);
|
|
1361
|
-
if (Number.isNaN(planId)) return c.json({ error: "Invalid plan ID" }, 400);
|
|
1362
|
-
const plan = db.prepare("SELECT id FROM plans WHERE id = ? AND status = 'active'").get(planId);
|
|
1363
|
-
if (!plan) return c.json({ error: "Active plan not found" }, 404);
|
|
1364
|
-
const body = await parseJson2(c);
|
|
1365
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
1366
|
-
if (!body.content) return c.json({ error: "content is required" }, 400);
|
|
1367
|
-
const msg = addPlanMessage(db, planId, body.content);
|
|
1368
|
-
ctx.publishActivity({
|
|
1369
|
-
type: "plan_message",
|
|
1370
|
-
mind: actor.username,
|
|
1371
|
-
summary: `Plan message: "${body.content.slice(0, 100)}"`,
|
|
1372
|
-
metadata: { planId, messageId: msg.id }
|
|
1373
|
-
});
|
|
1374
|
-
return c.json(msg, 201);
|
|
1375
|
-
}).post("/:id{[0-9]+}/log", async (c) => {
|
|
1376
|
-
const actor = resolveUserId2(c);
|
|
1377
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
1378
|
-
const planId = parseInt(c.req.param("id"), 10);
|
|
1379
|
-
if (Number.isNaN(planId)) return c.json({ error: "Invalid plan ID" }, 400);
|
|
1380
|
-
const plan = db.prepare("SELECT id FROM plans WHERE id = ? AND status = 'active'").get(planId);
|
|
1381
|
-
if (!plan) return c.json({ error: "Active plan not found" }, 404);
|
|
1382
|
-
const body = await parseJson2(c);
|
|
1383
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
1384
|
-
if (!body.content) return c.json({ error: "content is required" }, 400);
|
|
1385
|
-
const log = logProgress(db, planId, actor.username, body.content);
|
|
1386
|
-
ctx.publishActivity({
|
|
1387
|
-
type: "plan_progress",
|
|
1388
|
-
mind: actor.username,
|
|
1389
|
-
summary: `${actor.username} logged progress: "${body.content.slice(0, 100)}"`,
|
|
1390
|
-
metadata: { planId, logId: log.id }
|
|
1391
|
-
});
|
|
1392
|
-
return c.json(log, 201);
|
|
1393
|
-
}).patch("/:id{[0-9]+}/finish", async (c) => {
|
|
1394
|
-
const actor = resolveUserId2(c);
|
|
1395
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
1396
|
-
if (actor.role !== "admin" && actor.user_type !== "mind") {
|
|
1397
|
-
return c.json({ error: "Only spirit or admin can finish plans" }, 403);
|
|
1398
|
-
}
|
|
1399
|
-
const planId = parseInt(c.req.param("id"), 10);
|
|
1400
|
-
if (Number.isNaN(planId)) return c.json({ error: "Invalid plan ID" }, 400);
|
|
1401
|
-
const body = await parseJson2(c);
|
|
1402
|
-
const message = body?.message;
|
|
1403
|
-
const ok = finishPlan(db, planId, message);
|
|
1404
|
-
if (!ok) return c.json({ error: "Plan not found" }, 404);
|
|
1405
|
-
ctx.publishActivity({
|
|
1406
|
-
type: "plan_finished",
|
|
1407
|
-
mind: actor.username,
|
|
1408
|
-
summary: `${actor.username} finished a plan`,
|
|
1409
|
-
metadata: { planId }
|
|
1410
|
-
});
|
|
1411
|
-
return c.json({ ok: true });
|
|
1412
|
-
}).get("/feed", async (c) => {
|
|
1413
|
-
const rawLimit = c.req.query("limit");
|
|
1414
|
-
const limit = rawLimit ? parseInt(rawLimit, 10) : 5;
|
|
1415
|
-
if (Number.isNaN(limit)) return c.json({ error: "Invalid limit parameter" }, 400);
|
|
1416
|
-
const plans = await listPlans(db, getUser2, { limit });
|
|
1417
|
-
return c.json(
|
|
1418
|
-
plans.map((p) => ({
|
|
1419
|
-
id: `plan-${p.id}`,
|
|
1420
|
-
title: p.title,
|
|
1421
|
-
url: `/plan`,
|
|
1422
|
-
date: p.created_at,
|
|
1423
|
-
author: p.set_by_username,
|
|
1424
|
-
bodyHtml: p.description || `<em>${p.status}</em>`,
|
|
1425
|
-
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="M8 4v4l2.5 2.5"/></svg>',
|
|
1426
|
-
color: "blue"
|
|
1427
|
-
}))
|
|
1428
|
-
);
|
|
1429
|
-
});
|
|
1430
|
-
return app;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// packages/extensions/plan/src/index.ts
|
|
1434
|
-
var assetsDir3 = resolve5(import.meta.dirname, "../dist/ui");
|
|
1435
|
-
var skillsDir3 = resolve5(import.meta.dirname, "../skills");
|
|
1436
|
-
var src_default3 = createExtension({
|
|
1437
|
-
id: "plan",
|
|
1438
|
-
name: "Plan",
|
|
1439
|
-
version: "0.1.0",
|
|
1440
|
-
description: "System-wide plans for coordinated mind activity",
|
|
1441
|
-
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="M8 4v4l2.5 2.5"/></svg>',
|
|
1442
|
-
color: "blue",
|
|
1443
|
-
routes: (ctx) => createRoutes3(ctx),
|
|
1444
|
-
commands: createCommands3(),
|
|
1445
|
-
initDb: initDb3,
|
|
1446
|
-
skillsDir: skillsDir3,
|
|
1447
|
-
standardSkill: true,
|
|
1448
|
-
ui: {
|
|
1449
|
-
assetsDir: assetsDir3,
|
|
1450
|
-
systemSection: { id: "plan", label: "Plan", urlPatterns: ["/plan"] },
|
|
1451
|
-
feedSource: { endpoint: "/api/ext/plan/feed" }
|
|
1452
|
-
}
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
// src/lib/systems-config.ts
|
|
1456
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
1457
|
-
import { resolve as resolve6 } from "path";
|
|
1458
|
-
var DEFAULT_API_URL = "https://volute.systems";
|
|
1459
|
-
function configPath() {
|
|
1460
|
-
return resolve6(voluteSystemDir(), "systems.json");
|
|
1461
|
-
}
|
|
1462
|
-
function readSystemsConfig() {
|
|
1463
|
-
const path = configPath();
|
|
1464
|
-
if (!existsSync2(path)) return null;
|
|
1465
|
-
const raw = readFileSync2(path, "utf-8");
|
|
1466
|
-
let data;
|
|
1467
|
-
try {
|
|
1468
|
-
data = JSON.parse(raw);
|
|
1469
|
-
} catch {
|
|
1470
|
-
console.error(
|
|
1471
|
-
`Warning: ${path} contains invalid JSON. Run "volute systems logout" and re-login.`
|
|
1472
|
-
);
|
|
1473
|
-
return null;
|
|
1474
|
-
}
|
|
1475
|
-
if (!data.apiKey || !data.system) return null;
|
|
1476
|
-
return {
|
|
1477
|
-
apiKey: data.apiKey,
|
|
1478
|
-
system: data.system,
|
|
1479
|
-
apiUrl: data.apiUrl || DEFAULT_API_URL
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
function writeSystemsConfig(config) {
|
|
1483
|
-
mkdirSync(voluteSystemDir(), { recursive: true });
|
|
1484
|
-
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}
|
|
1485
|
-
`, { mode: 384 });
|
|
1486
|
-
}
|
|
1487
|
-
function deleteSystemsConfig() {
|
|
1488
|
-
try {
|
|
1489
|
-
unlinkSync(configPath());
|
|
1490
|
-
return true;
|
|
1491
|
-
} catch (err) {
|
|
1492
|
-
if (err.code === "ENOENT") return false;
|
|
1493
|
-
throw err;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// src/lib/extensions.ts
|
|
1498
|
-
var VALID_EXTENSION_ID2 = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1499
|
-
var loaded = [];
|
|
1500
|
-
var discovered = [];
|
|
1501
|
-
function extensionsBaseDir() {
|
|
1502
|
-
return resolve7(voluteHome(), "extensions");
|
|
1503
|
-
}
|
|
1504
|
-
function extensionDataDir(id) {
|
|
1505
|
-
return resolve7(voluteSystemDir(), "extension-data", id);
|
|
1506
|
-
}
|
|
1507
|
-
function extensionsConfigPath() {
|
|
1508
|
-
return resolve7(voluteHome(), "system", "extensions.json");
|
|
1509
|
-
}
|
|
1510
|
-
function readExtensionsConfig() {
|
|
1511
|
-
const configPath2 = extensionsConfigPath();
|
|
1512
|
-
if (!existsSync3(configPath2)) return [];
|
|
1513
|
-
try {
|
|
1514
|
-
const data = JSON.parse(readFileSync3(configPath2, "utf-8"));
|
|
1515
|
-
return Array.isArray(data) ? data : [];
|
|
1516
|
-
} catch (err) {
|
|
1517
|
-
logger_default.warn("failed to read extensions config, ignoring installed extensions", {
|
|
1518
|
-
path: configPath2,
|
|
1519
|
-
error: err.message
|
|
1520
|
-
});
|
|
1521
|
-
return [];
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
var _LibsqlDatabase = null;
|
|
1525
|
-
async function getLibsqlDatabase() {
|
|
1526
|
-
if (_LibsqlDatabase) return _LibsqlDatabase;
|
|
1527
|
-
const mod = await import("libsql");
|
|
1528
|
-
_LibsqlDatabase = mod.default ?? mod;
|
|
1529
|
-
return _LibsqlDatabase;
|
|
1530
|
-
}
|
|
1531
|
-
async function openExtensionDb(_id, dataDir) {
|
|
1532
|
-
const dbPath = resolve7(dataDir, "data.db");
|
|
1533
|
-
const Database = await getLibsqlDatabase();
|
|
1534
|
-
return new Database(dbPath);
|
|
1535
|
-
}
|
|
1536
|
-
async function buildContext(manifest, dataDir, authMw) {
|
|
1537
|
-
let db = null;
|
|
1538
|
-
if (manifest.initDb) {
|
|
1539
|
-
const realDb = await openExtensionDb(manifest.id, dataDir);
|
|
1540
|
-
try {
|
|
1541
|
-
manifest.initDb(realDb);
|
|
1542
|
-
} catch (err) {
|
|
1543
|
-
realDb.close();
|
|
1544
|
-
throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
|
|
1545
|
-
}
|
|
1546
|
-
db = realDb;
|
|
1547
|
-
}
|
|
1548
|
-
return {
|
|
1549
|
-
db,
|
|
1550
|
-
authMiddleware: authMw,
|
|
1551
|
-
resolveUser: (c) => {
|
|
1552
|
-
const user = c.get("user");
|
|
1553
|
-
if (!user || typeof user !== "object") return null;
|
|
1554
|
-
return user;
|
|
1555
|
-
},
|
|
1556
|
-
getUser: async (id) => getUser(id),
|
|
1557
|
-
getUserByUsername: async (username) => getUserByUsername(username),
|
|
1558
|
-
publishActivity: (event) => {
|
|
1559
|
-
const enriched = {
|
|
1560
|
-
...event,
|
|
1561
|
-
metadata: {
|
|
1562
|
-
...event.metadata,
|
|
1563
|
-
...manifest.icon && !event.metadata?.icon ? { icon: manifest.icon } : {},
|
|
1564
|
-
...manifest.color && !event.metadata?.color ? { color: manifest.color } : {}
|
|
1565
|
-
}
|
|
1566
|
-
};
|
|
1567
|
-
publish(enriched).catch(
|
|
1568
|
-
(err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
|
|
1569
|
-
);
|
|
1570
|
-
},
|
|
1571
|
-
getMindDir: (name) => {
|
|
1572
|
-
try {
|
|
1573
|
-
const dir = mindDir(name);
|
|
1574
|
-
return existsSync3(dir) ? dir : null;
|
|
1575
|
-
} catch (err) {
|
|
1576
|
-
logger_default.warn(
|
|
1577
|
-
`extension ${manifest.id}: failed to resolve mind dir for ${name}`,
|
|
1578
|
-
logger_default.errorData(err)
|
|
1579
|
-
);
|
|
1580
|
-
return null;
|
|
1581
|
-
}
|
|
1582
|
-
},
|
|
1583
|
-
getSystemsConfig: () => readSystemsConfig(),
|
|
1584
|
-
dataDir
|
|
1585
|
-
};
|
|
1586
|
-
}
|
|
1587
|
-
async function loadExtension(manifest, app, authMw) {
|
|
1588
|
-
if (!VALID_EXTENSION_ID2.test(manifest.id)) {
|
|
1589
|
-
logger_default.error(`invalid extension ID "${manifest.id}", skipping (must match ${VALID_EXTENSION_ID2})`);
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
1592
|
-
const dataDir = extensionDataDir(manifest.id);
|
|
1593
|
-
mkdirSync2(dataDir, { recursive: true });
|
|
1594
|
-
const context = await buildContext(manifest, dataDir, authMw);
|
|
1595
|
-
const routesApp = manifest.routes(context);
|
|
1596
|
-
const extApiPath = `/api/ext/${manifest.id}`;
|
|
1597
|
-
app.use(extApiPath, authMw);
|
|
1598
|
-
app.use(`${extApiPath}/*`, authMw);
|
|
1599
|
-
app.route(extApiPath, routesApp);
|
|
1600
|
-
if (manifest.publicRoutes) {
|
|
1601
|
-
const publicApp = manifest.publicRoutes(context);
|
|
1602
|
-
app.route(`/ext/${manifest.id}/public`, publicApp);
|
|
1603
|
-
}
|
|
1604
|
-
if (manifest.commands) {
|
|
1605
|
-
for (const [cmdName, cmd] of Object.entries(manifest.commands)) {
|
|
1606
|
-
app.post(`${extApiPath}/commands/${cmdName}`, async (c) => {
|
|
1607
|
-
let body;
|
|
1608
|
-
try {
|
|
1609
|
-
body = await c.req.json();
|
|
1610
|
-
} catch {
|
|
1611
|
-
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
1612
|
-
}
|
|
1613
|
-
const user = c.get("user");
|
|
1614
|
-
const mindName = body.mind || user?.username;
|
|
1615
|
-
const session = c.get("mindSession");
|
|
1616
|
-
try {
|
|
1617
|
-
const activityPromises = [];
|
|
1618
|
-
const result = await cmd.handler(body.args ?? [], {
|
|
1619
|
-
...context,
|
|
1620
|
-
publishActivity: (rawEvent) => {
|
|
1621
|
-
const event = {
|
|
1622
|
-
...rawEvent,
|
|
1623
|
-
metadata: {
|
|
1624
|
-
...rawEvent.metadata,
|
|
1625
|
-
...manifest.icon && !rawEvent.metadata?.icon ? { icon: manifest.icon } : {},
|
|
1626
|
-
...manifest.color && !rawEvent.metadata?.color ? { color: manifest.color } : {}
|
|
1627
|
-
}
|
|
1628
|
-
};
|
|
1629
|
-
activityPromises.push(
|
|
1630
|
-
publish(event).catch((err) => {
|
|
1631
|
-
logger_default.error(
|
|
1632
|
-
`extension ${manifest.id}: failed to publish activity`,
|
|
1633
|
-
logger_default.errorData(err)
|
|
1634
|
-
);
|
|
1635
|
-
return 0;
|
|
1636
|
-
})
|
|
1637
|
-
);
|
|
1638
|
-
},
|
|
1639
|
-
mindName,
|
|
1640
|
-
session,
|
|
1641
|
-
stdin: body.stdin
|
|
1642
|
-
});
|
|
1643
|
-
const activityIds = (await Promise.all(activityPromises)).filter((id) => id > 0);
|
|
1644
|
-
const markers = activityIds.map((id) => `[volute:activity:${id}]`).join("");
|
|
1645
|
-
const output = result && typeof result === "object" && "output" in result ? { ...result, output: `${result.output}${markers}` } : markers ? { ...result, output: markers } : result;
|
|
1646
|
-
return c.json(output);
|
|
1647
|
-
} catch (err) {
|
|
1648
|
-
logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
|
|
1649
|
-
return c.json({ error: err.message }, 500);
|
|
1650
|
-
}
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
|
|
1655
|
-
if (resolvedAssetsDir && !existsSync3(resolvedAssetsDir)) {
|
|
1656
|
-
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
1657
|
-
for (let i = 0; i < 5; i++) {
|
|
1658
|
-
const candidate = resolve7(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
|
|
1659
|
-
if (existsSync3(candidate)) {
|
|
1660
|
-
resolvedAssetsDir = candidate;
|
|
1661
|
-
break;
|
|
1662
|
-
}
|
|
1663
|
-
searchDir = dirname(searchDir);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
if (resolvedAssetsDir && existsSync3(resolvedAssetsDir)) {
|
|
1667
|
-
const assetsDir4 = resolvedAssetsDir;
|
|
1668
|
-
const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
|
|
1669
|
-
const { extname: ext } = await import("path");
|
|
1670
|
-
const mimeTypes = {
|
|
1671
|
-
".html": "text/html",
|
|
1672
|
-
".js": "application/javascript",
|
|
1673
|
-
".css": "text/css",
|
|
1674
|
-
".json": "application/json",
|
|
1675
|
-
".svg": "image/svg+xml",
|
|
1676
|
-
".png": "image/png",
|
|
1677
|
-
".jpg": "image/jpeg",
|
|
1678
|
-
".ico": "image/x-icon",
|
|
1679
|
-
".woff": "font/woff",
|
|
1680
|
-
".woff2": "font/woff2"
|
|
1681
|
-
};
|
|
1682
|
-
const prefix = `/ext/${manifest.id}`;
|
|
1683
|
-
const indexPath = resolve7(assetsDir4, "index.html");
|
|
1684
|
-
const serveExtAssets = async (c) => {
|
|
1685
|
-
const urlPath = new URL(c.req.url).pathname;
|
|
1686
|
-
const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
|
|
1687
|
-
const filePath = resolve7(assetsDir4, relativePath);
|
|
1688
|
-
if (filePath !== assetsDir4 && !filePath.startsWith(assetsDir4 + "/"))
|
|
1689
|
-
return c.text("Forbidden", 403);
|
|
1690
|
-
const s = await fsStat(filePath).catch(() => null);
|
|
1691
|
-
if (s?.isFile()) {
|
|
1692
|
-
const mime = mimeTypes[ext(filePath)] || "application/octet-stream";
|
|
1693
|
-
const body = await readFile2(filePath);
|
|
1694
|
-
return c.body(body, 200, { "Content-Type": mime });
|
|
1695
|
-
}
|
|
1696
|
-
if (existsSync3(indexPath)) {
|
|
1697
|
-
const body = await readFile2(indexPath, "utf-8");
|
|
1698
|
-
return c.html(body);
|
|
1699
|
-
}
|
|
1700
|
-
return c.text("Not found", 404);
|
|
1701
|
-
};
|
|
1702
|
-
app.get(`${prefix}/*`, serveExtAssets);
|
|
1703
|
-
app.get(prefix, serveExtAssets);
|
|
1704
|
-
}
|
|
1705
|
-
const skillsDir4 = resolveSkillsDir(manifest);
|
|
1706
|
-
if (skillsDir4) {
|
|
1707
|
-
let entries;
|
|
1708
|
-
try {
|
|
1709
|
-
entries = readdirSync2(skillsDir4, { withFileTypes: true });
|
|
1710
|
-
} catch (err) {
|
|
1711
|
-
logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
|
|
1712
|
-
entries = [];
|
|
1713
|
-
}
|
|
1714
|
-
for (const entry of entries) {
|
|
1715
|
-
if (!entry.isDirectory()) continue;
|
|
1716
|
-
try {
|
|
1717
|
-
const skillPath = resolve7(skillsDir4, entry.name);
|
|
1718
|
-
const sourceHash = hashSkillDir(skillPath);
|
|
1719
|
-
const destDir = resolve7(sharedSkillsDir(), entry.name);
|
|
1720
|
-
if (existsSync3(destDir)) {
|
|
1721
|
-
const destHash = hashSkillDir(destDir);
|
|
1722
|
-
if (sourceHash === destHash) continue;
|
|
1723
|
-
}
|
|
1724
|
-
await importSkillFromDir(skillPath, `ext:${manifest.id}`);
|
|
1725
|
-
logger_default.info(`synced skill "${entry.name}" for extension: ${manifest.id}`);
|
|
1726
|
-
} catch (err) {
|
|
1727
|
-
logger_default.error(
|
|
1728
|
-
`failed to sync skill "${entry.name}" for extension ${manifest.id}`,
|
|
1729
|
-
logger_default.errorData(err)
|
|
1730
|
-
);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
if (manifest.standardSkill && !manifest.skillsDir) {
|
|
1735
|
-
logger_default.warn(`extension ${manifest.id}: standardSkill is true but no skillsDir declared`);
|
|
1736
|
-
}
|
|
1737
|
-
loaded.push({ manifest, context });
|
|
1738
|
-
logger_default.info(`loaded extension: ${manifest.id} v${manifest.version}`);
|
|
1739
|
-
}
|
|
1740
|
-
function resolveSkillsDir(manifest) {
|
|
1741
|
-
if (!manifest.skillsDir) return null;
|
|
1742
|
-
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
1743
|
-
for (let i = 0; i < 5; i++) {
|
|
1744
|
-
const candidate = resolve7(searchDir, "packages", "extensions", manifest.id, "skills");
|
|
1745
|
-
if (existsSync3(candidate)) return candidate;
|
|
1746
|
-
searchDir = dirname(searchDir);
|
|
1747
|
-
}
|
|
1748
|
-
if (existsSync3(manifest.skillsDir)) return manifest.skillsDir;
|
|
1749
|
-
logger_default.warn(`skills dir not found for extension ${manifest.id}: ${manifest.skillsDir}`);
|
|
1750
|
-
return null;
|
|
1751
|
-
}
|
|
1752
|
-
function discoverBuiltinExtensions() {
|
|
1753
|
-
return [src_default, src_default2, src_default3];
|
|
1754
|
-
}
|
|
1755
|
-
async function discoverInstalledExtensions() {
|
|
1756
|
-
const results = [];
|
|
1757
|
-
const packages = readExtensionsConfig();
|
|
1758
|
-
const npmDir = resolve7(voluteHome(), "extensions", "_npm");
|
|
1759
|
-
const { createRequire } = await import("module");
|
|
1760
|
-
for (const pkg of packages) {
|
|
1761
|
-
try {
|
|
1762
|
-
let resolved = pkg;
|
|
1763
|
-
const npmPkgDir = resolve7(npmDir, "node_modules", pkg);
|
|
1764
|
-
if (existsSync3(npmPkgDir)) {
|
|
1765
|
-
const require2 = createRequire(resolve7(npmDir, "noop.js"));
|
|
1766
|
-
resolved = require2.resolve(pkg);
|
|
1767
|
-
}
|
|
1768
|
-
const mod = await import(resolved);
|
|
1769
|
-
const manifest = mod.default ?? mod.extension ?? mod;
|
|
1770
|
-
if (!validateManifest(manifest, `package ${pkg}`)) continue;
|
|
1771
|
-
results.push({ manifest, package: pkg });
|
|
1772
|
-
} catch (err) {
|
|
1773
|
-
logger_default.error(`failed to load extension package: ${pkg}`, logger_default.errorData(err));
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
return results;
|
|
1777
|
-
}
|
|
1778
|
-
function validateManifest(manifest, source) {
|
|
1779
|
-
if (!manifest || typeof manifest !== "object") {
|
|
1780
|
-
logger_default.warn(`extension from ${source} does not export a valid manifest`);
|
|
1781
|
-
return false;
|
|
1782
|
-
}
|
|
1783
|
-
const m = manifest;
|
|
1784
|
-
if (!m.id || typeof m.id !== "string") {
|
|
1785
|
-
logger_default.warn(`extension from ${source} is missing a valid id`);
|
|
1786
|
-
return false;
|
|
1787
|
-
}
|
|
1788
|
-
if (!VALID_EXTENSION_ID2.test(m.id)) {
|
|
1789
|
-
logger_default.warn(`extension from ${source} has invalid id "${m.id}"`);
|
|
1790
|
-
return false;
|
|
1791
|
-
}
|
|
1792
|
-
if (typeof m.routes !== "function") {
|
|
1793
|
-
logger_default.warn(`extension from ${source} is missing a routes function`);
|
|
1794
|
-
return false;
|
|
1795
|
-
}
|
|
1796
|
-
if (!m.name || typeof m.name !== "string") {
|
|
1797
|
-
logger_default.warn(`extension "${m.id}" from ${source} is missing a name`);
|
|
1798
|
-
return false;
|
|
1799
|
-
}
|
|
1800
|
-
if (!m.version || typeof m.version !== "string") {
|
|
1801
|
-
logger_default.warn(`extension "${m.id}" from ${source} is missing a version`);
|
|
1802
|
-
return false;
|
|
1803
|
-
}
|
|
1804
|
-
return true;
|
|
1805
|
-
}
|
|
1806
|
-
async function discoverLocalExtensions() {
|
|
1807
|
-
const baseDir = extensionsBaseDir();
|
|
1808
|
-
if (!existsSync3(baseDir)) return [];
|
|
1809
|
-
const manifests = [];
|
|
1810
|
-
let entries;
|
|
1811
|
-
try {
|
|
1812
|
-
entries = readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
|
|
1813
|
-
} catch (err) {
|
|
1814
|
-
logger_default.error("failed to read local extensions directory", logger_default.errorData(err));
|
|
1815
|
-
return [];
|
|
1816
|
-
}
|
|
1817
|
-
for (const dir of entries) {
|
|
1818
|
-
const extDir = resolve7(baseDir, dir);
|
|
1819
|
-
const candidates = [resolve7(extDir, "src", "index.js"), resolve7(extDir, "index.js")];
|
|
1820
|
-
const entryPoint = candidates.find((p) => existsSync3(p));
|
|
1821
|
-
if (!entryPoint) continue;
|
|
1822
|
-
try {
|
|
1823
|
-
const mod = await import(entryPoint);
|
|
1824
|
-
const manifest = mod.default ?? mod.extension ?? mod;
|
|
1825
|
-
if (!validateManifest(manifest, `local dir ${extDir}`)) continue;
|
|
1826
|
-
manifests.push(manifest);
|
|
1827
|
-
logger_default.info(`discovered local extension: ${manifest.id} from ${extDir}`);
|
|
1828
|
-
} catch (err) {
|
|
1829
|
-
logger_default.error(`failed to load local extension from ${extDir}`, logger_default.errorData(err));
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
return manifests;
|
|
1833
|
-
}
|
|
1834
|
-
async function loadAllExtensions(app, authMw) {
|
|
1835
|
-
const builtins = discoverBuiltinExtensions();
|
|
1836
|
-
const installed = await discoverInstalledExtensions();
|
|
1837
|
-
const local = await discoverLocalExtensions();
|
|
1838
|
-
const disabledIds = new Set(readGlobalConfig().disabledExtensions ?? []);
|
|
1839
|
-
const all = [
|
|
1840
|
-
...builtins.map((m) => ({ manifest: m, source: "builtin" })),
|
|
1841
|
-
...installed.map((i) => ({ manifest: i.manifest, source: "npm", package: i.package })),
|
|
1842
|
-
...local.map((m) => ({ manifest: m, source: "local" }))
|
|
1843
|
-
];
|
|
1844
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1845
|
-
for (const entry of all) {
|
|
1846
|
-
const { manifest } = entry;
|
|
1847
|
-
if (seen.has(manifest.id)) {
|
|
1848
|
-
logger_default.warn(`duplicate extension ID: ${manifest.id}, skipping`);
|
|
1849
|
-
continue;
|
|
1850
|
-
}
|
|
1851
|
-
seen.add(manifest.id);
|
|
1852
|
-
discovered.push(entry);
|
|
1853
|
-
if (disabledIds.has(manifest.id)) {
|
|
1854
|
-
logger_default.info(`extension disabled, skipping: ${manifest.id}`);
|
|
1855
|
-
continue;
|
|
1856
|
-
}
|
|
1857
|
-
try {
|
|
1858
|
-
await loadExtension(manifest, app, authMw);
|
|
1859
|
-
} catch (err) {
|
|
1860
|
-
logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
app.get("/api/extensions/commands", (c) => {
|
|
1864
|
-
const result = {};
|
|
1865
|
-
for (const { manifest } of loaded) {
|
|
1866
|
-
if (!manifest.commands) continue;
|
|
1867
|
-
const cmds = {};
|
|
1868
|
-
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1869
|
-
cmds[name] = { description: cmd.description, ...cmd.usage ? { usage: cmd.usage } : {} };
|
|
1870
|
-
}
|
|
1871
|
-
result[manifest.id] = { commands: cmds };
|
|
1872
|
-
}
|
|
1873
|
-
return c.json(result);
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1876
|
-
function getLoadedExtensions() {
|
|
1877
|
-
return loaded.map(({ manifest }) => {
|
|
1878
|
-
let commands;
|
|
1879
|
-
if (manifest.commands) {
|
|
1880
|
-
commands = {};
|
|
1881
|
-
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1882
|
-
commands[name] = {
|
|
1883
|
-
description: cmd.description,
|
|
1884
|
-
...cmd.usage ? { usage: cmd.usage } : {}
|
|
1885
|
-
};
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
return {
|
|
1889
|
-
id: manifest.id,
|
|
1890
|
-
name: manifest.name,
|
|
1891
|
-
version: manifest.version,
|
|
1892
|
-
description: manifest.description,
|
|
1893
|
-
icon: manifest.icon,
|
|
1894
|
-
systemSection: manifest.ui?.systemSection,
|
|
1895
|
-
mindSections: manifest.ui?.mindSections,
|
|
1896
|
-
feedSource: manifest.ui?.feedSource,
|
|
1897
|
-
commands
|
|
1898
|
-
};
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
function getAllDiscoveredExtensions() {
|
|
1902
|
-
const disabledIds = new Set(readGlobalConfig().disabledExtensions ?? []);
|
|
1903
|
-
return discovered.map((d) => ({
|
|
1904
|
-
id: d.manifest.id,
|
|
1905
|
-
name: d.manifest.name,
|
|
1906
|
-
version: d.manifest.version,
|
|
1907
|
-
description: d.manifest.description,
|
|
1908
|
-
icon: d.manifest.icon,
|
|
1909
|
-
source: d.source,
|
|
1910
|
-
enabled: !disabledIds.has(d.manifest.id),
|
|
1911
|
-
package: d.package
|
|
1912
|
-
}));
|
|
1913
|
-
}
|
|
1914
|
-
function setExtensionEnabled(id, enabled) {
|
|
1915
|
-
if (!discovered.find((d) => d.manifest.id === id)) {
|
|
1916
|
-
throw new Error(`Extension "${id}" not found`);
|
|
1917
|
-
}
|
|
1918
|
-
const config = readGlobalConfig();
|
|
1919
|
-
const disabled = new Set(config.disabledExtensions ?? []);
|
|
1920
|
-
if (enabled) {
|
|
1921
|
-
disabled.delete(id);
|
|
1922
|
-
} else {
|
|
1923
|
-
disabled.add(id);
|
|
1924
|
-
}
|
|
1925
|
-
config.disabledExtensions = disabled.size > 0 ? [...disabled] : void 0;
|
|
1926
|
-
writeGlobalConfig(config);
|
|
1927
|
-
}
|
|
1928
|
-
function extensionsNpmDir() {
|
|
1929
|
-
return resolve7(voluteHome(), "extensions", "_npm");
|
|
1930
|
-
}
|
|
1931
|
-
function ensureExtensionsNpmDir() {
|
|
1932
|
-
const dir = extensionsNpmDir();
|
|
1933
|
-
mkdirSync2(dir, { recursive: true });
|
|
1934
|
-
const pkgPath = resolve7(dir, "package.json");
|
|
1935
|
-
if (!existsSync3(pkgPath)) {
|
|
1936
|
-
writeFileSync2(pkgPath, '{"private":true,"dependencies":{}}\n');
|
|
1937
|
-
}
|
|
1938
|
-
return dir;
|
|
1939
|
-
}
|
|
1940
|
-
function writeExtensionsConfig(packages) {
|
|
1941
|
-
const configPath2 = extensionsConfigPath();
|
|
1942
|
-
mkdirSync2(resolve7(configPath2, ".."), { recursive: true });
|
|
1943
|
-
writeFileSync2(configPath2, `${JSON.stringify(packages, null, 2)}
|
|
1944
|
-
`);
|
|
1945
|
-
}
|
|
1946
|
-
var VALID_NPM_PACKAGE = /^(@[a-z0-9-~][a-z0-9._-~]*\/)?[a-z0-9-~][a-z0-9._-~]*(@[^\s]+)?$/;
|
|
1947
|
-
async function installNpmExtension(pkg) {
|
|
1948
|
-
if (!VALID_NPM_PACKAGE.test(pkg)) {
|
|
1949
|
-
throw new Error(`Invalid package name: "${pkg}"`);
|
|
1950
|
-
}
|
|
1951
|
-
const packages = readExtensionsConfig();
|
|
1952
|
-
if (packages.includes(pkg)) {
|
|
1953
|
-
throw new Error(`Extension "${pkg}" is already installed`);
|
|
1954
|
-
}
|
|
1955
|
-
const dir = ensureExtensionsNpmDir();
|
|
1956
|
-
const { exec } = await import("./exec-DVLXKRIO.js");
|
|
1957
|
-
try {
|
|
1958
|
-
await exec("npm", ["install", pkg], { cwd: dir });
|
|
1959
|
-
} catch (err) {
|
|
1960
|
-
logger_default.error(`npm install failed for "${pkg}"`, logger_default.errorData(err));
|
|
1961
|
-
throw new Error(`Failed to install "${pkg}". Check daemon logs for details.`);
|
|
1962
|
-
}
|
|
1963
|
-
packages.push(pkg);
|
|
1964
|
-
writeExtensionsConfig(packages);
|
|
1965
|
-
logger_default.info(`installed extension package: ${pkg}`);
|
|
1966
|
-
}
|
|
1967
|
-
async function uninstallNpmExtension(pkg) {
|
|
1968
|
-
const packages = readExtensionsConfig();
|
|
1969
|
-
const idx = packages.indexOf(pkg);
|
|
1970
|
-
if (idx === -1) {
|
|
1971
|
-
throw new Error(`Extension "${pkg}" is not installed`);
|
|
1972
|
-
}
|
|
1973
|
-
await cleanupExtensionSkills(pkg);
|
|
1974
|
-
packages.splice(idx, 1);
|
|
1975
|
-
writeExtensionsConfig(packages);
|
|
1976
|
-
try {
|
|
1977
|
-
const { exec } = await import("./exec-DVLXKRIO.js");
|
|
1978
|
-
await exec("npm", ["uninstall", pkg], { cwd: extensionsNpmDir() });
|
|
1979
|
-
} catch (err) {
|
|
1980
|
-
logger_default.warn(
|
|
1981
|
-
`npm uninstall failed for "${pkg}" (may have been manually removed)`,
|
|
1982
|
-
logger_default.errorData(err)
|
|
1983
|
-
);
|
|
1984
|
-
}
|
|
1985
|
-
logger_default.info(`uninstalled extension package: ${pkg}`);
|
|
1986
|
-
}
|
|
1987
|
-
async function cleanupExtensionSkills(pkg) {
|
|
1988
|
-
try {
|
|
1989
|
-
const pkgDir = resolve7(extensionsNpmDir(), "node_modules", pkg);
|
|
1990
|
-
if (!existsSync3(pkgDir)) return;
|
|
1991
|
-
const { createRequire } = await import("module");
|
|
1992
|
-
const require2 = createRequire(resolve7(extensionsNpmDir(), "noop.js"));
|
|
1993
|
-
const mod = require2(pkg);
|
|
1994
|
-
const manifest = mod.default ?? mod.extension ?? mod;
|
|
1995
|
-
if (!manifest?.skillsDir || !existsSync3(manifest.skillsDir)) return;
|
|
1996
|
-
const skillDirs = readdirSync2(manifest.skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1997
|
-
for (const skillId of skillDirs) {
|
|
1998
|
-
try {
|
|
1999
|
-
await removeSharedSkill(skillId);
|
|
2000
|
-
logger_default.info(`removed skill "${skillId}" from extension ${pkg}`);
|
|
2001
|
-
} catch (err) {
|
|
2002
|
-
logger_default.warn(`failed to remove skill "${skillId}" for extension ${pkg}`, logger_default.errorData(err));
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
} catch (err) {
|
|
2006
|
-
logger_default.warn(`could not clean up skills for "${pkg}"`, logger_default.errorData(err));
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
function getExtensionStandardSkills() {
|
|
2010
|
-
const skills = [];
|
|
2011
|
-
for (const { manifest } of loaded) {
|
|
2012
|
-
if (!manifest.standardSkill) continue;
|
|
2013
|
-
const dir = resolveSkillsDir(manifest);
|
|
2014
|
-
if (!dir) continue;
|
|
2015
|
-
try {
|
|
2016
|
-
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2017
|
-
if (entry.isDirectory()) skills.push(entry.name);
|
|
2018
|
-
}
|
|
2019
|
-
} catch (err) {
|
|
2020
|
-
logger_default.warn(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
return skills;
|
|
2024
|
-
}
|
|
2025
|
-
function notifyExtensionsDaemonStart() {
|
|
2026
|
-
for (const { manifest } of loaded) {
|
|
2027
|
-
try {
|
|
2028
|
-
manifest.onDaemonStart?.();
|
|
2029
|
-
} catch (err) {
|
|
2030
|
-
logger_default.error(`extension ${manifest.id}: onDaemonStart failed`, logger_default.errorData(err));
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
function notifyExtensionsDaemonStop() {
|
|
2035
|
-
for (const { manifest, context } of loaded) {
|
|
2036
|
-
try {
|
|
2037
|
-
manifest.onDaemonStop?.();
|
|
2038
|
-
} catch (err) {
|
|
2039
|
-
logger_default.error(`extension ${manifest.id}: onDaemonStop failed`, logger_default.errorData(err));
|
|
2040
|
-
}
|
|
2041
|
-
try {
|
|
2042
|
-
context.db?.close();
|
|
2043
|
-
} catch (err) {
|
|
2044
|
-
logger_default.warn(`extension ${manifest.id}: failed to close db`, logger_default.errorData(err));
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
loaded.length = 0;
|
|
2048
|
-
discovered.length = 0;
|
|
2049
|
-
}
|
|
2050
|
-
function notifyExtensionsMindStart(mindName) {
|
|
2051
|
-
for (const { manifest } of loaded) {
|
|
2052
|
-
try {
|
|
2053
|
-
manifest.onMindStart?.(mindName);
|
|
2054
|
-
} catch (err) {
|
|
2055
|
-
logger_default.error(`extension ${manifest.id}: onMindStart failed for ${mindName}`, logger_default.errorData(err));
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
function notifyExtensionsMindStop(mindName) {
|
|
2060
|
-
for (const { manifest } of loaded) {
|
|
2061
|
-
try {
|
|
2062
|
-
manifest.onMindStop?.(mindName);
|
|
2063
|
-
} catch (err) {
|
|
2064
|
-
logger_default.error(`extension ${manifest.id}: onMindStop failed for ${mindName}`, logger_default.errorData(err));
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
export {
|
|
2070
|
-
readSystemsConfig,
|
|
2071
|
-
writeSystemsConfig,
|
|
2072
|
-
deleteSystemsConfig,
|
|
2073
|
-
loadAllExtensions,
|
|
2074
|
-
getLoadedExtensions,
|
|
2075
|
-
getAllDiscoveredExtensions,
|
|
2076
|
-
setExtensionEnabled,
|
|
2077
|
-
installNpmExtension,
|
|
2078
|
-
uninstallNpmExtension,
|
|
2079
|
-
getExtensionStandardSkills,
|
|
2080
|
-
notifyExtensionsDaemonStart,
|
|
2081
|
-
notifyExtensionsDaemonStop,
|
|
2082
|
-
notifyExtensionsMindStart,
|
|
2083
|
-
notifyExtensionsMindStop
|
|
2084
|
-
};
|