volute 0.33.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-ZW4SDL2C.js +15 -0
- package/dist/{ai-service-SBY2WG7O.js → ai-service-LURBEDDB.js} +6 -6
- package/dist/{api-client-YPKOZP2O.js → api-client-3A77HMH7.js} +2 -2
- package/dist/api.d.ts +1 -5195
- package/dist/{archive-INXYFVCW.js → archive-ESU2FUN4.js} +4 -4
- package/dist/{auth-GKCDSO4T.js → auth-WX4TESEI.js} +6 -6
- package/dist/bridge-PXIO6PS2.js +206 -0
- package/dist/chat-QXAJF3FU.js +51 -0
- package/dist/{chunk-NNB4WIG7.js → chunk-2TGZJFAT.js} +3 -3
- package/dist/{chunk-6LXAAQ43.js → chunk-33ODGMFZ.js} +1 -1
- package/dist/{chunk-RPZZSXV3.js → chunk-5N7Y5WAM.js} +21 -2
- package/dist/chunk-5T5YMX6S.js +23 -0
- package/dist/{chunk-7J3HEVR7.js → chunk-5XJYUFZH.js} +28 -16
- package/dist/chunk-7KJOFUNN.js +22 -0
- package/dist/{chunk-2NGTS5UU.js → chunk-A2ZLHBHG.js} +2 -2
- package/dist/{chunk-KIEPMIM5.js → chunk-AN2W47GW.js} +2 -2
- package/dist/{chunk-G53F3JA4.js → chunk-AOB6GVRM.js} +1 -1
- package/dist/{chunk-LRCG2JLP.js → chunk-BDYXIWA5.js} +9 -5
- package/dist/{chunk-YUIHSKR6.js → chunk-BKF4WQCY.js} +2 -2
- package/dist/{chunk-N432I7QH.js → chunk-BMZQYACC.js} +2 -2
- package/dist/{chunk-NAOW2CLO.js → chunk-BTY4WNFE.js} +1 -1
- package/dist/{chunk-ALEF47VT.js → chunk-BV65KRHM.js} +2 -2
- package/dist/{chunk-KVK2DLWI.js → chunk-CORXD635.js} +4 -4
- package/dist/{chunk-PVY5W6QN.js → chunk-F7ZNLYKZ.js} +2 -2
- package/dist/{chunk-QTUVYI7W.js → chunk-FT5KETXZ.js} +3 -3
- package/dist/{chunk-C7I35G4R.js → chunk-IJHIXLVN.js} +44 -8
- package/dist/{chunk-JUKK7FPS.js → chunk-J6CJQDWI.js} +37 -28
- package/dist/{chunk-4RQBJWQX.js → chunk-LOPXTW6H.js} +1 -1
- package/dist/{chunk-RSX4OPZY.js → chunk-MDJGMOSD.js} +8 -137
- package/dist/{chunk-LOEJ4HPQ.js → chunk-N446KRP7.js} +3 -3
- package/dist/{chunk-I5KY25PQ.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-M7UL5S3Q.js → chunk-OTC67N2Z.js} +2 -2
- package/dist/{chunk-GY5HBI7A.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-SKLSMHXO.js → chunk-QWTR6AWZ.js} +3 -3
- package/dist/chunk-TXSA4Q3V.js +116 -0
- package/dist/{chunk-VH33ZWMW.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-RVGLDGMI.js → chunk-WZRZFFCL.js} +25 -27
- package/dist/{chunk-JYVGHWEJ.js → chunk-XRQSAMX2.js} +4 -4
- package/dist/{chunk-OYAKCAVY.js → chunk-ZSR72JB3.js} +1 -1
- package/dist/{chunk-UKVWJRKN.js → chunk-ZX7EAV5J.js} +17 -7
- package/dist/cli.js +90 -29
- package/dist/clock-HSEKS5AR.js +289 -0
- package/dist/{cloud-sync-4NWLMFVH.js → cloud-sync-6JL4C24T.js} +22 -23
- package/dist/config-UTS7QULS.js +76 -0
- package/dist/connectors/discord-bridge.js +4 -4
- package/dist/connectors/slack-bridge.js +4 -4
- package/dist/connectors/telegram-bridge.js +4 -4
- package/dist/{conversations-AWI5SZW2.js → conversations-2PW57WO2.js} +6 -6
- 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 +1349 -1211
- package/dist/db-BDMH4SZ2.js +20 -0
- package/dist/db-BVBJ57TU.js +9 -0
- package/dist/delete-L5PAVDGQ.js +42 -0
- package/dist/delivery-manager-H5ZVBMCQ.js +31 -0
- package/dist/{delivery-router-FL45JL7N.js → delivery-router-HEJSJAHQ.js} +5 -5
- package/dist/down-74VXM45A.js +17 -0
- package/dist/env-E4XHO2BI.js +223 -0
- package/dist/exec-PY7THYH4.js +17 -0
- package/dist/export-OAS6QVBN.js +113 -0
- package/dist/extension-D74CNM7G.js +89 -0
- package/dist/extensions-XDDFY72A.js +49 -0
- package/dist/files-CWTK6V3H.js +53 -0
- package/dist/import-5A3T7QV4.js +143 -0
- package/dist/{isolation-LLAYQYDY.js → isolation-TK5RX2WM.js} +4 -4
- package/dist/join-DF5XSJAC.js +67 -0
- package/dist/lib-DYEZMGW7.js +6588 -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-QBLIV7ZJ.js +18 -0
- package/dist/mind-history-IE2QH7U5.js +275 -0
- 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-U3VFO273.js → package-D2FSVFAX.js} +11 -8
- package/dist/read-67VRP2DO.js +91 -0
- package/dist/{read-stdin-HQJ7774D.js → read-stdin-3X5VYKNS.js} +2 -2
- package/dist/register-SB7NXCOE.js +51 -0
- package/dist/{registry-PJ4S5PHQ.js → registry-GBSNW3HG.js} +3 -3
- package/dist/reject-MUR2KWJ4.js +40 -0
- package/dist/restart-5EGG4JXU.js +42 -0
- package/dist/{sandbox-GJOK4QLQ.js → sandbox-R37VIU36.js} +6 -6
- package/dist/scheduler-Y7O4CJXL.js +31 -0
- package/dist/{schema-PA3M5ZKH.js → schema-XVZ2CLKW.js} +4 -2
- package/dist/{seed-QDYVLG74.js → seed-EQORWX77.js} +3 -3
- 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-XMCBE3LF.js → setup-BJ4YAY26.js} +155 -129
- package/dist/{setup-TISPCO22.js → setup-RHJRFURI.js} +4 -4
- package/dist/skill-TAAKEYBV.js +389 -0
- package/dist/skills/plan-coordinator/SKILL.md +60 -0
- package/dist/skills/volute-mind/SKILL.md +9 -227
- package/dist/skills/volute-mind/references/extensions.md +34 -0
- package/dist/skills/volute-mind/references/integrations.md +48 -0
- package/dist/skills/volute-mind/references/routing.md +86 -0
- package/dist/skills/volute-mind/references/sleep.md +33 -0
- package/dist/skills/volute-mind/references/variants.md +31 -0
- package/dist/{skills-7FV7EJTE.js → skills-EKMCQ46K.js} +12 -8
- 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-WKLZXUIQ.js → sprout-HE4TITMK.js} +3 -3
- 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-XHQBZROW.js → tailscale-ZIZ2HWJ5.js} +5 -5
- package/dist/template-hash-A7FNHTB7.js +9 -0
- package/dist/up-77ICEDEW.js +19 -0
- package/dist/update-ANE5ZM7F.js +225 -0
- package/dist/{update-check-ZD6OOIYQ.js → update-check-UV55CBEP.js} +4 -4
- package/dist/upgrade-ZMDGC7M2.js +74 -0
- package/dist/variant-QWL2WSRI.js +62 -0
- package/dist/{version-notify-NBI2MTJO.js → version-notify-FXSEMXWW.js} +29 -28
- package/dist/{volute-config-HD7WWUQC.js → volute-config-D2XVS2YI.js} +2 -2
- package/dist/web-assets/assets/index-BhxWKvbB.css +1 -0
- package/dist/web-assets/assets/index-CHVKJ9II.js +75 -0
- package/dist/web-assets/ext-theme.css +48 -9
- package/dist/web-assets/index.html +2 -2
- package/dist/web-assets/sw.js +117 -0
- package/drizzle/0005_meta_summaries.sql +15 -0
- package/drizzle/meta/0005_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +10 -7
- 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/packages/extensions/plan/dist/ui/assets/index-CJj2gZnZ.css +1 -0
- package/packages/extensions/plan/dist/ui/assets/index-FMEJmvQz.js +61 -0
- package/packages/extensions/plan/dist/ui/index.html +14 -0
- package/packages/extensions/plan/skills/plan/SKILL.md +43 -0
- package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +37 -0
- package/templates/_base/home/VOLUTE.md +12 -19
- package/templates/_base/src/lib/auto-commit.ts +8 -8
- package/templates/_base/src/lib/context-breakdown.ts +450 -0
- package/templates/_base/src/lib/format-prefix.ts +17 -0
- package/templates/_base/src/lib/hook-loader.ts +8 -2
- package/templates/_base/src/lib/router.ts +75 -33
- package/templates/_base/src/lib/routing.ts +4 -1
- package/templates/_base/src/lib/startup.ts +16 -8
- package/templates/_base/src/lib/types.ts +2 -1
- package/templates/_base/src/lib/volute-server.ts +75 -8
- package/templates/claude/.init/CLAUDE.md +4 -10
- package/templates/claude/package.json.tmpl +1 -0
- package/templates/claude/src/agent.ts +108 -33
- package/templates/claude/src/lib/hooks/reply-instructions.ts +27 -7
- package/templates/claude/src/lib/stream-consumer.ts +2 -2
- package/templates/claude/src/server.ts +1 -0
- package/templates/codex/package.json.tmpl +1 -0
- package/templates/codex/src/agent.ts +80 -8
- package/templates/codex/src/server.ts +1 -4
- package/templates/pi/package.json.tmpl +1 -0
- package/templates/pi/src/agent.ts +115 -36
- package/templates/pi/src/lib/event-handler.ts +22 -7
- package/templates/pi/src/lib/reply-instructions-extension.ts +23 -4
- package/templates/pi/src/lib/subagents.ts +20 -17
- package/templates/pi/src/server.ts +2 -5
- package/dist/accept-D5VBM7JW.js +0 -42
- package/dist/activity-events-XJO3P4RR.js +0 -15
- package/dist/bridge-TXWWPPOJ.js +0 -207
- package/dist/chat-U5ZOME3O.js +0 -68
- package/dist/chunk-3Z2DPESO.js +0 -3634
- package/dist/chunk-A2A4KLFE.js +0 -1528
- package/dist/chunk-K3NQKI34.js +0 -10
- package/dist/chunk-NPKSDYA2.js +0 -156
- package/dist/chunk-PB65JZK2.js +0 -85
- package/dist/clock-BVH3V6E3.js +0 -266
- package/dist/config-H2H4UIF7.js +0 -72
- package/dist/create-2FK7Z46Y.js +0 -44
- package/dist/create-YWD2TIP4.js +0 -71
- package/dist/daemon-client-6QXHZ7US.js +0 -12
- package/dist/daemon-restart-GOBUKLX7.js +0 -52
- package/dist/db-F34YLV7D.js +0 -9
- package/dist/db-RA45JBFG.js +0 -16
- package/dist/delete-QTGWEDBI.js +0 -35
- package/dist/delivery-manager-PFAKEJTC.js +0 -32
- package/dist/down-FWWTEKXM.js +0 -15
- package/dist/env-JCOF2222.js +0 -191
- package/dist/export-SUYRLI5Q.js +0 -112
- package/dist/extension-OBTGKQQD.js +0 -175
- package/dist/extensions-KYNTVTMO.js +0 -30
- package/dist/files-65PMW5IK.js +0 -47
- package/dist/history-DKCDI3JO.js +0 -128
- package/dist/import-DDUFE7AY.js +0 -23
- package/dist/join-I5QEE3LG.js +0 -66
- package/dist/list-JQ463EDA.js +0 -41
- package/dist/login-D7ETSU4R.js +0 -47
- package/dist/login-RIJF2F4G.js +0 -47
- package/dist/logout-5MLHZALK.js +0 -40
- package/dist/logout-UZJRGY4Z.js +0 -21
- package/dist/message-delivery-DFF5SJRM.js +0 -42
- package/dist/mind-IOJFLEM5.js +0 -108
- package/dist/mind-activity-tracker-F6O4Q2SL.js +0 -18
- package/dist/mind-list-WUPMQDYQ.js +0 -30
- package/dist/mind-manager-NBJF5D26.js +0 -32
- package/dist/mind-profile-P67FEHOY.js +0 -47
- package/dist/mind-service-2MQ6UK5N.js +0 -38
- package/dist/mind-sleep-WW2IX7JT.js +0 -42
- package/dist/mind-status-L3EFFRPR.js +0 -56
- package/dist/mind-wake-VSSGW465.js +0 -37
- package/dist/read-EBY56C33.js +0 -75
- package/dist/register-HD74C4TT.js +0 -47
- package/dist/reject-UJKFBHRO.js +0 -40
- package/dist/restart-3UCMRUVC.js +0 -33
- package/dist/scheduler-ZZ7XGQG6.js +0 -32
- package/dist/seed-check-S2IX25RL.js +0 -32
- package/dist/seed-cmd-DKOUFEAU.js +0 -36
- package/dist/seed-create-4XBBOLRH.js +0 -112
- package/dist/seed-sprout-GQEIIQRT.js +0 -132
- package/dist/send-QIV2INHB.js +0 -373
- package/dist/skill-PSQGRRJX.js +0 -358
- 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-JTXSN7NV.js +0 -36
- package/dist/spirit-VRONKFMF.js +0 -23
- package/dist/split-STOROBYJ.js +0 -63
- package/dist/start-K2NCUUCG.js +0 -33
- package/dist/status-3JBTFSMI.js +0 -115
- package/dist/stop-H26JZDXF.js +0 -32
- package/dist/system-chat-JAPOJ3KE.js +0 -36
- package/dist/systems-XRI52VCH.js +0 -61
- package/dist/template-hash-A6VVKOXJ.js +0 -9
- package/dist/up-M5AS6SBV.js +0 -18
- package/dist/update-UD543CXX.js +0 -215
- package/dist/upgrade-O4Q7WJM3.js +0 -67
- package/dist/variant-7TGZHOU3.js +0 -41
- package/dist/web-assets/assets/index-CWJrVveV.css +0 -1
- package/dist/web-assets/assets/index-DJt14FRI.js +0 -75
- package/packages/extensions/pages/dist/ui/assets/index-tLTROSk5.js +0 -2
package/dist/chunk-A2A4KLFE.js
DELETED
|
@@ -1,1528 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getAllSites,
|
|
4
|
-
getPublishedPages,
|
|
5
|
-
getRecentPages,
|
|
6
|
-
initDb,
|
|
7
|
-
syncPublishedPages
|
|
8
|
-
} from "./chunk-PB65JZK2.js";
|
|
9
|
-
import {
|
|
10
|
-
getUser,
|
|
11
|
-
getUserByUsername
|
|
12
|
-
} from "./chunk-JYVGHWEJ.js";
|
|
13
|
-
import {
|
|
14
|
-
publish
|
|
15
|
-
} from "./chunk-KVK2DLWI.js";
|
|
16
|
-
import {
|
|
17
|
-
hashSkillDir,
|
|
18
|
-
importSkillFromDir,
|
|
19
|
-
sharedSkillsDir
|
|
20
|
-
} from "./chunk-C7I35G4R.js";
|
|
21
|
-
import {
|
|
22
|
-
logger_default
|
|
23
|
-
} from "./chunk-YUIHSKR6.js";
|
|
24
|
-
import {
|
|
25
|
-
mindDir,
|
|
26
|
-
voluteHome,
|
|
27
|
-
voluteSystemDir
|
|
28
|
-
} from "./chunk-LRCG2JLP.js";
|
|
29
|
-
|
|
30
|
-
// src/lib/extensions.ts
|
|
31
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
32
|
-
import { dirname, resolve as resolve6 } from "path";
|
|
33
|
-
|
|
34
|
-
// packages/extensions/notes/src/index.ts
|
|
35
|
-
import { resolve } from "path";
|
|
36
|
-
|
|
37
|
-
// packages/extensions/sdk/src/index.ts
|
|
38
|
-
var VALID_EXTENSION_ID = /^[a-z0-9][a-z0-9_-]*$/;
|
|
39
|
-
function createExtension(manifest) {
|
|
40
|
-
if (!manifest.id) throw new Error("Extension manifest requires an id");
|
|
41
|
-
if (!VALID_EXTENSION_ID.test(manifest.id))
|
|
42
|
-
throw new Error(
|
|
43
|
-
"Extension id must be lowercase alphanumeric with hyphens/underscores, starting with a letter or digit"
|
|
44
|
-
);
|
|
45
|
-
if (typeof manifest.routes !== "function")
|
|
46
|
-
throw new Error("Extension manifest requires a routes function");
|
|
47
|
-
return manifest;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// packages/extensions/notes/src/notes.ts
|
|
51
|
-
function slugify(text) {
|
|
52
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
53
|
-
}
|
|
54
|
-
async function createNote(db, getUser2, authorId, title, content, replyToId) {
|
|
55
|
-
let slug = slugify(title) || "untitled";
|
|
56
|
-
const existing = db.prepare("SELECT slug FROM notes WHERE author_id = ?").all(authorId);
|
|
57
|
-
const existingSlugs = new Set(existing.map((r) => r.slug));
|
|
58
|
-
if (existingSlugs.has(slug)) {
|
|
59
|
-
let i = 2;
|
|
60
|
-
while (existingSlugs.has(`${slug}-${i}`)) i++;
|
|
61
|
-
slug = `${slug}-${i}`;
|
|
62
|
-
}
|
|
63
|
-
const row = db.prepare(
|
|
64
|
-
`INSERT INTO notes (author_id, title, slug, content, reply_to_id)
|
|
65
|
-
VALUES (?, ?, ?, ?, ?)
|
|
66
|
-
RETURNING *`
|
|
67
|
-
).get(authorId, title, slug, content, replyToId ?? null);
|
|
68
|
-
const author = await getUser2(authorId);
|
|
69
|
-
return {
|
|
70
|
-
...row,
|
|
71
|
-
author_username: author?.username ?? "unknown",
|
|
72
|
-
author_display_name: author?.display_name ?? null,
|
|
73
|
-
comment_count: 0
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
async function getNote(db, getUser2, getUserByUsername2, authorUsername, slug) {
|
|
77
|
-
const author = await getUserByUsername2(authorUsername);
|
|
78
|
-
if (!author) return null;
|
|
79
|
-
const row = db.prepare("SELECT * FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
80
|
-
if (!row) return null;
|
|
81
|
-
const comments = await getComments(db, getUser2, row.id);
|
|
82
|
-
const reactions = await getReactions(db, getUser2, row.id);
|
|
83
|
-
let reply_to = null;
|
|
84
|
-
if (row.reply_to_id) {
|
|
85
|
-
const parent = db.prepare("SELECT * FROM notes WHERE id = ?").get(row.reply_to_id);
|
|
86
|
-
if (parent) {
|
|
87
|
-
const parentAuthor = await getUser2(parent.author_id);
|
|
88
|
-
reply_to = {
|
|
89
|
-
author_username: parentAuthor?.username ?? "unknown",
|
|
90
|
-
slug: parent.slug,
|
|
91
|
-
title: parent.title
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const replyRows = db.prepare("SELECT * FROM notes WHERE reply_to_id = ? ORDER BY created_at").all(row.id);
|
|
96
|
-
const replies = [];
|
|
97
|
-
for (const r of replyRows) {
|
|
98
|
-
const replyAuthor = await getUser2(r.author_id);
|
|
99
|
-
replies.push({
|
|
100
|
-
author_username: replyAuthor?.username ?? "unknown",
|
|
101
|
-
slug: r.slug,
|
|
102
|
-
title: r.title,
|
|
103
|
-
created_at: r.created_at
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
...row,
|
|
108
|
-
author_username: authorUsername,
|
|
109
|
-
author_display_name: author.display_name ?? null,
|
|
110
|
-
comment_count: comments.length,
|
|
111
|
-
comments,
|
|
112
|
-
reactions,
|
|
113
|
-
reply_to,
|
|
114
|
-
replies
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
async function listNotes(db, getUser2, getUserByUsername2, opts) {
|
|
118
|
-
const limit = opts?.limit ?? 50;
|
|
119
|
-
const offset = opts?.offset ?? 0;
|
|
120
|
-
let authorId;
|
|
121
|
-
if (opts?.authorUsername) {
|
|
122
|
-
const author = await getUserByUsername2(opts.authorUsername);
|
|
123
|
-
if (!author) return [];
|
|
124
|
-
authorId = author.id;
|
|
125
|
-
}
|
|
126
|
-
const rows = authorId ? db.prepare(
|
|
127
|
-
"SELECT * FROM notes WHERE author_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
128
|
-
).all(authorId, limit, offset) : db.prepare("SELECT * FROM notes ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
129
|
-
if (rows.length === 0) return [];
|
|
130
|
-
const noteIds = rows.map((r) => r.id);
|
|
131
|
-
const commentCounts = db.prepare(
|
|
132
|
-
`SELECT note_id, COUNT(*) as count FROM note_comments
|
|
133
|
-
WHERE note_id IN (${noteIds.map(() => "?").join(",")})
|
|
134
|
-
GROUP BY note_id`
|
|
135
|
-
).all(...noteIds);
|
|
136
|
-
const countMap = new Map(commentCounts.map((r) => [r.note_id, r.count]));
|
|
137
|
-
const allReactions = db.prepare(
|
|
138
|
-
`SELECT note_id, emoji, COUNT(*) as count FROM note_reactions
|
|
139
|
-
WHERE note_id IN (${noteIds.map(() => "?").join(",")})
|
|
140
|
-
GROUP BY note_id, emoji`
|
|
141
|
-
).all(...noteIds);
|
|
142
|
-
const reactionMap = /* @__PURE__ */ new Map();
|
|
143
|
-
for (const r of allReactions) {
|
|
144
|
-
if (!reactionMap.has(r.note_id)) reactionMap.set(r.note_id, []);
|
|
145
|
-
reactionMap.get(r.note_id).push({ emoji: r.emoji, count: r.count });
|
|
146
|
-
}
|
|
147
|
-
const replyToIds = [
|
|
148
|
-
...new Set(rows.filter((r) => r.reply_to_id).map((r) => r.reply_to_id))
|
|
149
|
-
];
|
|
150
|
-
const replyToMap = /* @__PURE__ */ new Map();
|
|
151
|
-
if (replyToIds.length > 0) {
|
|
152
|
-
const parents = db.prepare(`SELECT * FROM notes WHERE id IN (${replyToIds.map(() => "?").join(",")})`).all(...replyToIds);
|
|
153
|
-
for (const parent of parents) {
|
|
154
|
-
const parentAuthor = await getUser2(parent.author_id);
|
|
155
|
-
replyToMap.set(parent.id, {
|
|
156
|
-
author_username: parentAuthor?.username ?? "unknown",
|
|
157
|
-
slug: parent.slug,
|
|
158
|
-
title: parent.title
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
const authorCache = /* @__PURE__ */ new Map();
|
|
163
|
-
const result = [];
|
|
164
|
-
for (const r of rows) {
|
|
165
|
-
if (!authorCache.has(r.author_id)) {
|
|
166
|
-
const u = await getUser2(r.author_id);
|
|
167
|
-
authorCache.set(r.author_id, {
|
|
168
|
-
username: u?.username ?? "unknown",
|
|
169
|
-
display_name: u?.display_name ?? null
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
const authorInfo = authorCache.get(r.author_id);
|
|
173
|
-
const reactions = reactionMap.get(r.id);
|
|
174
|
-
const topReactions = reactions ? reactions.sort((a, b) => b.count - a.count).slice(0, 3).map((rx) => ({ ...rx, usernames: [] })) : void 0;
|
|
175
|
-
result.push({
|
|
176
|
-
...r,
|
|
177
|
-
author_username: authorInfo.username,
|
|
178
|
-
author_display_name: authorInfo.display_name,
|
|
179
|
-
comment_count: countMap.get(r.id) ?? 0,
|
|
180
|
-
reactions: topReactions,
|
|
181
|
-
reply_to: r.reply_to_id ? replyToMap.get(r.reply_to_id) ?? null : null
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
async function updateNote(db, getUser2, getUserByUsername2, authorUsername, slug, updates) {
|
|
187
|
-
const author = await getUserByUsername2(authorUsername);
|
|
188
|
-
if (!author) return null;
|
|
189
|
-
const existing = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
190
|
-
if (!existing) return null;
|
|
191
|
-
const sets = ["updated_at = datetime('now')"];
|
|
192
|
-
const params = [];
|
|
193
|
-
if (updates.title !== void 0) {
|
|
194
|
-
sets.push("title = ?");
|
|
195
|
-
params.push(updates.title);
|
|
196
|
-
}
|
|
197
|
-
if (updates.content !== void 0) {
|
|
198
|
-
sets.push("content = ?");
|
|
199
|
-
params.push(updates.content);
|
|
200
|
-
}
|
|
201
|
-
params.push(existing.id);
|
|
202
|
-
db.prepare(`UPDATE notes SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
203
|
-
const full = await getNote(db, getUser2, getUserByUsername2, authorUsername, slug);
|
|
204
|
-
if (!full) return null;
|
|
205
|
-
const { comments, replies, ...note } = full;
|
|
206
|
-
return note;
|
|
207
|
-
}
|
|
208
|
-
async function deleteNote(db, getUserByUsername2, authorUsername, slug, authorId) {
|
|
209
|
-
const author = await getUserByUsername2(authorUsername);
|
|
210
|
-
if (!author) return false;
|
|
211
|
-
const existing = db.prepare("SELECT id, author_id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
212
|
-
if (!existing || existing.author_id !== authorId) return false;
|
|
213
|
-
db.prepare("DELETE FROM notes WHERE id = ?").run(existing.id);
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
async function addComment(db, getUser2, noteId, authorId, content) {
|
|
217
|
-
const row = db.prepare(`INSERT INTO note_comments (note_id, author_id, content) VALUES (?, ?, ?) RETURNING *`).get(noteId, authorId, content);
|
|
218
|
-
const author = await getUser2(authorId);
|
|
219
|
-
return {
|
|
220
|
-
...row,
|
|
221
|
-
author_username: author?.username ?? "unknown",
|
|
222
|
-
author_display_name: author?.display_name ?? null
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
async function getComments(db, getUser2, noteId) {
|
|
226
|
-
const rows = db.prepare("SELECT * FROM note_comments WHERE note_id = ? ORDER BY created_at").all(noteId);
|
|
227
|
-
const result = [];
|
|
228
|
-
for (const row of rows) {
|
|
229
|
-
const author = await getUser2(row.author_id);
|
|
230
|
-
result.push({
|
|
231
|
-
...row,
|
|
232
|
-
author_username: author?.username ?? "unknown",
|
|
233
|
-
author_display_name: author?.display_name ?? null
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
async function deleteComment(db, commentId, authorId) {
|
|
239
|
-
const existing = db.prepare("SELECT id, author_id FROM note_comments WHERE id = ?").get(commentId);
|
|
240
|
-
if (!existing || existing.author_id !== authorId) return false;
|
|
241
|
-
db.prepare("DELETE FROM note_comments WHERE id = ?").run(existing.id);
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
function toggleReaction(db, noteId, userId, emoji) {
|
|
245
|
-
const existing = db.prepare("SELECT id FROM note_reactions WHERE note_id = ? AND user_id = ? AND emoji = ?").get(noteId, userId, emoji);
|
|
246
|
-
if (existing) {
|
|
247
|
-
db.prepare("DELETE FROM note_reactions WHERE id = ?").run(existing.id);
|
|
248
|
-
return { added: false };
|
|
249
|
-
}
|
|
250
|
-
db.prepare("INSERT INTO note_reactions (note_id, user_id, emoji) VALUES (?, ?, ?)").run(
|
|
251
|
-
noteId,
|
|
252
|
-
userId,
|
|
253
|
-
emoji
|
|
254
|
-
);
|
|
255
|
-
return { added: true };
|
|
256
|
-
}
|
|
257
|
-
async function getReactions(db, getUser2, noteId) {
|
|
258
|
-
const rows = db.prepare("SELECT * FROM note_reactions WHERE note_id = ? ORDER BY emoji").all(noteId);
|
|
259
|
-
const userCache = /* @__PURE__ */ new Map();
|
|
260
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
261
|
-
for (const r of rows) {
|
|
262
|
-
if (!grouped.has(r.emoji)) grouped.set(r.emoji, []);
|
|
263
|
-
grouped.get(r.emoji).push(r.user_id);
|
|
264
|
-
}
|
|
265
|
-
const result = [];
|
|
266
|
-
for (const [emoji, userIds] of grouped) {
|
|
267
|
-
const usernames = [];
|
|
268
|
-
for (const uid of userIds) {
|
|
269
|
-
if (!userCache.has(uid)) {
|
|
270
|
-
const u = await getUser2(uid);
|
|
271
|
-
userCache.set(uid, u?.username ?? "unknown");
|
|
272
|
-
}
|
|
273
|
-
usernames.push(userCache.get(uid));
|
|
274
|
-
}
|
|
275
|
-
result.push({ emoji, count: userIds.length, usernames });
|
|
276
|
-
}
|
|
277
|
-
return result;
|
|
278
|
-
}
|
|
279
|
-
async function resolveNoteId(db, getUserByUsername2, authorSlug) {
|
|
280
|
-
const [authorName, slug] = authorSlug.split("/", 2);
|
|
281
|
-
if (!authorName || !slug) return null;
|
|
282
|
-
const author = await getUserByUsername2(authorName);
|
|
283
|
-
if (!author) return null;
|
|
284
|
-
const row = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
|
|
285
|
-
return row?.id ?? null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// packages/extensions/notes/src/commands.ts
|
|
289
|
-
function getFlag(args, flag) {
|
|
290
|
-
const idx = args.indexOf(flag);
|
|
291
|
-
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
292
|
-
return void 0;
|
|
293
|
-
}
|
|
294
|
-
function createCommands() {
|
|
295
|
-
return {
|
|
296
|
-
write: {
|
|
297
|
-
description: "Write a new note",
|
|
298
|
-
usage: 'volute notes write "title" ["content"] [--reply-to author/slug] (content can be piped via stdin)',
|
|
299
|
-
handler: async (args, ctx) => {
|
|
300
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
301
|
-
const mindName = ctx.mindName;
|
|
302
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
303
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
304
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
305
|
-
const title = args[0];
|
|
306
|
-
const content = args[1] ?? ctx.stdin;
|
|
307
|
-
if (!title || !content)
|
|
308
|
-
return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
|
|
309
|
-
let replyToId;
|
|
310
|
-
const replyTo = getFlag(args, "--reply-to");
|
|
311
|
-
if (replyTo) {
|
|
312
|
-
const id = await resolveNoteId(ctx.db, ctx.getUserByUsername, replyTo);
|
|
313
|
-
if (id === null) return { error: `Reply target not found: ${replyTo}` };
|
|
314
|
-
replyToId = id;
|
|
315
|
-
}
|
|
316
|
-
const note = await createNote(ctx.db, ctx.getUser, user.id, title, content, replyToId);
|
|
317
|
-
ctx.publishActivity({
|
|
318
|
-
type: "note_created",
|
|
319
|
-
mind: user.username,
|
|
320
|
-
summary: `${user.username} wrote "${title}"`,
|
|
321
|
-
metadata: { author: user.username, slug: note.slug, bodyHtml: content.slice(0, 500) }
|
|
322
|
-
});
|
|
323
|
-
return { output: `Published: ${note.author_username}/${note.slug}` };
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
list: {
|
|
327
|
-
description: "List notes",
|
|
328
|
-
usage: "volute notes list [--author name] [--limit N]",
|
|
329
|
-
handler: async (args, ctx) => {
|
|
330
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
331
|
-
const author = getFlag(args, "--author");
|
|
332
|
-
const limit = parseInt(getFlag(args, "--limit") ?? "10", 10);
|
|
333
|
-
const notes = await listNotes(ctx.db, ctx.getUser, ctx.getUserByUsername, {
|
|
334
|
-
authorUsername: author,
|
|
335
|
-
limit
|
|
336
|
-
});
|
|
337
|
-
if (notes.length === 0) return { output: "No notes found." };
|
|
338
|
-
const lines = notes.map((n) => {
|
|
339
|
-
const date = new Date(n.created_at).toLocaleDateString();
|
|
340
|
-
return ` ${n.author_username}/${n.slug} "${n.title}" (${date})`;
|
|
341
|
-
});
|
|
342
|
-
return { output: lines.join("\n") };
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
read: {
|
|
346
|
-
description: "Read a note",
|
|
347
|
-
usage: "volute notes read <author/slug>",
|
|
348
|
-
handler: async (args, ctx) => {
|
|
349
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
350
|
-
const ref = args[0];
|
|
351
|
-
if (!ref || !ref.includes("/")) return { error: "Usage: volute notes read <author/slug>" };
|
|
352
|
-
const [author, slug] = ref.split("/", 2);
|
|
353
|
-
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
354
|
-
if (!note) return { error: "Note not found" };
|
|
355
|
-
const lines = [
|
|
356
|
-
`# ${note.title}
|
|
357
|
-
`,
|
|
358
|
-
`By ${note.author_username} \u2014 ${new Date(note.created_at).toLocaleString()}
|
|
359
|
-
`,
|
|
360
|
-
note.content
|
|
361
|
-
];
|
|
362
|
-
if (note.reactions?.length) {
|
|
363
|
-
lines.push(
|
|
364
|
-
`
|
|
365
|
-
Reactions: ${note.reactions.map((r) => `${r.emoji} (${r.count})`).join(" ")}`
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
if (note.comments?.length) {
|
|
369
|
-
lines.push(`
|
|
370
|
-
Comments (${note.comments.length}):`);
|
|
371
|
-
for (const c of note.comments) {
|
|
372
|
-
lines.push(` ${c.author_username}: ${c.content}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return { output: lines.join("\n") };
|
|
376
|
-
}
|
|
377
|
-
},
|
|
378
|
-
comment: {
|
|
379
|
-
description: "Comment on a note",
|
|
380
|
-
usage: 'volute notes comment <author/slug> ["content"] (content can be piped via stdin)',
|
|
381
|
-
handler: async (args, ctx) => {
|
|
382
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
383
|
-
const mindName = ctx.mindName;
|
|
384
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
385
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
386
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
387
|
-
const ref = args[0];
|
|
388
|
-
const content = args[1] ?? ctx.stdin;
|
|
389
|
-
if (!ref || !ref.includes("/") || !content) {
|
|
390
|
-
return { error: 'Usage: volute notes comment <author/slug> "content"' };
|
|
391
|
-
}
|
|
392
|
-
const [author, slug] = ref.split("/", 2);
|
|
393
|
-
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
394
|
-
if (!note) return { error: "Note not found" };
|
|
395
|
-
await addComment(ctx.db, ctx.getUser, note.id, user.id, content);
|
|
396
|
-
return { output: "Comment added." };
|
|
397
|
-
}
|
|
398
|
-
},
|
|
399
|
-
react: {
|
|
400
|
-
description: "React to a note",
|
|
401
|
-
usage: 'volute notes react <author/slug> "emoji"',
|
|
402
|
-
handler: async (args, ctx) => {
|
|
403
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
404
|
-
const mindName = ctx.mindName;
|
|
405
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
406
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
407
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
408
|
-
const ref = args[0];
|
|
409
|
-
const emoji = args[1];
|
|
410
|
-
if (!ref || !ref.includes("/") || !emoji) {
|
|
411
|
-
return { error: 'Usage: volute notes react <author/slug> "emoji"' };
|
|
412
|
-
}
|
|
413
|
-
const [author, slug] = ref.split("/", 2);
|
|
414
|
-
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
415
|
-
if (!note) return { error: "Note not found" };
|
|
416
|
-
const result = toggleReaction(ctx.db, note.id, user.id, emoji);
|
|
417
|
-
return { output: result.added ? "Reaction added." : "Reaction removed." };
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
delete: {
|
|
421
|
-
description: "Delete your own note",
|
|
422
|
-
usage: "volute notes delete <author/slug>",
|
|
423
|
-
handler: async (args, ctx) => {
|
|
424
|
-
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
425
|
-
const mindName = ctx.mindName;
|
|
426
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
427
|
-
const user = await ctx.getUserByUsername(mindName);
|
|
428
|
-
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
429
|
-
const ref = args[0];
|
|
430
|
-
if (!ref || !ref.includes("/"))
|
|
431
|
-
return { error: "Usage: volute notes delete <author/slug>" };
|
|
432
|
-
const [author, slug] = ref.split("/", 2);
|
|
433
|
-
const deleted = await deleteNote(ctx.db, ctx.getUserByUsername, author, slug, user.id);
|
|
434
|
-
if (!deleted) return { error: "Note not found or not authorized" };
|
|
435
|
-
return { output: "Note deleted." };
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// packages/extensions/notes/src/db.ts
|
|
442
|
-
function initDb2(db) {
|
|
443
|
-
db.exec(`
|
|
444
|
-
CREATE TABLE IF NOT EXISTS notes (
|
|
445
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
446
|
-
author_id INTEGER NOT NULL,
|
|
447
|
-
title TEXT NOT NULL,
|
|
448
|
-
slug TEXT NOT NULL,
|
|
449
|
-
content TEXT NOT NULL,
|
|
450
|
-
reply_to_id INTEGER,
|
|
451
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
452
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
453
|
-
);
|
|
454
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
|
|
455
|
-
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
|
|
456
|
-
CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
|
|
457
|
-
|
|
458
|
-
CREATE TABLE IF NOT EXISTS note_comments (
|
|
459
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
460
|
-
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
461
|
-
author_id INTEGER NOT NULL,
|
|
462
|
-
content TEXT NOT NULL,
|
|
463
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
464
|
-
);
|
|
465
|
-
CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
|
|
466
|
-
|
|
467
|
-
CREATE TABLE IF NOT EXISTS note_reactions (
|
|
468
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
469
|
-
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
470
|
-
user_id INTEGER NOT NULL,
|
|
471
|
-
emoji TEXT NOT NULL,
|
|
472
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
473
|
-
);
|
|
474
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
|
|
475
|
-
CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
|
|
476
|
-
`);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// packages/extensions/notes/src/routes.ts
|
|
480
|
-
import { Hono } from "hono";
|
|
481
|
-
async function parseJson(c) {
|
|
482
|
-
try {
|
|
483
|
-
return await c.req.json();
|
|
484
|
-
} catch {
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function resolveUserId(c) {
|
|
489
|
-
const user = c.get("user");
|
|
490
|
-
if (!user || user.id === 0) return null;
|
|
491
|
-
return { id: user.id, username: user.username };
|
|
492
|
-
}
|
|
493
|
-
function createRoutes(ctx) {
|
|
494
|
-
if (!ctx.db) throw new Error("Notes extension requires a database");
|
|
495
|
-
const db = ctx.db;
|
|
496
|
-
const { getUser: getUser2, getUserByUsername: getUserByUsername2 } = ctx;
|
|
497
|
-
const app = new Hono().get("/", async (c) => {
|
|
498
|
-
const author = c.req.query("author");
|
|
499
|
-
const rawLimit = c.req.query("limit");
|
|
500
|
-
const rawOffset = c.req.query("offset");
|
|
501
|
-
const limit = rawLimit ? parseInt(rawLimit, 10) : void 0;
|
|
502
|
-
const offset = rawOffset ? parseInt(rawOffset, 10) : void 0;
|
|
503
|
-
if (limit !== void 0 && Number.isNaN(limit) || offset !== void 0 && Number.isNaN(offset)) {
|
|
504
|
-
return c.json({ error: "Invalid limit or offset parameter" }, 400);
|
|
505
|
-
}
|
|
506
|
-
const result = await listNotes(db, getUser2, getUserByUsername2, {
|
|
507
|
-
authorUsername: author,
|
|
508
|
-
limit,
|
|
509
|
-
offset
|
|
510
|
-
});
|
|
511
|
-
return c.json(result);
|
|
512
|
-
}).post("/", async (c) => {
|
|
513
|
-
const actor = resolveUserId(c);
|
|
514
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
515
|
-
const body = await parseJson(c);
|
|
516
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
517
|
-
if (!body.title || !body.content) {
|
|
518
|
-
return c.json({ error: "title and content are required" }, 400);
|
|
519
|
-
}
|
|
520
|
-
let replyToId;
|
|
521
|
-
if (body.reply_to) {
|
|
522
|
-
const id = await resolveNoteId(db, getUserByUsername2, body.reply_to);
|
|
523
|
-
if (id === null) return c.json({ error: `Reply target not found: ${body.reply_to}` }, 404);
|
|
524
|
-
replyToId = id;
|
|
525
|
-
}
|
|
526
|
-
const note = await createNote(db, getUser2, actor.id, body.title, body.content, replyToId);
|
|
527
|
-
ctx.publishActivity({
|
|
528
|
-
type: "note_created",
|
|
529
|
-
mind: actor.username,
|
|
530
|
-
summary: `${actor.username} wrote "${body.title}"`,
|
|
531
|
-
metadata: {
|
|
532
|
-
author: actor.username,
|
|
533
|
-
slug: note.slug,
|
|
534
|
-
bodyHtml: body.content.slice(0, 500)
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
return c.json(note, 201);
|
|
538
|
-
}).get("/:author/:slug", async (c) => {
|
|
539
|
-
const { author, slug } = c.req.param();
|
|
540
|
-
const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
|
|
541
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
542
|
-
return c.json(note);
|
|
543
|
-
}).put("/:author/:slug", async (c) => {
|
|
544
|
-
const actor = resolveUserId(c);
|
|
545
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
546
|
-
const { author, slug } = c.req.param();
|
|
547
|
-
if (actor.username !== author) return c.json({ error: "Forbidden" }, 403);
|
|
548
|
-
const body = await parseJson(c);
|
|
549
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
550
|
-
const note = await updateNote(db, getUser2, getUserByUsername2, author, slug, body);
|
|
551
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
552
|
-
return c.json(note);
|
|
553
|
-
}).delete("/:author/:slug", async (c) => {
|
|
554
|
-
const actor = resolveUserId(c);
|
|
555
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
556
|
-
const { author, slug } = c.req.param();
|
|
557
|
-
const deleted = await deleteNote(db, getUserByUsername2, author, slug, actor.id);
|
|
558
|
-
if (!deleted) return c.json({ error: "Note not found or not authorized" }, 404);
|
|
559
|
-
return c.json({ ok: true });
|
|
560
|
-
}).post("/:author/:slug/reactions", async (c) => {
|
|
561
|
-
const actor = resolveUserId(c);
|
|
562
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
563
|
-
const { author, slug } = c.req.param();
|
|
564
|
-
const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
|
|
565
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
566
|
-
const body = await parseJson(c);
|
|
567
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
568
|
-
if (!body.emoji) return c.json({ error: "emoji is required" }, 400);
|
|
569
|
-
const result = toggleReaction(db, note.id, actor.id, body.emoji);
|
|
570
|
-
const reactions = await getReactions(db, getUser2, note.id);
|
|
571
|
-
return c.json({ ...result, reactions });
|
|
572
|
-
}).post("/:author/:slug/comments", async (c) => {
|
|
573
|
-
const actor = resolveUserId(c);
|
|
574
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
575
|
-
const { author, slug } = c.req.param();
|
|
576
|
-
const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
|
|
577
|
-
if (!note) return c.json({ error: "Note not found" }, 404);
|
|
578
|
-
const body = await parseJson(c);
|
|
579
|
-
if (!body) return c.json({ error: "Invalid JSON body" }, 400);
|
|
580
|
-
if (!body.content) return c.json({ error: "content is required" }, 400);
|
|
581
|
-
const comment = await addComment(db, getUser2, note.id, actor.id, body.content);
|
|
582
|
-
return c.json(comment, 201);
|
|
583
|
-
}).delete("/:author/:slug/comments/:id", async (c) => {
|
|
584
|
-
const actor = resolveUserId(c);
|
|
585
|
-
if (!actor) return c.json({ error: "Unauthorized" }, 401);
|
|
586
|
-
const commentId = parseInt(c.req.param("id"), 10);
|
|
587
|
-
if (Number.isNaN(commentId)) return c.json({ error: "Invalid comment ID" }, 400);
|
|
588
|
-
const deleted = await deleteComment(db, commentId, actor.id);
|
|
589
|
-
if (!deleted) return c.json({ error: "Comment not found or not authorized" }, 404);
|
|
590
|
-
return c.json({ ok: true });
|
|
591
|
-
}).get("/feed", async (c) => {
|
|
592
|
-
const rawFeedLimit = c.req.query("limit");
|
|
593
|
-
const limit = rawFeedLimit ? parseInt(rawFeedLimit, 10) : 8;
|
|
594
|
-
if (Number.isNaN(limit)) return c.json({ error: "Invalid limit parameter" }, 400);
|
|
595
|
-
const mind = c.req.query("mind");
|
|
596
|
-
const notes = await listNotes(db, getUser2, getUserByUsername2, {
|
|
597
|
-
limit,
|
|
598
|
-
...mind ? { authorUsername: mind } : {}
|
|
599
|
-
});
|
|
600
|
-
return c.json(
|
|
601
|
-
notes.map((n) => ({
|
|
602
|
-
id: `note-${n.author_username}-${n.slug}`,
|
|
603
|
-
title: n.title,
|
|
604
|
-
url: `/minds/${n.author_username}/notes/${n.slug}`,
|
|
605
|
-
date: n.created_at,
|
|
606
|
-
author: n.author_username,
|
|
607
|
-
bodyHtml: n.content,
|
|
608
|
-
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>',
|
|
609
|
-
color: "yellow"
|
|
610
|
-
}))
|
|
611
|
-
);
|
|
612
|
-
});
|
|
613
|
-
return app;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// packages/extensions/notes/src/index.ts
|
|
617
|
-
var assetsDir = resolve(import.meta.dirname, "../dist/ui");
|
|
618
|
-
var skillsDir = resolve(import.meta.dirname, "../skills");
|
|
619
|
-
var src_default = createExtension({
|
|
620
|
-
id: "notes",
|
|
621
|
-
name: "Notes",
|
|
622
|
-
version: "0.1.0",
|
|
623
|
-
description: "Public notes for sharing thoughts, reflections, and ideas",
|
|
624
|
-
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>',
|
|
625
|
-
color: "yellow",
|
|
626
|
-
routes: (ctx) => createRoutes(ctx),
|
|
627
|
-
commands: createCommands(),
|
|
628
|
-
initDb: initDb2,
|
|
629
|
-
skillsDir,
|
|
630
|
-
standardSkill: true,
|
|
631
|
-
ui: {
|
|
632
|
-
assetsDir,
|
|
633
|
-
systemSection: { id: "notes", label: "Notes", urlPatterns: ["/notes", "/notes/:author/:slug"] },
|
|
634
|
-
mindSections: [{ id: "notes", label: "Notes" }],
|
|
635
|
-
feedSource: {
|
|
636
|
-
endpoint: "/api/ext/notes/feed"
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
// packages/extensions/pages/src/index.ts
|
|
642
|
-
import { resolve as resolve4 } from "path";
|
|
643
|
-
|
|
644
|
-
// packages/extensions/pages/src/commands.ts
|
|
645
|
-
import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
646
|
-
import { relative, resolve as resolve2 } from "path";
|
|
647
|
-
function createCommands2() {
|
|
648
|
-
return {
|
|
649
|
-
publish: {
|
|
650
|
-
description: "Publish all pages (copy to public snapshot)",
|
|
651
|
-
usage: "volute pages publish [--remote]",
|
|
652
|
-
handler: async (args, ctx) => {
|
|
653
|
-
const mindName = ctx.mindName;
|
|
654
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
655
|
-
const remote = args.includes("--remote");
|
|
656
|
-
const mindDir2 = ctx.getMindDir(mindName);
|
|
657
|
-
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
658
|
-
const sourceDir = resolve2(mindDir2, "home", "public", "pages");
|
|
659
|
-
if (!existsSync(sourceDir))
|
|
660
|
-
return { error: "No pages directory found (home/public/pages/)" };
|
|
661
|
-
const db = ctx.db;
|
|
662
|
-
if (!db) return { error: "Database not available" };
|
|
663
|
-
const snapshotDir = resolve2(ctx.dataDir, "sites", mindName);
|
|
664
|
-
try {
|
|
665
|
-
if (existsSync(snapshotDir)) rmSync(snapshotDir, { recursive: true });
|
|
666
|
-
cpSync(sourceDir, snapshotDir, { recursive: true });
|
|
667
|
-
} catch (err) {
|
|
668
|
-
return { error: `Failed to publish snapshot: ${err.message}` };
|
|
669
|
-
}
|
|
670
|
-
const htmlFiles = collectHtmlFiles(snapshotDir, snapshotDir);
|
|
671
|
-
let diff;
|
|
672
|
-
try {
|
|
673
|
-
diff = syncPublishedPages(db, mindName, htmlFiles);
|
|
674
|
-
} catch (err) {
|
|
675
|
-
return { error: `Failed to update page database: ${err.message}` };
|
|
676
|
-
}
|
|
677
|
-
for (const file of diff.added) {
|
|
678
|
-
ctx.publishActivity({
|
|
679
|
-
type: "page_published",
|
|
680
|
-
mind: mindName,
|
|
681
|
-
summary: `${mindName} published ${file}`,
|
|
682
|
-
metadata: { file, iframeUrl: `/ext/pages/public/${mindName}/${file}` }
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
for (const file of diff.removed) {
|
|
686
|
-
ctx.publishActivity({
|
|
687
|
-
type: "page_removed",
|
|
688
|
-
mind: mindName,
|
|
689
|
-
summary: `${mindName} removed ${file}`,
|
|
690
|
-
metadata: { file }
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
let output = `Published ${htmlFiles.length} files`;
|
|
694
|
-
const parts = [];
|
|
695
|
-
if (diff.added.length > 0) parts.push(`${diff.added.length} new`);
|
|
696
|
-
if (diff.updated.length > 0) parts.push(`${diff.updated.length} updated`);
|
|
697
|
-
if (diff.removed.length > 0) parts.push(`${diff.removed.length} removed`);
|
|
698
|
-
if (parts.length > 0) output += ` (${parts.join(", ")})`;
|
|
699
|
-
if (remote) {
|
|
700
|
-
const config = ctx.getSystemsConfig();
|
|
701
|
-
if (!config)
|
|
702
|
-
return {
|
|
703
|
-
error: "Not connected to volute.systems. Run volute systems register or login first."
|
|
704
|
-
};
|
|
705
|
-
const allFiles = collectAllFiles(snapshotDir, snapshotDir);
|
|
706
|
-
const files = {};
|
|
707
|
-
for (const f of allFiles) {
|
|
708
|
-
const fp = resolve2(snapshotDir, f);
|
|
709
|
-
files[f] = readFileSync(fp).toString("base64");
|
|
710
|
-
}
|
|
711
|
-
try {
|
|
712
|
-
const res = await fetch(`${config.apiUrl}/api/pages/publish/${mindName}`, {
|
|
713
|
-
method: "PUT",
|
|
714
|
-
headers: {
|
|
715
|
-
"Content-Type": "application/json",
|
|
716
|
-
Authorization: `Bearer ${config.apiKey}`
|
|
717
|
-
},
|
|
718
|
-
body: JSON.stringify({ files })
|
|
719
|
-
});
|
|
720
|
-
const data = await res.json().catch(() => ({}));
|
|
721
|
-
if (!res.ok) {
|
|
722
|
-
const errMsg = data.error || `HTTP ${res.status}`;
|
|
723
|
-
output += `
|
|
724
|
-
Warning: remote publish failed: ${errMsg}`;
|
|
725
|
-
} else if (data.url) {
|
|
726
|
-
output += `
|
|
727
|
-
Remote: ${data.url}`;
|
|
728
|
-
}
|
|
729
|
-
} catch (err) {
|
|
730
|
-
output += `
|
|
731
|
-
Warning: remote publish failed: ${err.message}`;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return { output };
|
|
735
|
-
}
|
|
736
|
-
},
|
|
737
|
-
list: {
|
|
738
|
-
description: "List pages with publish status",
|
|
739
|
-
usage: "volute pages list [--all]",
|
|
740
|
-
handler: async (args, ctx) => {
|
|
741
|
-
const mindName = ctx.mindName;
|
|
742
|
-
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
743
|
-
const db = ctx.db;
|
|
744
|
-
if (!db) return { error: "Database not available" };
|
|
745
|
-
const allFlag = args.includes("--all");
|
|
746
|
-
const port = process.env.VOLUTE_DAEMON_PORT || "1618";
|
|
747
|
-
if (allFlag) {
|
|
748
|
-
const { getAllSites: getAllSites2 } = await import("./db-RA45JBFG.js");
|
|
749
|
-
const sites = getAllSites2(db);
|
|
750
|
-
const lines2 = [];
|
|
751
|
-
for (const site of sites) {
|
|
752
|
-
for (const f of site.files) {
|
|
753
|
-
const url = `http://localhost:${port}/ext/pages/public/${site.mind}/${f.file}`;
|
|
754
|
-
lines2.push(`${site.mind.padEnd(15)} ${f.file.padEnd(25)} ${url}`);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
return { output: lines2.length > 0 ? lines2.join("\n") : "No published pages found." };
|
|
758
|
-
}
|
|
759
|
-
const mindDir2 = ctx.getMindDir(mindName);
|
|
760
|
-
if (!mindDir2) return { error: `Mind not found: ${mindName}` };
|
|
761
|
-
const sourceDir = resolve2(mindDir2, "home", "public", "pages");
|
|
762
|
-
const published = new Set(getPublishedPages(db, mindName).map((p) => p.file));
|
|
763
|
-
const draftFiles = existsSync(sourceDir) ? collectHtmlFiles(sourceDir, sourceDir) : [];
|
|
764
|
-
const allFiles = /* @__PURE__ */ new Set([...published, ...draftFiles]);
|
|
765
|
-
if (allFiles.size === 0) return { output: "No pages found." };
|
|
766
|
-
const lines = [...allFiles].sort().map((file) => {
|
|
767
|
-
const isPublished = published.has(file);
|
|
768
|
-
const status = isPublished ? "published" : "draft";
|
|
769
|
-
const url = isPublished ? `http://localhost:${port}/ext/pages/public/${mindName}/${file}` : "";
|
|
770
|
-
return `${status.padEnd(11)} ${file.padEnd(25)} ${url}`;
|
|
771
|
-
});
|
|
772
|
-
return { output: lines.join("\n") };
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
function collectHtmlFiles(dir, baseDir) {
|
|
778
|
-
const files = [];
|
|
779
|
-
let items;
|
|
780
|
-
try {
|
|
781
|
-
items = readdirSync(dir);
|
|
782
|
-
} catch (err) {
|
|
783
|
-
console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
|
|
784
|
-
return files;
|
|
785
|
-
}
|
|
786
|
-
for (const item of items) {
|
|
787
|
-
if (item.startsWith(".")) continue;
|
|
788
|
-
const fullPath = resolve2(dir, item);
|
|
789
|
-
try {
|
|
790
|
-
const s = statSync(fullPath);
|
|
791
|
-
if (s.isFile() && item.endsWith(".html")) {
|
|
792
|
-
files.push(relative(baseDir, fullPath));
|
|
793
|
-
} else if (s.isDirectory()) {
|
|
794
|
-
files.push(...collectHtmlFiles(fullPath, baseDir));
|
|
795
|
-
}
|
|
796
|
-
} catch (err) {
|
|
797
|
-
console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
return files.sort();
|
|
801
|
-
}
|
|
802
|
-
function collectAllFiles(dir, baseDir) {
|
|
803
|
-
const files = [];
|
|
804
|
-
let items;
|
|
805
|
-
try {
|
|
806
|
-
items = readdirSync(dir);
|
|
807
|
-
} catch (err) {
|
|
808
|
-
console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
|
|
809
|
-
return files;
|
|
810
|
-
}
|
|
811
|
-
for (const item of items) {
|
|
812
|
-
if (item.startsWith(".")) continue;
|
|
813
|
-
const fullPath = resolve2(dir, item);
|
|
814
|
-
try {
|
|
815
|
-
const s = statSync(fullPath);
|
|
816
|
-
if (s.isFile()) {
|
|
817
|
-
files.push(relative(baseDir, fullPath));
|
|
818
|
-
} else if (s.isDirectory()) {
|
|
819
|
-
files.push(...collectAllFiles(fullPath, baseDir));
|
|
820
|
-
}
|
|
821
|
-
} catch (err) {
|
|
822
|
-
console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
return files.sort();
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// packages/extensions/pages/src/routes.ts
|
|
829
|
-
import { readFile, stat } from "fs/promises";
|
|
830
|
-
import { extname, resolve as resolve3 } from "path";
|
|
831
|
-
import { Hono as Hono2 } from "hono";
|
|
832
|
-
|
|
833
|
-
// packages/extensions/pages/src/cache.ts
|
|
834
|
-
function getSites(db) {
|
|
835
|
-
const sites = getAllSites(db);
|
|
836
|
-
return sites.map((site) => ({
|
|
837
|
-
name: site.mind,
|
|
838
|
-
label: site.mind,
|
|
839
|
-
pages: site.files.map((f) => ({
|
|
840
|
-
file: f.file,
|
|
841
|
-
modified: f.updated_at,
|
|
842
|
-
url: `/ext/pages/public/${site.mind}/${f.file}`
|
|
843
|
-
}))
|
|
844
|
-
}));
|
|
845
|
-
}
|
|
846
|
-
function getRecentPagesList(db, opts) {
|
|
847
|
-
const rows = getRecentPages(db, opts);
|
|
848
|
-
return rows.map((r) => ({
|
|
849
|
-
mind: r.mind,
|
|
850
|
-
file: r.file,
|
|
851
|
-
modified: r.updated_at,
|
|
852
|
-
url: `/ext/pages/public/${r.mind}/${r.file}`
|
|
853
|
-
}));
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// packages/extensions/pages/src/routes.ts
|
|
857
|
-
var MIME_TYPES = {
|
|
858
|
-
".html": "text/html",
|
|
859
|
-
".js": "application/javascript",
|
|
860
|
-
".css": "text/css",
|
|
861
|
-
".json": "application/json",
|
|
862
|
-
".svg": "image/svg+xml",
|
|
863
|
-
".png": "image/png",
|
|
864
|
-
".jpg": "image/jpeg",
|
|
865
|
-
".jpeg": "image/jpeg",
|
|
866
|
-
".gif": "image/gif",
|
|
867
|
-
".ico": "image/x-icon",
|
|
868
|
-
".woff": "font/woff",
|
|
869
|
-
".woff2": "font/woff2",
|
|
870
|
-
".txt": "text/plain",
|
|
871
|
-
".xml": "application/xml"
|
|
872
|
-
};
|
|
873
|
-
function createRoutes2(ctx) {
|
|
874
|
-
return new Hono2().get("/", async (c) => {
|
|
875
|
-
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
876
|
-
const sites = getSites(ctx.db);
|
|
877
|
-
const recentPages = getRecentPagesList(ctx.db);
|
|
878
|
-
return c.json({ sites, recentPages });
|
|
879
|
-
}).get("/feed", async (c) => {
|
|
880
|
-
if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
|
|
881
|
-
const mind = c.req.query("mind");
|
|
882
|
-
const rawLimit = c.req.query("limit");
|
|
883
|
-
const limit = rawLimit ? parseInt(rawLimit, 10) || 8 : 8;
|
|
884
|
-
const recentPages = getRecentPagesList(ctx.db, { mind: mind || void 0, limit });
|
|
885
|
-
return c.json(
|
|
886
|
-
recentPages.map((p) => ({
|
|
887
|
-
id: `page-${p.mind}-${p.file}`,
|
|
888
|
-
title: `${p.mind}/${p.file}`,
|
|
889
|
-
url: p.url ?? `/minds/${p.mind}/pages/${p.file}`,
|
|
890
|
-
date: p.modified,
|
|
891
|
-
author: p.mind,
|
|
892
|
-
bodyHtml: `<p>Page updated</p>`,
|
|
893
|
-
iframeUrl: `/ext/pages/public/${p.mind}/${p.file}`,
|
|
894
|
-
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>',
|
|
895
|
-
color: "purple"
|
|
896
|
-
}))
|
|
897
|
-
);
|
|
898
|
-
}).put("/publish/:name", async (c) => {
|
|
899
|
-
const user = ctx.resolveUser(c);
|
|
900
|
-
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
901
|
-
const name = c.req.param("name");
|
|
902
|
-
if (user.role !== "admin" && user.username !== name) {
|
|
903
|
-
return c.json({ error: "Forbidden" }, 403);
|
|
904
|
-
}
|
|
905
|
-
const config = ctx.getSystemsConfig();
|
|
906
|
-
if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
|
|
907
|
-
const body = await c.req.text();
|
|
908
|
-
try {
|
|
909
|
-
const res = await fetch(`${config.apiUrl}/api/pages/publish/${name}`, {
|
|
910
|
-
method: "PUT",
|
|
911
|
-
headers: {
|
|
912
|
-
"Content-Type": "application/json",
|
|
913
|
-
Authorization: `Bearer ${config.apiKey}`
|
|
914
|
-
},
|
|
915
|
-
body
|
|
916
|
-
});
|
|
917
|
-
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
918
|
-
return c.json(data, res.status);
|
|
919
|
-
} catch (err) {
|
|
920
|
-
return c.json({ error: `Connection failed: ${err.message}` }, 502);
|
|
921
|
-
}
|
|
922
|
-
}).get("/status/:name", async (c) => {
|
|
923
|
-
const user = ctx.resolveUser(c);
|
|
924
|
-
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
925
|
-
const name = c.req.param("name");
|
|
926
|
-
if (user.role !== "admin" && user.username !== name) {
|
|
927
|
-
return c.json({ error: "Forbidden" }, 403);
|
|
928
|
-
}
|
|
929
|
-
const config = ctx.getSystemsConfig();
|
|
930
|
-
if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
|
|
931
|
-
try {
|
|
932
|
-
const res = await fetch(`${config.apiUrl}/api/pages/status/${name}`, {
|
|
933
|
-
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
934
|
-
});
|
|
935
|
-
const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
936
|
-
return c.json(data, res.status);
|
|
937
|
-
} catch (err) {
|
|
938
|
-
return c.json({ error: `Connection failed: ${err.message}` }, 502);
|
|
939
|
-
}
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
var _voluteHome = null;
|
|
943
|
-
async function getVoluteHome() {
|
|
944
|
-
if (_voluteHome) return _voluteHome();
|
|
945
|
-
const mod = await import("./registry-PJ4S5PHQ.js");
|
|
946
|
-
_voluteHome = mod.voluteHome;
|
|
947
|
-
return _voluteHome();
|
|
948
|
-
}
|
|
949
|
-
function createPublicRoutes(ctx) {
|
|
950
|
-
return new Hono2().get("/:name/*", async (c) => {
|
|
951
|
-
const name = c.req.param("name");
|
|
952
|
-
if (name.includes("/") || name.includes("\\") || name === "." || name === "..")
|
|
953
|
-
return c.text("Not found", 404);
|
|
954
|
-
let pagesRoot;
|
|
955
|
-
if (name === "_system") {
|
|
956
|
-
const home = await getVoluteHome();
|
|
957
|
-
pagesRoot = resolve3(home, "shared", "pages");
|
|
958
|
-
} else {
|
|
959
|
-
pagesRoot = resolve3(ctx.dataDir, "sites", name);
|
|
960
|
-
}
|
|
961
|
-
const prefix = `/public/${name}`;
|
|
962
|
-
const idx = c.req.path.indexOf(prefix);
|
|
963
|
-
const wildcard = idx >= 0 ? c.req.path.slice(idx + prefix.length) : "/";
|
|
964
|
-
const requestedPath = resolve3(pagesRoot, wildcard.slice(1));
|
|
965
|
-
if (requestedPath !== pagesRoot && !requestedPath.startsWith(`${pagesRoot}/`))
|
|
966
|
-
return c.text("Forbidden", 403);
|
|
967
|
-
let fileToServe = requestedPath;
|
|
968
|
-
let fileStat = await stat(requestedPath).catch(() => null);
|
|
969
|
-
if (fileStat?.isDirectory()) {
|
|
970
|
-
const indexPath = resolve3(requestedPath, "index.html");
|
|
971
|
-
fileStat = await stat(indexPath).catch(() => null);
|
|
972
|
-
if (fileStat?.isFile()) {
|
|
973
|
-
fileToServe = indexPath;
|
|
974
|
-
} else {
|
|
975
|
-
return c.text("Not found", 404);
|
|
976
|
-
}
|
|
977
|
-
} else if (!fileStat?.isFile()) {
|
|
978
|
-
return c.text("Not found", 404);
|
|
979
|
-
}
|
|
980
|
-
const ext = extname(fileToServe);
|
|
981
|
-
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
982
|
-
try {
|
|
983
|
-
const body = await readFile(fileToServe);
|
|
984
|
-
return c.body(body, 200, { "Content-Type": mime });
|
|
985
|
-
} catch (err) {
|
|
986
|
-
const code = err.code;
|
|
987
|
-
if (code === "EACCES") return c.text("Forbidden", 403);
|
|
988
|
-
if (code === "ENOENT") return c.text("Not found", 404);
|
|
989
|
-
return c.text("Internal server error", 500);
|
|
990
|
-
}
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// packages/extensions/pages/src/index.ts
|
|
995
|
-
var assetsDir2 = resolve4(import.meta.dirname, "../dist/ui");
|
|
996
|
-
var skillsDir2 = resolve4(import.meta.dirname, "../skills");
|
|
997
|
-
var src_default2 = createExtension({
|
|
998
|
-
id: "pages",
|
|
999
|
-
name: "Pages",
|
|
1000
|
-
version: "0.1.0",
|
|
1001
|
-
description: "Publish and serve web pages from mind directories",
|
|
1002
|
-
initDb,
|
|
1003
|
-
routes: (ctx) => createRoutes2(ctx),
|
|
1004
|
-
publicRoutes: (ctx) => createPublicRoutes(ctx),
|
|
1005
|
-
commands: createCommands2(),
|
|
1006
|
-
skillsDir: skillsDir2,
|
|
1007
|
-
standardSkill: true,
|
|
1008
|
-
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>',
|
|
1009
|
-
color: "purple",
|
|
1010
|
-
ui: {
|
|
1011
|
-
assetsDir: assetsDir2,
|
|
1012
|
-
systemSection: {
|
|
1013
|
-
id: "pages",
|
|
1014
|
-
label: "Pages",
|
|
1015
|
-
urlPatterns: ["/pages", "/pages/:site", "/pages/:site/:path"]
|
|
1016
|
-
},
|
|
1017
|
-
mindSections: [{ id: "pages", label: "Pages" }],
|
|
1018
|
-
feedSource: {
|
|
1019
|
-
endpoint: "/api/ext/pages/feed"
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
// src/lib/systems-config.ts
|
|
1025
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
1026
|
-
import { resolve as resolve5 } from "path";
|
|
1027
|
-
var DEFAULT_API_URL = "https://volute.systems";
|
|
1028
|
-
function configPath() {
|
|
1029
|
-
return resolve5(voluteSystemDir(), "systems.json");
|
|
1030
|
-
}
|
|
1031
|
-
function readSystemsConfig() {
|
|
1032
|
-
const path = configPath();
|
|
1033
|
-
if (!existsSync2(path)) return null;
|
|
1034
|
-
const raw = readFileSync2(path, "utf-8");
|
|
1035
|
-
let data;
|
|
1036
|
-
try {
|
|
1037
|
-
data = JSON.parse(raw);
|
|
1038
|
-
} catch {
|
|
1039
|
-
console.error(
|
|
1040
|
-
`Warning: ${path} contains invalid JSON. Run "volute systems logout" and re-login.`
|
|
1041
|
-
);
|
|
1042
|
-
return null;
|
|
1043
|
-
}
|
|
1044
|
-
if (!data.apiKey || !data.system) return null;
|
|
1045
|
-
return {
|
|
1046
|
-
apiKey: data.apiKey,
|
|
1047
|
-
system: data.system,
|
|
1048
|
-
apiUrl: data.apiUrl || DEFAULT_API_URL
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
function writeSystemsConfig(config) {
|
|
1052
|
-
mkdirSync(voluteSystemDir(), { recursive: true });
|
|
1053
|
-
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}
|
|
1054
|
-
`, { mode: 384 });
|
|
1055
|
-
}
|
|
1056
|
-
function deleteSystemsConfig() {
|
|
1057
|
-
try {
|
|
1058
|
-
unlinkSync(configPath());
|
|
1059
|
-
return true;
|
|
1060
|
-
} catch (err) {
|
|
1061
|
-
if (err.code === "ENOENT") return false;
|
|
1062
|
-
throw err;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// src/lib/extensions.ts
|
|
1067
|
-
var VALID_EXTENSION_ID2 = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1068
|
-
var loaded = [];
|
|
1069
|
-
function extensionsBaseDir() {
|
|
1070
|
-
return resolve6(voluteHome(), "extensions");
|
|
1071
|
-
}
|
|
1072
|
-
function extensionDataDir(id) {
|
|
1073
|
-
return resolve6(voluteSystemDir(), "extension-data", id);
|
|
1074
|
-
}
|
|
1075
|
-
function extensionsConfigPath() {
|
|
1076
|
-
return resolve6(voluteHome(), "system", "extensions.json");
|
|
1077
|
-
}
|
|
1078
|
-
function readExtensionsConfig() {
|
|
1079
|
-
const configPath2 = extensionsConfigPath();
|
|
1080
|
-
if (!existsSync3(configPath2)) return [];
|
|
1081
|
-
try {
|
|
1082
|
-
const data = JSON.parse(readFileSync3(configPath2, "utf-8"));
|
|
1083
|
-
return Array.isArray(data) ? data : [];
|
|
1084
|
-
} catch (err) {
|
|
1085
|
-
logger_default.warn("failed to read extensions config, ignoring installed extensions", {
|
|
1086
|
-
path: configPath2,
|
|
1087
|
-
error: err.message
|
|
1088
|
-
});
|
|
1089
|
-
return [];
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
var _LibsqlDatabase = null;
|
|
1093
|
-
async function getLibsqlDatabase() {
|
|
1094
|
-
if (_LibsqlDatabase) return _LibsqlDatabase;
|
|
1095
|
-
const mod = await import("libsql");
|
|
1096
|
-
_LibsqlDatabase = mod.default ?? mod;
|
|
1097
|
-
return _LibsqlDatabase;
|
|
1098
|
-
}
|
|
1099
|
-
async function openExtensionDb(_id, dataDir) {
|
|
1100
|
-
const dbPath = resolve6(dataDir, "data.db");
|
|
1101
|
-
const Database = await getLibsqlDatabase();
|
|
1102
|
-
return new Database(dbPath);
|
|
1103
|
-
}
|
|
1104
|
-
async function buildContext(manifest, dataDir, authMw) {
|
|
1105
|
-
let db = null;
|
|
1106
|
-
if (manifest.initDb) {
|
|
1107
|
-
const realDb = await openExtensionDb(manifest.id, dataDir);
|
|
1108
|
-
try {
|
|
1109
|
-
manifest.initDb(realDb);
|
|
1110
|
-
} catch (err) {
|
|
1111
|
-
realDb.close();
|
|
1112
|
-
throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
|
|
1113
|
-
}
|
|
1114
|
-
db = realDb;
|
|
1115
|
-
}
|
|
1116
|
-
return {
|
|
1117
|
-
db,
|
|
1118
|
-
authMiddleware: authMw,
|
|
1119
|
-
resolveUser: (c) => {
|
|
1120
|
-
const user = c.get("user");
|
|
1121
|
-
if (!user || typeof user !== "object") return null;
|
|
1122
|
-
return user;
|
|
1123
|
-
},
|
|
1124
|
-
getUser: async (id) => getUser(id),
|
|
1125
|
-
getUserByUsername: async (username) => getUserByUsername(username),
|
|
1126
|
-
publishActivity: (event) => {
|
|
1127
|
-
const enriched = {
|
|
1128
|
-
...event,
|
|
1129
|
-
metadata: {
|
|
1130
|
-
...event.metadata,
|
|
1131
|
-
...manifest.icon && !event.metadata?.icon ? { icon: manifest.icon } : {},
|
|
1132
|
-
...manifest.color && !event.metadata?.color ? { color: manifest.color } : {}
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
publish(enriched).catch(
|
|
1136
|
-
(err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
|
|
1137
|
-
);
|
|
1138
|
-
},
|
|
1139
|
-
getMindDir: (name) => {
|
|
1140
|
-
try {
|
|
1141
|
-
const dir = mindDir(name);
|
|
1142
|
-
return existsSync3(dir) ? dir : null;
|
|
1143
|
-
} catch (err) {
|
|
1144
|
-
logger_default.warn(
|
|
1145
|
-
`extension ${manifest.id}: failed to resolve mind dir for ${name}`,
|
|
1146
|
-
logger_default.errorData(err)
|
|
1147
|
-
);
|
|
1148
|
-
return null;
|
|
1149
|
-
}
|
|
1150
|
-
},
|
|
1151
|
-
getSystemsConfig: () => readSystemsConfig(),
|
|
1152
|
-
dataDir
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
async function loadExtension(manifest, app, authMw) {
|
|
1156
|
-
if (!VALID_EXTENSION_ID2.test(manifest.id)) {
|
|
1157
|
-
logger_default.error(`invalid extension ID "${manifest.id}", skipping (must match ${VALID_EXTENSION_ID2})`);
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
const dataDir = extensionDataDir(manifest.id);
|
|
1161
|
-
mkdirSync2(dataDir, { recursive: true });
|
|
1162
|
-
const context = await buildContext(manifest, dataDir, authMw);
|
|
1163
|
-
const routesApp = manifest.routes(context);
|
|
1164
|
-
const extApiPath = `/api/ext/${manifest.id}`;
|
|
1165
|
-
app.use(extApiPath, authMw);
|
|
1166
|
-
app.use(`${extApiPath}/*`, authMw);
|
|
1167
|
-
app.route(extApiPath, routesApp);
|
|
1168
|
-
if (manifest.publicRoutes) {
|
|
1169
|
-
const publicApp = manifest.publicRoutes(context);
|
|
1170
|
-
app.route(`/ext/${manifest.id}/public`, publicApp);
|
|
1171
|
-
}
|
|
1172
|
-
if (manifest.commands) {
|
|
1173
|
-
for (const [cmdName, cmd] of Object.entries(manifest.commands)) {
|
|
1174
|
-
app.post(`${extApiPath}/commands/${cmdName}`, async (c) => {
|
|
1175
|
-
let body;
|
|
1176
|
-
try {
|
|
1177
|
-
body = await c.req.json();
|
|
1178
|
-
} catch {
|
|
1179
|
-
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
1180
|
-
}
|
|
1181
|
-
const user = c.get("user");
|
|
1182
|
-
const mindName = body.mind || user?.username;
|
|
1183
|
-
const session = c.get("mindSession");
|
|
1184
|
-
try {
|
|
1185
|
-
const activityPromises = [];
|
|
1186
|
-
const result = await cmd.handler(body.args ?? [], {
|
|
1187
|
-
...context,
|
|
1188
|
-
publishActivity: (rawEvent) => {
|
|
1189
|
-
const event = {
|
|
1190
|
-
...rawEvent,
|
|
1191
|
-
metadata: {
|
|
1192
|
-
...rawEvent.metadata,
|
|
1193
|
-
...manifest.icon && !rawEvent.metadata?.icon ? { icon: manifest.icon } : {},
|
|
1194
|
-
...manifest.color && !rawEvent.metadata?.color ? { color: manifest.color } : {}
|
|
1195
|
-
}
|
|
1196
|
-
};
|
|
1197
|
-
activityPromises.push(
|
|
1198
|
-
publish(event).catch((err) => {
|
|
1199
|
-
logger_default.error(
|
|
1200
|
-
`extension ${manifest.id}: failed to publish activity`,
|
|
1201
|
-
logger_default.errorData(err)
|
|
1202
|
-
);
|
|
1203
|
-
return 0;
|
|
1204
|
-
})
|
|
1205
|
-
);
|
|
1206
|
-
},
|
|
1207
|
-
mindName,
|
|
1208
|
-
session,
|
|
1209
|
-
stdin: body.stdin
|
|
1210
|
-
});
|
|
1211
|
-
const activityIds = (await Promise.all(activityPromises)).filter((id) => id > 0);
|
|
1212
|
-
const markers = activityIds.map((id) => `[volute:activity:${id}]`).join("");
|
|
1213
|
-
const output = result && typeof result === "object" && "output" in result ? { ...result, output: `${result.output}${markers}` } : markers ? { ...result, output: markers } : result;
|
|
1214
|
-
return c.json(output);
|
|
1215
|
-
} catch (err) {
|
|
1216
|
-
logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
|
|
1217
|
-
return c.json({ error: err.message }, 500);
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
|
|
1223
|
-
if (resolvedAssetsDir && !existsSync3(resolvedAssetsDir)) {
|
|
1224
|
-
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
1225
|
-
for (let i = 0; i < 5; i++) {
|
|
1226
|
-
const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
|
|
1227
|
-
if (existsSync3(candidate)) {
|
|
1228
|
-
resolvedAssetsDir = candidate;
|
|
1229
|
-
break;
|
|
1230
|
-
}
|
|
1231
|
-
searchDir = dirname(searchDir);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
if (resolvedAssetsDir && existsSync3(resolvedAssetsDir)) {
|
|
1235
|
-
const assetsDir3 = resolvedAssetsDir;
|
|
1236
|
-
const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
|
|
1237
|
-
const { extname: ext } = await import("path");
|
|
1238
|
-
const mimeTypes = {
|
|
1239
|
-
".html": "text/html",
|
|
1240
|
-
".js": "application/javascript",
|
|
1241
|
-
".css": "text/css",
|
|
1242
|
-
".json": "application/json",
|
|
1243
|
-
".svg": "image/svg+xml",
|
|
1244
|
-
".png": "image/png",
|
|
1245
|
-
".jpg": "image/jpeg",
|
|
1246
|
-
".ico": "image/x-icon",
|
|
1247
|
-
".woff": "font/woff",
|
|
1248
|
-
".woff2": "font/woff2"
|
|
1249
|
-
};
|
|
1250
|
-
const prefix = `/ext/${manifest.id}`;
|
|
1251
|
-
const indexPath = resolve6(assetsDir3, "index.html");
|
|
1252
|
-
const serveExtAssets = async (c) => {
|
|
1253
|
-
const urlPath = new URL(c.req.url).pathname;
|
|
1254
|
-
const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
|
|
1255
|
-
const filePath = resolve6(assetsDir3, relativePath);
|
|
1256
|
-
if (filePath !== assetsDir3 && !filePath.startsWith(assetsDir3 + "/"))
|
|
1257
|
-
return c.text("Forbidden", 403);
|
|
1258
|
-
const s = await fsStat(filePath).catch(() => null);
|
|
1259
|
-
if (s?.isFile()) {
|
|
1260
|
-
const mime = mimeTypes[ext(filePath)] || "application/octet-stream";
|
|
1261
|
-
const body = await readFile2(filePath);
|
|
1262
|
-
return c.body(body, 200, { "Content-Type": mime });
|
|
1263
|
-
}
|
|
1264
|
-
if (existsSync3(indexPath)) {
|
|
1265
|
-
const body = await readFile2(indexPath, "utf-8");
|
|
1266
|
-
return c.html(body);
|
|
1267
|
-
}
|
|
1268
|
-
return c.text("Not found", 404);
|
|
1269
|
-
};
|
|
1270
|
-
app.get(`${prefix}/*`, serveExtAssets);
|
|
1271
|
-
app.get(prefix, serveExtAssets);
|
|
1272
|
-
}
|
|
1273
|
-
const skillsDir3 = resolveSkillsDir(manifest);
|
|
1274
|
-
if (skillsDir3) {
|
|
1275
|
-
let entries;
|
|
1276
|
-
try {
|
|
1277
|
-
entries = readdirSync2(skillsDir3, { withFileTypes: true });
|
|
1278
|
-
} catch (err) {
|
|
1279
|
-
logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
|
|
1280
|
-
entries = [];
|
|
1281
|
-
}
|
|
1282
|
-
for (const entry of entries) {
|
|
1283
|
-
if (!entry.isDirectory()) continue;
|
|
1284
|
-
try {
|
|
1285
|
-
const skillPath = resolve6(skillsDir3, entry.name);
|
|
1286
|
-
const sourceHash = hashSkillDir(skillPath);
|
|
1287
|
-
const destDir = resolve6(sharedSkillsDir(), entry.name);
|
|
1288
|
-
if (existsSync3(destDir)) {
|
|
1289
|
-
const destHash = hashSkillDir(destDir);
|
|
1290
|
-
if (sourceHash === destHash) continue;
|
|
1291
|
-
}
|
|
1292
|
-
await importSkillFromDir(skillPath, `ext:${manifest.id}`);
|
|
1293
|
-
logger_default.info(`synced skill "${entry.name}" for extension: ${manifest.id}`);
|
|
1294
|
-
} catch (err) {
|
|
1295
|
-
logger_default.error(
|
|
1296
|
-
`failed to sync skill "${entry.name}" for extension ${manifest.id}`,
|
|
1297
|
-
logger_default.errorData(err)
|
|
1298
|
-
);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
if (manifest.standardSkill && !manifest.skillsDir) {
|
|
1303
|
-
logger_default.warn(`extension ${manifest.id}: standardSkill is true but no skillsDir declared`);
|
|
1304
|
-
}
|
|
1305
|
-
loaded.push({ manifest, context });
|
|
1306
|
-
logger_default.info(`loaded extension: ${manifest.id} v${manifest.version}`);
|
|
1307
|
-
}
|
|
1308
|
-
function resolveSkillsDir(manifest) {
|
|
1309
|
-
if (!manifest.skillsDir) return null;
|
|
1310
|
-
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
1311
|
-
for (let i = 0; i < 5; i++) {
|
|
1312
|
-
const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "skills");
|
|
1313
|
-
if (existsSync3(candidate)) return candidate;
|
|
1314
|
-
searchDir = dirname(searchDir);
|
|
1315
|
-
}
|
|
1316
|
-
if (existsSync3(manifest.skillsDir)) return manifest.skillsDir;
|
|
1317
|
-
logger_default.warn(`skills dir not found for extension ${manifest.id}: ${manifest.skillsDir}`);
|
|
1318
|
-
return null;
|
|
1319
|
-
}
|
|
1320
|
-
function discoverBuiltinExtensions() {
|
|
1321
|
-
return [src_default, src_default2];
|
|
1322
|
-
}
|
|
1323
|
-
async function discoverInstalledExtensions() {
|
|
1324
|
-
const manifests = [];
|
|
1325
|
-
const packages = readExtensionsConfig();
|
|
1326
|
-
const npmDir = resolve6(voluteHome(), "extensions", "_npm");
|
|
1327
|
-
const { createRequire } = await import("module");
|
|
1328
|
-
for (const pkg of packages) {
|
|
1329
|
-
try {
|
|
1330
|
-
let resolved = pkg;
|
|
1331
|
-
const npmPkgDir = resolve6(npmDir, "node_modules", pkg);
|
|
1332
|
-
if (existsSync3(npmPkgDir)) {
|
|
1333
|
-
const require2 = createRequire(resolve6(npmDir, "noop.js"));
|
|
1334
|
-
resolved = require2.resolve(pkg);
|
|
1335
|
-
}
|
|
1336
|
-
const mod = await import(resolved);
|
|
1337
|
-
const manifest = mod.default ?? mod.extension ?? mod;
|
|
1338
|
-
if (!validateManifest(manifest, `package ${pkg}`)) continue;
|
|
1339
|
-
manifests.push(manifest);
|
|
1340
|
-
} catch (err) {
|
|
1341
|
-
logger_default.error(`failed to load extension package: ${pkg}`, logger_default.errorData(err));
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
return manifests;
|
|
1345
|
-
}
|
|
1346
|
-
function validateManifest(manifest, source) {
|
|
1347
|
-
if (!manifest || typeof manifest !== "object") {
|
|
1348
|
-
logger_default.warn(`extension from ${source} does not export a valid manifest`);
|
|
1349
|
-
return false;
|
|
1350
|
-
}
|
|
1351
|
-
const m = manifest;
|
|
1352
|
-
if (!m.id || typeof m.id !== "string") {
|
|
1353
|
-
logger_default.warn(`extension from ${source} is missing a valid id`);
|
|
1354
|
-
return false;
|
|
1355
|
-
}
|
|
1356
|
-
if (!VALID_EXTENSION_ID2.test(m.id)) {
|
|
1357
|
-
logger_default.warn(`extension from ${source} has invalid id "${m.id}"`);
|
|
1358
|
-
return false;
|
|
1359
|
-
}
|
|
1360
|
-
if (typeof m.routes !== "function") {
|
|
1361
|
-
logger_default.warn(`extension from ${source} is missing a routes function`);
|
|
1362
|
-
return false;
|
|
1363
|
-
}
|
|
1364
|
-
if (!m.name || typeof m.name !== "string") {
|
|
1365
|
-
logger_default.warn(`extension "${m.id}" from ${source} is missing a name`);
|
|
1366
|
-
return false;
|
|
1367
|
-
}
|
|
1368
|
-
if (!m.version || typeof m.version !== "string") {
|
|
1369
|
-
logger_default.warn(`extension "${m.id}" from ${source} is missing a version`);
|
|
1370
|
-
return false;
|
|
1371
|
-
}
|
|
1372
|
-
return true;
|
|
1373
|
-
}
|
|
1374
|
-
async function discoverLocalExtensions() {
|
|
1375
|
-
const baseDir = extensionsBaseDir();
|
|
1376
|
-
if (!existsSync3(baseDir)) return [];
|
|
1377
|
-
const manifests = [];
|
|
1378
|
-
let entries;
|
|
1379
|
-
try {
|
|
1380
|
-
entries = readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
|
|
1381
|
-
} catch (err) {
|
|
1382
|
-
logger_default.error("failed to read local extensions directory", logger_default.errorData(err));
|
|
1383
|
-
return [];
|
|
1384
|
-
}
|
|
1385
|
-
for (const dir of entries) {
|
|
1386
|
-
const extDir = resolve6(baseDir, dir);
|
|
1387
|
-
const candidates = [resolve6(extDir, "src", "index.js"), resolve6(extDir, "index.js")];
|
|
1388
|
-
const entryPoint = candidates.find((p) => existsSync3(p));
|
|
1389
|
-
if (!entryPoint) continue;
|
|
1390
|
-
try {
|
|
1391
|
-
const mod = await import(entryPoint);
|
|
1392
|
-
const manifest = mod.default ?? mod.extension ?? mod;
|
|
1393
|
-
if (!validateManifest(manifest, `local dir ${extDir}`)) continue;
|
|
1394
|
-
manifests.push(manifest);
|
|
1395
|
-
logger_default.info(`discovered local extension: ${manifest.id} from ${extDir}`);
|
|
1396
|
-
} catch (err) {
|
|
1397
|
-
logger_default.error(`failed to load local extension from ${extDir}`, logger_default.errorData(err));
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
return manifests;
|
|
1401
|
-
}
|
|
1402
|
-
async function loadAllExtensions(app, authMw) {
|
|
1403
|
-
const builtins = discoverBuiltinExtensions();
|
|
1404
|
-
const installed = await discoverInstalledExtensions();
|
|
1405
|
-
const local = await discoverLocalExtensions();
|
|
1406
|
-
const all = [...builtins, ...installed, ...local];
|
|
1407
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1408
|
-
for (const manifest of all) {
|
|
1409
|
-
if (seen.has(manifest.id)) {
|
|
1410
|
-
logger_default.warn(`duplicate extension ID: ${manifest.id}, skipping`);
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
seen.add(manifest.id);
|
|
1414
|
-
try {
|
|
1415
|
-
await loadExtension(manifest, app, authMw);
|
|
1416
|
-
} catch (err) {
|
|
1417
|
-
logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
app.get("/api/extensions/commands", (c) => {
|
|
1421
|
-
const result = {};
|
|
1422
|
-
for (const { manifest } of loaded) {
|
|
1423
|
-
if (!manifest.commands) continue;
|
|
1424
|
-
const cmds = {};
|
|
1425
|
-
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1426
|
-
cmds[name] = { description: cmd.description, ...cmd.usage ? { usage: cmd.usage } : {} };
|
|
1427
|
-
}
|
|
1428
|
-
result[manifest.id] = { commands: cmds };
|
|
1429
|
-
}
|
|
1430
|
-
return c.json(result);
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
function getLoadedExtensions() {
|
|
1434
|
-
return loaded.map(({ manifest }) => {
|
|
1435
|
-
let commands;
|
|
1436
|
-
if (manifest.commands) {
|
|
1437
|
-
commands = {};
|
|
1438
|
-
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1439
|
-
commands[name] = {
|
|
1440
|
-
description: cmd.description,
|
|
1441
|
-
...cmd.usage ? { usage: cmd.usage } : {}
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
return {
|
|
1446
|
-
id: manifest.id,
|
|
1447
|
-
name: manifest.name,
|
|
1448
|
-
version: manifest.version,
|
|
1449
|
-
description: manifest.description,
|
|
1450
|
-
icon: manifest.icon,
|
|
1451
|
-
systemSection: manifest.ui?.systemSection,
|
|
1452
|
-
mindSections: manifest.ui?.mindSections,
|
|
1453
|
-
feedSource: manifest.ui?.feedSource,
|
|
1454
|
-
commands
|
|
1455
|
-
};
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
function getExtensionStandardSkills() {
|
|
1459
|
-
const skills = [];
|
|
1460
|
-
for (const { manifest } of loaded) {
|
|
1461
|
-
if (!manifest.standardSkill) continue;
|
|
1462
|
-
const dir = resolveSkillsDir(manifest);
|
|
1463
|
-
if (!dir) continue;
|
|
1464
|
-
try {
|
|
1465
|
-
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1466
|
-
if (entry.isDirectory()) skills.push(entry.name);
|
|
1467
|
-
}
|
|
1468
|
-
} catch (err) {
|
|
1469
|
-
logger_default.warn(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
return skills;
|
|
1473
|
-
}
|
|
1474
|
-
function notifyExtensionsDaemonStart() {
|
|
1475
|
-
for (const { manifest } of loaded) {
|
|
1476
|
-
try {
|
|
1477
|
-
manifest.onDaemonStart?.();
|
|
1478
|
-
} catch (err) {
|
|
1479
|
-
logger_default.error(`extension ${manifest.id}: onDaemonStart failed`, logger_default.errorData(err));
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
function notifyExtensionsDaemonStop() {
|
|
1484
|
-
for (const { manifest, context } of loaded) {
|
|
1485
|
-
try {
|
|
1486
|
-
manifest.onDaemonStop?.();
|
|
1487
|
-
} catch (err) {
|
|
1488
|
-
logger_default.error(`extension ${manifest.id}: onDaemonStop failed`, logger_default.errorData(err));
|
|
1489
|
-
}
|
|
1490
|
-
try {
|
|
1491
|
-
context.db?.close();
|
|
1492
|
-
} catch (err) {
|
|
1493
|
-
logger_default.warn(`extension ${manifest.id}: failed to close db`, logger_default.errorData(err));
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
loaded.length = 0;
|
|
1497
|
-
}
|
|
1498
|
-
function notifyExtensionsMindStart(mindName) {
|
|
1499
|
-
for (const { manifest } of loaded) {
|
|
1500
|
-
try {
|
|
1501
|
-
manifest.onMindStart?.(mindName);
|
|
1502
|
-
} catch (err) {
|
|
1503
|
-
logger_default.error(`extension ${manifest.id}: onMindStart failed for ${mindName}`, logger_default.errorData(err));
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
function notifyExtensionsMindStop(mindName) {
|
|
1508
|
-
for (const { manifest } of loaded) {
|
|
1509
|
-
try {
|
|
1510
|
-
manifest.onMindStop?.(mindName);
|
|
1511
|
-
} catch (err) {
|
|
1512
|
-
logger_default.error(`extension ${manifest.id}: onMindStop failed for ${mindName}`, logger_default.errorData(err));
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
export {
|
|
1518
|
-
readSystemsConfig,
|
|
1519
|
-
writeSystemsConfig,
|
|
1520
|
-
deleteSystemsConfig,
|
|
1521
|
-
loadAllExtensions,
|
|
1522
|
-
getLoadedExtensions,
|
|
1523
|
-
getExtensionStandardSkills,
|
|
1524
|
-
notifyExtensionsDaemonStart,
|
|
1525
|
-
notifyExtensionsDaemonStop,
|
|
1526
|
-
notifyExtensionsMindStart,
|
|
1527
|
-
notifyExtensionsMindStop
|
|
1528
|
-
};
|