pi-roundtable-web 0.27.0 → 0.28.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/CHANGELOG.md +37 -0
- package/README.md +23 -7
- package/dist/chunk-account-678dgbww.js +1 -0
- package/dist/chunk-action-menu-qpp9bdxh.js +1 -0
- package/dist/chunk-agent-x0yvvdq2.js +1 -0
- package/dist/chunk-arranged-rooms-6rdkk8t1.js +1 -0
- package/dist/chunk-bulk-actions-zbwdaxtc.js +1 -0
- package/dist/chunk-character-creator-215psy17.js +1 -0
- package/dist/chunk-codeeditor-jgj32247.js +2 -0
- package/dist/chunk-confirm-2pzrt272.js +1 -0
- package/dist/chunk-connectors-zha2d834.js +37 -0
- package/dist/chunk-conversations-fb071rez.js +1 -0
- package/dist/chunk-council-sheet-k5fycefd.js +2 -0
- package/dist/chunk-drag-04qfef58.js +1 -0
- package/dist/chunk-entry-mws704mc.js +1 -0
- package/dist/chunk-folder-list-qxhmczwv.js +1 -0
- package/dist/chunk-folds-b7asegfs.js +1 -0
- package/dist/chunk-gate-vxetvn8t.js +1 -0
- package/dist/chunk-group-scene-63yhh1mh.js +1 -0
- package/dist/chunk-group-table-zy4hmgk3.js +1 -0
- package/dist/chunk-host-icons-tyktazzz.js +1 -0
- package/dist/chunk-host-picture-ebhejn2k.js +1 -0
- package/dist/chunk-host-state-wajep0yr.js +1 -0
- package/dist/chunk-host-ui-pc9r24e3.js +1 -0
- package/dist/chunk-index-1af8vsyb.js +2 -0
- package/dist/{chunk-index-g1awv7rk.js → chunk-index-20xgx0k2.js} +2 -2
- package/dist/chunk-index-254701sm.js +3 -0
- package/dist/{chunk-index-ffbrm6tr.js → chunk-index-2822z600.js} +3 -3
- package/dist/{chunk-index-2fdns8tn.js → chunk-index-2hvn2kte.js} +2 -2
- package/dist/chunk-index-2syyyjnq.js +2 -0
- package/dist/chunk-index-2tr41qht.js +2 -0
- package/dist/chunk-index-2y8r4s4f.js +2 -0
- package/dist/chunk-index-31m1b2f5.js +2 -0
- package/dist/{chunk-index-ak6ecqy3.js → chunk-index-3gfbhmpb.js} +2 -2
- package/dist/chunk-index-3yy10a5h.js +2 -0
- package/dist/{chunk-index-sm7m4fej.js → chunk-index-4t6vnwap.js} +2 -2
- package/dist/{chunk-index-j4x9dq30.css → chunk-index-5hbq6g2e.css} +1 -1
- package/dist/chunk-index-5t2jkecy.js +2 -0
- package/dist/chunk-index-5vatmakn.js +2 -0
- package/dist/{chunk-index-zyvwhxga.js → chunk-index-6cg8xp1c.js} +1 -1
- package/dist/chunk-index-6yra9kdq.js +2 -0
- package/dist/{chunk-index-ac1ktr56.js → chunk-index-7qd604rq.js} +2 -2
- package/dist/chunk-index-9bdstf8t.js +2 -0
- package/dist/chunk-index-acj4xxgq.js +100 -0
- package/dist/chunk-index-aexy023z.js +2 -0
- package/dist/chunk-index-amxa2bn3.js +2 -0
- package/dist/chunk-index-bef0fc2m.js +2 -0
- package/dist/chunk-index-c6tphrjr.js +1 -0
- package/dist/chunk-index-c7dbvkrq.js +2 -0
- package/dist/chunk-index-cn49mawx.js +50 -0
- package/dist/{chunk-index-3d8s3565.js → chunk-index-e2gy763y.js} +2 -2
- package/dist/{chunk-index-31x6krbs.js → chunk-index-e35v554x.js} +3 -3
- package/dist/chunk-index-fepsbhr6.js +47 -0
- package/dist/chunk-index-gcmmwh8t.js +2 -0
- package/dist/{chunk-index-kbrg32sx.js → chunk-index-gwkhs8s3.js} +2 -2
- package/dist/chunk-index-h2vhv4kh.js +2 -0
- package/dist/chunk-index-h9ccz1r3.js +2 -0
- package/dist/{chunk-index-0w4bfjb5.js → chunk-index-jc3r4qam.js} +2 -2
- package/dist/{chunk-index-8d81qy69.js → chunk-index-m81362sc.js} +2 -2
- package/dist/{chunk-index-xq84gg8w.js → chunk-index-n0cmnwd2.js} +2 -2
- package/dist/chunk-index-n0ejy0wd.js +2 -0
- package/dist/chunk-index-nkdg1dfz.js +7 -0
- package/dist/chunk-index-p7jkr7gv.js +14 -0
- package/dist/{chunk-index-e2dqjde8.js → chunk-index-q5fyrtx4.js} +1 -1
- package/dist/{chunk-index-jgzygbt8.js → chunk-index-qxynr1hf.js} +2 -2
- package/dist/chunk-index-r7fdgzy1.js +2 -0
- package/dist/chunk-index-rgf4rvns.js +4 -0
- package/dist/chunk-index-rsd8rftk.js +2 -0
- package/dist/{chunk-index-rh0x9rzf.js → chunk-index-s53bxwkk.js} +2 -2
- package/dist/{chunk-index-xxeb2xp0.js → chunk-index-s9p27f9b.js} +3 -3
- package/dist/chunk-index-sdcpf5dp.js +2 -0
- package/dist/chunk-index-sm7kfb07.js +2 -0
- package/dist/chunk-index-sw9r4aa4.js +2 -0
- package/dist/chunk-index-t5bmsqqd.js +1 -0
- package/dist/chunk-index-v8mma5np.js +2 -0
- package/dist/{chunk-index-q7g62sgm.js → chunk-index-v9gbt5tz.js} +3 -3
- package/dist/chunk-index-vsfsv8hd.js +2 -0
- package/dist/chunk-index-wxft0aqb.js +2 -0
- package/dist/chunk-index-x60n1me6.js +2 -0
- package/dist/chunk-index-zcfr8jdv.js +13 -0
- package/dist/{chunk-index-w42rcbx1.js → chunk-index-zj0gjcvr.js} +2 -2
- package/dist/{chunk-index-2t30xwed.js → chunk-index-zt19fdmc.js} +2 -2
- package/dist/chunk-index-zvvwxkev.js +2 -0
- package/dist/chunk-lang-css-7q1dtpgm.js +1 -0
- package/dist/{chunk-lang-dockerfile-cnwqn660.js → chunk-lang-dockerfile-n4bt8jbt.js} +1 -1
- package/dist/{chunk-lang-go-yy6kgsxv.js → chunk-lang-go-9k4y107e.js} +3 -3
- package/dist/chunk-lang-html-xgvba8rn.js +1 -0
- package/dist/chunk-lang-javascript-vgxmgd0a.js +1 -0
- package/dist/chunk-lang-json-hr12bzs1.js +1 -0
- package/dist/chunk-lang-markdown-ttzcpgkg.js +6 -0
- package/dist/chunk-lang-python-ntj8maem.js +3 -0
- package/dist/chunk-lang-rust-gv8y859d.js +1 -0
- package/dist/{chunk-lang-shell-1rpgtepw.js → chunk-lang-shell-5zzs09b7.js} +1 -1
- package/dist/chunk-lang-sql-6vmze4pg.js +2 -0
- package/dist/{chunk-lang-toml-tbj7467c.js → chunk-lang-toml-tt0d4rw9.js} +1 -1
- package/dist/chunk-lang-typescript-2v652sh5.js +1 -0
- package/dist/chunk-lang-xml-r2sbqnjv.js +1 -0
- package/dist/chunk-lang-yaml-ex31qbph.js +1 -0
- package/dist/chunk-layout-actions-bfqtg71d.js +1 -0
- package/dist/chunk-menu-items-rztgekfj.js +1 -0
- package/dist/chunk-mode-toggle-m5kgb7c7.js +1 -0
- package/dist/chunk-note-row-40ypghn9.js +204 -0
- package/dist/chunk-notes-a8nqffbf.js +1 -0
- package/dist/chunk-notice-bell-rdnfmtf2.js +1 -0
- package/dist/{chunk-notices-fte9f196.js → chunk-notices-tefx93s6.js} +1 -1
- package/dist/chunk-onboarding-wvh0s3vg.js +1 -0
- package/dist/chunk-overview-zrx3bpwf.js +1 -0
- package/dist/chunk-palette-5xtdvy9a.js +1 -0
- package/dist/chunk-placement-items-rgyrktvy.js +1 -0
- package/dist/chunk-profile-tyb48b79.js +1 -0
- package/dist/chunk-prompt-history-wv0vdsjv.js +1 -0
- package/dist/chunk-prompts-75hgb0t0.js +1 -0
- package/dist/chunk-room-confirms-bcvhpcry.js +1 -0
- package/dist/chunk-room-menu-ha1evv71.js +1 -0
- package/dist/chunk-rooms-menu-button-ej3a2hyc.js +1 -0
- package/dist/chunk-schedules-a5pewkhd.js +1 -0
- package/dist/{chunk-seat-menu-r0k0eawr.js → chunk-seat-menu-eh3ymmks.js} +1 -1
- package/dist/chunk-sheet-hosts-d5zbwbbr.js +29 -0
- package/dist/{chunk-shortcut-help-a6pkjcq7.js → chunk-shortcut-help-bh9ntk9s.js} +2 -2
- package/dist/{chunk-shortcuts-cgt87jkz.js → chunk-shortcuts-6em0atk9.js} +1 -1
- package/dist/chunk-skill-editor-e43mwz01.js +8 -0
- package/dist/chunk-skills-vp107wrp.js +1 -0
- package/dist/chunk-start-over-line-xd1v2p0g.js +16 -0
- package/dist/chunk-switcher-list-dtfh87zy.js +1 -0
- package/dist/{chunk-tab-dot-vr11dfrw.js → chunk-tab-dot-qd640mjr.js} +1 -1
- package/dist/chunk-table-svb2xhfd.js +1 -0
- package/dist/chunk-table2d-5nrhfwft.js +1 -0
- package/dist/chunk-table3d-5cb66d99.js +4220 -0
- package/dist/chunk-thread-actions-926eab0w.js +1 -0
- package/dist/{chunk-thread-cards-c2mrp2gj.js → chunk-thread-cards-kgt22sbp.js} +1 -1
- package/dist/chunk-thread-list-j2v1pqkz.js +217 -0
- package/dist/chunk-transcript-a0p3vwh6.js +1 -0
- package/dist/chunk-waiting-cdyz7r1g.js +1 -0
- package/dist/chunk-waiting-chip-58ttx9av.js +28 -0
- package/dist/chunk-where-9jp6g029.js +1 -0
- package/dist/chunk-workspace-d34njy4p.js +578 -0
- package/dist/chunk-zh-tw-y3jb20wc.js +1 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +3 -3
- package/src/api-types.ts +98 -1
- package/src/app/app.ts +4 -7
- package/src/app/desk.ts +19 -4
- package/src/app/room-log.ts +66 -18
- package/src/app/rooms.ts +15 -5
- package/src/app/routes.ts +60 -9
- package/src/app/skill-doc-routes.ts +12 -2
- package/src/app/surface.ts +26 -2
- package/src/app/team-routes.ts +3 -7
- package/src/authz.ts +1 -0
- package/src/cleanup-api.ts +9 -2
- package/src/connector-manage-api.ts +257 -0
- package/src/console-api.ts +60 -2
- package/src/features.ts +91 -0
- package/src/host/reads.ts +10 -1
- package/src/i18n/zh-tw.ts +107 -4
- package/src/web-plugin.ts +7 -1
- package/dist/chunk-account-1ecx5kt7.js +0 -1
- package/dist/chunk-action-menu-zh2n0ahf.js +0 -1
- package/dist/chunk-agent-ynm2tcbx.js +0 -1
- package/dist/chunk-arranged-rooms-qnz8vese.js +0 -1
- package/dist/chunk-bulk-actions-pr0mw0kc.js +0 -1
- package/dist/chunk-character-creator-caj4pmg4.js +0 -1
- package/dist/chunk-codeeditor-dga6s78p.js +0 -2
- package/dist/chunk-confirm-g76v5d9r.js +0 -1
- package/dist/chunk-connectors-j79ze3j1.js +0 -1
- package/dist/chunk-conversations-2farn72g.js +0 -1
- package/dist/chunk-council-sheet-s9sz1bzf.js +0 -2
- package/dist/chunk-drag-45e8222r.js +0 -1
- package/dist/chunk-entry-q11n47h9.js +0 -1
- package/dist/chunk-folder-list-4gcj7g7h.js +0 -1
- package/dist/chunk-folds-ten4ykfw.js +0 -1
- package/dist/chunk-gate-kcv1a4t0.js +0 -1
- package/dist/chunk-group-scene-12ghcvzd.js +0 -1
- package/dist/chunk-group-table-t458kdgn.js +0 -1
- package/dist/chunk-host-icons-h1k9k96a.js +0 -1
- package/dist/chunk-host-picture-3p0wzn3v.js +0 -1
- package/dist/chunk-host-state-7mdve7cb.js +0 -1
- package/dist/chunk-host-ui-bdgwa6sf.js +0 -1
- package/dist/chunk-index-02vfshtt.js +0 -2
- package/dist/chunk-index-0a81da8z.js +0 -2
- package/dist/chunk-index-12zyshae.js +0 -2
- package/dist/chunk-index-18pgxxw3.js +0 -2
- package/dist/chunk-index-2e4am3hw.js +0 -50
- package/dist/chunk-index-5bfqek1d.js +0 -2
- package/dist/chunk-index-64rsz6pf.js +0 -2
- package/dist/chunk-index-6bkcq9ka.js +0 -2
- package/dist/chunk-index-92zgb7my.js +0 -2
- package/dist/chunk-index-aamhjg5n.js +0 -2
- package/dist/chunk-index-b73y2gc0.js +0 -47
- package/dist/chunk-index-b8cpvr7y.js +0 -2
- package/dist/chunk-index-bhqczxr3.js +0 -2
- package/dist/chunk-index-cbqhzf40.js +0 -13
- package/dist/chunk-index-cdctnw71.js +0 -2
- package/dist/chunk-index-dj0tbfyb.js +0 -2
- package/dist/chunk-index-dnjftvtq.js +0 -2
- package/dist/chunk-index-ebg48jgy.js +0 -2
- package/dist/chunk-index-ek5mek4j.js +0 -14
- package/dist/chunk-index-f6q0stqx.js +0 -2
- package/dist/chunk-index-g3wrs02w.js +0 -1
- package/dist/chunk-index-j0m16cj1.js +0 -2
- package/dist/chunk-index-j4kk1m59.js +0 -2
- package/dist/chunk-index-kb3w11mn.js +0 -7
- package/dist/chunk-index-msqhgvfx.js +0 -2
- package/dist/chunk-index-pefwfm26.js +0 -2
- package/dist/chunk-index-pyfx77cj.js +0 -2
- package/dist/chunk-index-qfgkfq86.js +0 -2
- package/dist/chunk-index-sv9qpmhv.js +0 -1
- package/dist/chunk-index-t998yg8y.js +0 -2
- package/dist/chunk-index-tk4se135.js +0 -4
- package/dist/chunk-index-v6smgw77.js +0 -2
- package/dist/chunk-index-x4enmj72.js +0 -2
- package/dist/chunk-lang-css-y9133ktr.js +0 -1
- package/dist/chunk-lang-html-r2xmryek.js +0 -1
- package/dist/chunk-lang-javascript-1r6wd177.js +0 -1
- package/dist/chunk-lang-json-dx56g11y.js +0 -1
- package/dist/chunk-lang-markdown-fpcf2dfq.js +0 -6
- package/dist/chunk-lang-python-zzp95955.js +0 -3
- package/dist/chunk-lang-rust-214fh7sp.js +0 -1
- package/dist/chunk-lang-sql-vvp46hpj.js +0 -2
- package/dist/chunk-lang-typescript-y5w4tt4g.js +0 -1
- package/dist/chunk-lang-xml-tbcqmedk.js +0 -1
- package/dist/chunk-lang-yaml-jh56t3sv.js +0 -1
- package/dist/chunk-layout-actions-y73mpsh1.js +0 -1
- package/dist/chunk-menu-items-bza16bw6.js +0 -2
- package/dist/chunk-mode-toggle-nj9gnt3b.js +0 -1
- package/dist/chunk-note-row-6fmhrdng.js +0 -75
- package/dist/chunk-notes-wv0e6ssf.js +0 -1
- package/dist/chunk-notice-bell-9z27epd2.js +0 -1
- package/dist/chunk-onboarding-c5twahc3.js +0 -1
- package/dist/chunk-overview-k4ccprax.js +0 -1
- package/dist/chunk-palette-9rbp2fab.js +0 -1
- package/dist/chunk-placement-items-me598wsb.js +0 -1
- package/dist/chunk-profile-s7r7fcvz.js +0 -1
- package/dist/chunk-prompt-history-hf296e7v.js +0 -1
- package/dist/chunk-prompts-c8s0nhkm.js +0 -1
- package/dist/chunk-room-confirms-ga2sbvhc.js +0 -1
- package/dist/chunk-room-menu-7h4jym2b.js +0 -1
- package/dist/chunk-rooms-menu-button-pgtaanb6.js +0 -1
- package/dist/chunk-schedules-mbnwcxpc.js +0 -1
- package/dist/chunk-sheet-hosts-a5dfevy9.js +0 -1
- package/dist/chunk-skill-editor-67yk4x7t.js +0 -8
- package/dist/chunk-skills-rewemey4.js +0 -1
- package/dist/chunk-switcher-list-t8rbay2y.js +0 -1
- package/dist/chunk-table-52nh0azk.js +0 -1
- package/dist/chunk-table2d-g57vr6h0.js +0 -1
- package/dist/chunk-table3d-rp0w6kt4.js +0 -4220
- package/dist/chunk-thread-actions-qcxt1y1g.js +0 -1
- package/dist/chunk-thread-list-0071q455.js +0 -217
- package/dist/chunk-transcript-789yqysq.js +0 -1
- package/dist/chunk-where-hmwmme9e.js +0 -1
- package/dist/chunk-workspace-wr1qrtsh.js +0 -578
- package/dist/chunk-zh-tw-z5p5mpph.js +0 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { Logger } from "pi-roundtable";
|
|
2
|
+
import type {
|
|
3
|
+
ConnectorAddedView,
|
|
4
|
+
ManagedConnectorView,
|
|
5
|
+
McpAuthorizeInput,
|
|
6
|
+
McpChannelView,
|
|
7
|
+
McpGrantsView,
|
|
8
|
+
McpUrlView,
|
|
9
|
+
} from "./api-types.ts";
|
|
10
|
+
import type {
|
|
11
|
+
ConnectorManagement,
|
|
12
|
+
ConsoleActor,
|
|
13
|
+
ManagedConnector,
|
|
14
|
+
McpGrants,
|
|
15
|
+
} from "./features.ts";
|
|
16
|
+
import { ConsoleHttpError as HttpError, json } from "./http.ts";
|
|
17
|
+
|
|
18
|
+
/** What the routes below `connectors/` need besides the ports. */
|
|
19
|
+
export interface ManageContext {
|
|
20
|
+
logger: Logger;
|
|
21
|
+
actor: ConsoleActor;
|
|
22
|
+
/** Reads the request's JSON body, or undefined when it is not JSON. */
|
|
23
|
+
body(): Promise<unknown>;
|
|
24
|
+
/** Tells the open pages that something changed. */
|
|
25
|
+
changed(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CONNECTORS_DOWN =
|
|
29
|
+
"The connector service did not respond. Try again later.";
|
|
30
|
+
const GRANTS_DOWN =
|
|
31
|
+
"The MCP authorization service did not respond. Try again later.";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Scheme and host only: some servers take their credential in the path, query, or user part.
|
|
35
|
+
* Undefined for a URL that cannot be parsed.
|
|
36
|
+
*/
|
|
37
|
+
export function displayUrl(url: string): string | undefined {
|
|
38
|
+
const parsed = URL.parse(url);
|
|
39
|
+
if (!parsed) return undefined;
|
|
40
|
+
const hidden =
|
|
41
|
+
parsed.pathname !== "/" ||
|
|
42
|
+
parsed.search ||
|
|
43
|
+
parsed.hash ||
|
|
44
|
+
parsed.username ||
|
|
45
|
+
parsed.password;
|
|
46
|
+
return `${parsed.origin}${hidden ? "/…" : ""}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Only the fields the page shows, so nothing else the port's object holds can reach a response. */
|
|
50
|
+
function connectorView(connector: ManagedConnector): ManagedConnectorView {
|
|
51
|
+
const shown = displayUrl(connector.url);
|
|
52
|
+
return {
|
|
53
|
+
name: connector.name,
|
|
54
|
+
...(shown ? { displayUrl: shown } : {}),
|
|
55
|
+
description: connector.description,
|
|
56
|
+
...(connector.server ? { tools: connector.server.tools.length } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A port's refusal reaches the owner as its message, already in the host's wording; anything
|
|
62
|
+
* else is logged without the request and answered as the service being down.
|
|
63
|
+
*/
|
|
64
|
+
async function guarded<T>(
|
|
65
|
+
context: ManageContext,
|
|
66
|
+
service: "connectors" | "grants",
|
|
67
|
+
call: () => Promise<T> | T,
|
|
68
|
+
): Promise<T> {
|
|
69
|
+
try {
|
|
70
|
+
return await call();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof HttpError) throw error;
|
|
73
|
+
if (
|
|
74
|
+
error instanceof Error &&
|
|
75
|
+
(error as { refusal?: unknown }).refusal === true
|
|
76
|
+
)
|
|
77
|
+
throw new HttpError(400, error.message);
|
|
78
|
+
context.logger.error({ err: error }, "connector management failed");
|
|
79
|
+
if (service === "grants") throw new HttpError(502, GRANTS_DOWN);
|
|
80
|
+
throw new HttpError(502, CONNECTORS_DOWN);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const optional = (value: unknown, max: number): string | undefined => {
|
|
85
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
86
|
+
if (typeof value !== "string" || value.length > max)
|
|
87
|
+
throw new HttpError(400, "The body is not valid.");
|
|
88
|
+
return value;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const required = (value: unknown, max: number): string => {
|
|
92
|
+
if (typeof value !== "string" || value.length > max)
|
|
93
|
+
throw new HttpError(400, "The body is not valid.");
|
|
94
|
+
return value;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
async function objectBody(
|
|
98
|
+
context: ManageContext,
|
|
99
|
+
): Promise<Record<string, unknown>> {
|
|
100
|
+
const body = await context.body();
|
|
101
|
+
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
102
|
+
throw new HttpError(400, "The body is not valid.");
|
|
103
|
+
return body as Record<string, unknown>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `connectors/managed[/<name>]`: list, add, change the purpose of, and remove connectors. */
|
|
107
|
+
export async function connectorRoutes(
|
|
108
|
+
manage: ConnectorManagement,
|
|
109
|
+
method: string,
|
|
110
|
+
name: string | undefined,
|
|
111
|
+
context: ManageContext,
|
|
112
|
+
): Promise<Response> {
|
|
113
|
+
if (!name && method === "GET") {
|
|
114
|
+
const list = await guarded(context, "connectors", () => manage.list());
|
|
115
|
+
return json(list.map((c) => connectorView(c)));
|
|
116
|
+
}
|
|
117
|
+
if (!name && method === "POST") {
|
|
118
|
+
const body = await objectBody(context);
|
|
119
|
+
const header = optional(body.header, 100);
|
|
120
|
+
const token = optional(body.token, 4000);
|
|
121
|
+
const input = {
|
|
122
|
+
name: required(body.name, 100),
|
|
123
|
+
url: required(body.url, 2000),
|
|
124
|
+
description: required(body.description, 1000),
|
|
125
|
+
...(header ? { header } : {}),
|
|
126
|
+
...(token ? { token } : {}),
|
|
127
|
+
};
|
|
128
|
+
const added = await guarded(context, "connectors", () => manage.add(input));
|
|
129
|
+
context.changed();
|
|
130
|
+
return json(
|
|
131
|
+
{
|
|
132
|
+
connector: connectorView(added.connector),
|
|
133
|
+
skipped: [...added.skipped],
|
|
134
|
+
} satisfies ConnectorAddedView,
|
|
135
|
+
201,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (name && method === "PATCH") {
|
|
139
|
+
const description = required((await objectBody(context)).description, 1000);
|
|
140
|
+
await guarded(context, "connectors", () =>
|
|
141
|
+
manage.describe(name, description),
|
|
142
|
+
);
|
|
143
|
+
context.changed();
|
|
144
|
+
return json({ name });
|
|
145
|
+
}
|
|
146
|
+
if (name && method === "DELETE") {
|
|
147
|
+
await guarded(context, "connectors", () => manage.remove(name));
|
|
148
|
+
context.changed();
|
|
149
|
+
return json({ removed: name });
|
|
150
|
+
}
|
|
151
|
+
throw new HttpError(405, "This method is not supported.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `connectors/grants…`: the bundles with their channels (`GET`), the channels to pick (`GET
|
|
156
|
+
* channels`), authorize (`POST`), describe (`PATCH <bundle>/<channel>`), revoke (`DELETE
|
|
157
|
+
* <bundle>/<channel>`) and rotate (`POST <bundle>/rotate`).
|
|
158
|
+
*/
|
|
159
|
+
export async function grantRoutes(
|
|
160
|
+
grants: McpGrants,
|
|
161
|
+
method: string,
|
|
162
|
+
rest: readonly string[],
|
|
163
|
+
context: ManageContext,
|
|
164
|
+
): Promise<Response> {
|
|
165
|
+
const [first, second] = rest;
|
|
166
|
+
if (rest.length === 0 && method === "GET") {
|
|
167
|
+
const bundles = await guarded(context, "grants", () => grants.bundles());
|
|
168
|
+
return json({
|
|
169
|
+
operations: grants.operations.map((o) => ({ id: o.id, label: o.label })),
|
|
170
|
+
bundles: bundles.map((bundle) => ({
|
|
171
|
+
name: bundle.name,
|
|
172
|
+
grants: bundle.grants.map((g) => ({
|
|
173
|
+
channelId: g.channelId,
|
|
174
|
+
guildName: g.guildName,
|
|
175
|
+
channelName: g.channelName,
|
|
176
|
+
visible: g.visible,
|
|
177
|
+
displayName: g.displayName,
|
|
178
|
+
description: g.description,
|
|
179
|
+
operations: [...g.operations],
|
|
180
|
+
authorizedAt: g.authorizedAt.toISOString(),
|
|
181
|
+
audit: g.audit.map((a) => ({
|
|
182
|
+
tool: a.tool,
|
|
183
|
+
status: a.status,
|
|
184
|
+
at: a.createdAt.toISOString(),
|
|
185
|
+
})),
|
|
186
|
+
})),
|
|
187
|
+
})),
|
|
188
|
+
} satisfies McpGrantsView);
|
|
189
|
+
}
|
|
190
|
+
if (rest.length === 1 && first === "channels" && method === "GET") {
|
|
191
|
+
const channels = await guarded(context, "grants", () =>
|
|
192
|
+
grants.channels(context.actor),
|
|
193
|
+
);
|
|
194
|
+
return json(
|
|
195
|
+
channels.map(
|
|
196
|
+
(c): McpChannelView => ({ id: c.id, name: c.name, guild: c.guild }),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (rest.length === 0 && method === "POST") {
|
|
201
|
+
const body = await objectBody(context);
|
|
202
|
+
if (
|
|
203
|
+
!Array.isArray(body.operations) ||
|
|
204
|
+
body.operations.some((op) => typeof op !== "string")
|
|
205
|
+
)
|
|
206
|
+
throw new HttpError(400, "The body is not valid.");
|
|
207
|
+
const displayName = optional(body.displayName, 100);
|
|
208
|
+
const description = optional(body.description, 1000);
|
|
209
|
+
const input: McpAuthorizeInput = {
|
|
210
|
+
bundle: required(body.bundle, 200),
|
|
211
|
+
channelId: required(body.channelId, 40),
|
|
212
|
+
operations: body.operations as string[],
|
|
213
|
+
...(displayName ? { displayName } : {}),
|
|
214
|
+
...(description ? { description } : {}),
|
|
215
|
+
};
|
|
216
|
+
const done = await guarded(context, "grants", () =>
|
|
217
|
+
grants.authorize(input, context.actor),
|
|
218
|
+
);
|
|
219
|
+
context.changed();
|
|
220
|
+
return json(
|
|
221
|
+
{
|
|
222
|
+
bundle: done.bundle,
|
|
223
|
+
...(done.url ? { url: done.url } : {}),
|
|
224
|
+
} satisfies McpUrlView,
|
|
225
|
+
201,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (rest.length === 2 && first && second === "rotate" && method === "POST") {
|
|
229
|
+
const { url } = await guarded(context, "grants", () =>
|
|
230
|
+
grants.rotate(first),
|
|
231
|
+
);
|
|
232
|
+
context.changed();
|
|
233
|
+
return json({ bundle: first, url } satisfies McpUrlView);
|
|
234
|
+
}
|
|
235
|
+
if (rest.length === 2 && first && second && method === "PATCH") {
|
|
236
|
+
const body = await objectBody(context);
|
|
237
|
+
const displayName = optional(body.displayName, 100);
|
|
238
|
+
const description =
|
|
239
|
+
body.description === undefined
|
|
240
|
+
? undefined
|
|
241
|
+
: required(body.description, 1000);
|
|
242
|
+
await guarded(context, "grants", () =>
|
|
243
|
+
grants.describe(first, second, {
|
|
244
|
+
...(displayName === undefined ? {} : { displayName }),
|
|
245
|
+
...(description === undefined ? {} : { description }),
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
context.changed();
|
|
249
|
+
return json({ bundle: first, channelId: second });
|
|
250
|
+
}
|
|
251
|
+
if (rest.length === 2 && first && second && method === "DELETE") {
|
|
252
|
+
await guarded(context, "grants", () => grants.revoke(first, second));
|
|
253
|
+
context.changed();
|
|
254
|
+
return json({ bundle: first, channelId: second });
|
|
255
|
+
}
|
|
256
|
+
throw new HttpError(405, "This method is not supported.");
|
|
257
|
+
}
|
package/src/console-api.ts
CHANGED
|
@@ -11,7 +11,13 @@ import type {
|
|
|
11
11
|
SpeakerMemory,
|
|
12
12
|
Tier,
|
|
13
13
|
} from "pi-roundtable";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
agentRoomKey,
|
|
16
|
+
groupRoomKey,
|
|
17
|
+
MemoryError,
|
|
18
|
+
parseChannelKey,
|
|
19
|
+
SYSTEM_PRINCIPAL,
|
|
20
|
+
} from "pi-roundtable";
|
|
15
21
|
import { thinkingLabel } from "pi-roundtable/kit";
|
|
16
22
|
import type {
|
|
17
23
|
ApiError,
|
|
@@ -85,6 +91,11 @@ export interface ConsolePorts {
|
|
|
85
91
|
sessionsDir: string;
|
|
86
92
|
/** Present when the overview pane is served, or the agent server is there to classify channels. */
|
|
87
93
|
team?: Pick<AgentTeam, "status" | "owns" | "guildId">;
|
|
94
|
+
/**
|
|
95
|
+
* The web app's surface, when it hosts the agents' and groups' rooms: the overview then names
|
|
96
|
+
* each one's room, which its start-over resets instead of the Discord channel's conversation.
|
|
97
|
+
*/
|
|
98
|
+
roomSurface?: string;
|
|
88
99
|
queue: Pick<QueuePort, "size">;
|
|
89
100
|
/** Undefined when Discord no longer knows the channel; throws when it cannot be asked. */
|
|
90
101
|
channelName?: (channelId: string) => Promise<ChannelName | undefined>;
|
|
@@ -114,6 +125,7 @@ export interface ConsolePorts {
|
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
import { cleanupConversation } from "./cleanup-api.ts";
|
|
128
|
+
import { connectorRoutes, grantRoutes } from "./connector-manage-api.ts";
|
|
117
129
|
import { connectorsView } from "./connectors-api.ts";
|
|
118
130
|
import { ConsoleHttpError as HttpError, json } from "./http.ts";
|
|
119
131
|
import { readLimited } from "./request-body.ts";
|
|
@@ -256,6 +268,12 @@ export class ConsoleApi {
|
|
|
256
268
|
if (!connectors) throw new HttpError(404, "This pane is not served.");
|
|
257
269
|
return json(await connectorsView(connectors));
|
|
258
270
|
}
|
|
271
|
+
if (
|
|
272
|
+
head === "connectors" &&
|
|
273
|
+
(id === "managed" || id === "grants") &&
|
|
274
|
+
parts.length >= 2
|
|
275
|
+
)
|
|
276
|
+
return this.#manage(request, parts.slice(1), visitor);
|
|
259
277
|
if (head === "notes" && parts.length <= 2) {
|
|
260
278
|
this.#pane("notes", visitor);
|
|
261
279
|
return this.#notes(request, id, visitor);
|
|
@@ -267,6 +285,35 @@ export class ConsoleApi {
|
|
|
267
285
|
throw new HttpError(404, "There is no such API.");
|
|
268
286
|
}
|
|
269
287
|
|
|
288
|
+
/** Connector and MCP authorization management: the owners' alone, whatever panes another tier has. */
|
|
289
|
+
async #manage(
|
|
290
|
+
request: Request,
|
|
291
|
+
parts: string[],
|
|
292
|
+
visitor: Visitor,
|
|
293
|
+
): Promise<Response> {
|
|
294
|
+
this.#pane("connectors", visitor);
|
|
295
|
+
if (visitor.tier !== "owner")
|
|
296
|
+
throw new HttpError(403, "Only an owner may do this.");
|
|
297
|
+
const [area, ...rest] = parts;
|
|
298
|
+
const { features, logger } = this.#ports;
|
|
299
|
+
const context = {
|
|
300
|
+
logger,
|
|
301
|
+
actor: {
|
|
302
|
+
principalId: visitor.principal.id,
|
|
303
|
+
speakerId: visitor.speakerId,
|
|
304
|
+
},
|
|
305
|
+
body: () => readBody(request),
|
|
306
|
+
changed: () => this.#ports.changed(),
|
|
307
|
+
};
|
|
308
|
+
const manage = features?.connectors?.manage;
|
|
309
|
+
if (area === "managed" && manage && rest.length <= 1)
|
|
310
|
+
return connectorRoutes(manage, request.method, rest[0], context);
|
|
311
|
+
const grants = features?.mcpGrants;
|
|
312
|
+
if (area === "grants" && grants && rest.length <= 2)
|
|
313
|
+
return grantRoutes(grants, request.method, rest, context);
|
|
314
|
+
throw new HttpError(404, "There is no such API.");
|
|
315
|
+
}
|
|
316
|
+
|
|
270
317
|
/**
|
|
271
318
|
* The page's config for a visitor: the owners' view as it was, and for another tier the panes
|
|
272
319
|
* of that tier. Where the app admits more than its owners it also says the visitor's tier, the
|
|
@@ -313,6 +360,15 @@ export class ConsoleApi {
|
|
|
313
360
|
...(presentation?.messages ? { messages: presentation.messages } : {}),
|
|
314
361
|
...(features?.cleanup ? { cleanup: true } : {}),
|
|
315
362
|
...(connectorAdminUrl ? { connectorAdminUrl } : {}),
|
|
363
|
+
...(features?.connectors || features?.mcpGrants
|
|
364
|
+
? {
|
|
365
|
+
connectorParts: {
|
|
366
|
+
...(features.connectors ? { gateways: true as const } : {}),
|
|
367
|
+
...(features.connectors?.manage ? { manage: true as const } : {}),
|
|
368
|
+
...(features.mcpGrants ? { grants: true as const } : {}),
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
: {}),
|
|
316
372
|
look: this.#ports.look ?? "modern",
|
|
317
373
|
...(this.#ports.characterSkins
|
|
318
374
|
? { characterSkins: [...this.#ports.characterSkins] }
|
|
@@ -328,7 +384,7 @@ export class ConsoleApi {
|
|
|
328
384
|
const { team } = this.#ports;
|
|
329
385
|
if (!team) throw new HttpError(404, "This pane is not served.");
|
|
330
386
|
const status = await team.status();
|
|
331
|
-
const { features } = this.#ports;
|
|
387
|
+
const { features, roomSurface } = this.#ports;
|
|
332
388
|
const stored = (await this.conversations()).conversations.filter(
|
|
333
389
|
(c) => c.kind === "owner" || c.kind === "outside",
|
|
334
390
|
);
|
|
@@ -350,6 +406,7 @@ export class ConsoleApi {
|
|
|
350
406
|
name: agent.name,
|
|
351
407
|
displayName: agent.displayName,
|
|
352
408
|
...(agent.channelId ? { channelId: agent.channelId } : {}),
|
|
409
|
+
...(roomSurface ? { room: agentRoomKey(roomSurface, agent.name) } : {}),
|
|
353
410
|
model: agent.model,
|
|
354
411
|
thinking: thinkingLabel(agent.thinking),
|
|
355
412
|
...(agent.workingIn ? { workingIn: agent.workingIn } : {}),
|
|
@@ -363,6 +420,7 @@ export class ConsoleApi {
|
|
|
363
420
|
displayName: group.displayName,
|
|
364
421
|
// A group of a team without Discord has no channel; the pane then links none.
|
|
365
422
|
...(group.channelId ? { channelId: group.channelId } : {}),
|
|
423
|
+
...(roomSurface ? { room: groupRoomKey(roomSurface, group.name) } : {}),
|
|
366
424
|
members: group.members,
|
|
367
425
|
host: group.host,
|
|
368
426
|
busy: group.busy,
|
package/src/features.ts
CHANGED
|
@@ -35,7 +35,98 @@ export interface ConsoleFeatures {
|
|
|
35
35
|
usedBy(server: string): string[];
|
|
36
36
|
/** Optional administrator link, limited to HTTP(S) or an absolute local path. */
|
|
37
37
|
adminUrl?: string;
|
|
38
|
+
/** The owner's connectors, managed from the page; pi-roundtable-mcp's `CONNECTORS` fits it. */
|
|
39
|
+
manage?: ConnectorManagement;
|
|
38
40
|
};
|
|
41
|
+
/** Which channels outside agents may use through a bundle's MCP URL; absent hides the section. */
|
|
42
|
+
mcpGrants?: McpGrants;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One connector. `url` may hold a credential in its path or query, so the console shows only its
|
|
47
|
+
* scheme and host.
|
|
48
|
+
*/
|
|
49
|
+
export interface ManagedConnector {
|
|
50
|
+
name: string;
|
|
51
|
+
url: string;
|
|
52
|
+
description: string;
|
|
53
|
+
/** Its virtual server; absent when its tools could not be read. */
|
|
54
|
+
server?: { tools: readonly string[] } | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Connector management. A rejection whose error carries `refusal: true` is shown to the owner as
|
|
59
|
+
* its message; any other is logged and answered as the service being unavailable.
|
|
60
|
+
*/
|
|
61
|
+
export interface ConnectorManagement {
|
|
62
|
+
list(): readonly ManagedConnector[] | Promise<readonly ManagedConnector[]>;
|
|
63
|
+
/** `token` is write-only: the console never returns or logs it. */
|
|
64
|
+
add(input: {
|
|
65
|
+
name: string;
|
|
66
|
+
url: string;
|
|
67
|
+
description: string;
|
|
68
|
+
header?: string;
|
|
69
|
+
token?: string;
|
|
70
|
+
}): Promise<{ connector: ManagedConnector; skipped: readonly string[] }>;
|
|
71
|
+
describe(name: string, description: string): Promise<unknown>;
|
|
72
|
+
remove(name: string): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The owner acting through the console, for a host that maps them to their Discord identity. */
|
|
76
|
+
export interface ConsoleActor {
|
|
77
|
+
principalId: string;
|
|
78
|
+
/** The identity they spoke as, `<provider>:<subject>`. */
|
|
79
|
+
speakerId: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface McpGrantChannel {
|
|
83
|
+
channelId: string;
|
|
84
|
+
guildName: string;
|
|
85
|
+
channelName: string;
|
|
86
|
+
/** False when the bot cannot see the channel now. */
|
|
87
|
+
visible: boolean;
|
|
88
|
+
/** The name and purpose an agent sees. */
|
|
89
|
+
displayName: string;
|
|
90
|
+
description: string;
|
|
91
|
+
operations: readonly string[];
|
|
92
|
+
authorizedAt: Date;
|
|
93
|
+
audit: readonly { tool: string; status: string; createdAt: Date }[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The owner's MCP bundles, as `/<root> mcp` manages them; pi-roundtable-mcp's `REMOTE_MCP` fits
|
|
98
|
+
* it once the host closes over its Discord client and supplies the channels and the actor's
|
|
99
|
+
* Discord ID. Refusals follow `ConnectorManagement`'s rule.
|
|
100
|
+
*/
|
|
101
|
+
export interface McpGrants {
|
|
102
|
+
/** Every operation a grant may allow, in order. */
|
|
103
|
+
operations: readonly { id: string; label: string }[];
|
|
104
|
+
bundles(): Promise<
|
|
105
|
+
readonly { name: string; grants: readonly McpGrantChannel[] }[]
|
|
106
|
+
>;
|
|
107
|
+
/** The channels the owner may pick to authorize. */
|
|
108
|
+
channels(
|
|
109
|
+
actor: ConsoleActor,
|
|
110
|
+
): Promise<readonly { id: string; name: string; guild: string }[]>;
|
|
111
|
+
revoke(bundle: string, channelId: string): Promise<void>;
|
|
112
|
+
describe(
|
|
113
|
+
bundle: string,
|
|
114
|
+
channelId: string,
|
|
115
|
+
change: { displayName?: string; description?: string },
|
|
116
|
+
): Promise<void>;
|
|
117
|
+
/** The new URL, returned this once. */
|
|
118
|
+
rotate(bundle: string): Promise<{ url: string }>;
|
|
119
|
+
/** Adds a channel to a bundle, creating it when new; a new bundle's URL is returned this once. */
|
|
120
|
+
authorize(
|
|
121
|
+
input: {
|
|
122
|
+
bundle: string;
|
|
123
|
+
channelId: string;
|
|
124
|
+
operations: readonly string[];
|
|
125
|
+
displayName?: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
},
|
|
128
|
+
actor: ConsoleActor,
|
|
129
|
+
): Promise<{ bundle: string; url?: string | undefined }>;
|
|
39
130
|
}
|
|
40
131
|
|
|
41
132
|
/** English source strings are keys; a host supplies its own wording without a page fork. */
|
package/src/host/reads.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
2
|
import type { Migration } from "pi-roundtable";
|
|
3
|
+
import { START_OVER_TEXT } from "../api-types.ts";
|
|
3
4
|
|
|
4
5
|
/** Where one person has read a room up to. */
|
|
5
6
|
export interface ReadMark {
|
|
@@ -29,7 +30,8 @@ interface MarkRow {
|
|
|
29
30
|
* The read cursors, one per person and room, kept in `web_room_reads`. A cursor only moves
|
|
30
31
|
* forward: reading an older message again, or a second device that is behind, changes nothing.
|
|
31
32
|
* A message counts as unread for a person when it is newer than their cursor and is not a line the
|
|
32
|
-
* server wrote (a divider, a yolo approval), which is nobody's to read
|
|
33
|
+
* server wrote (a divider, a yolo approval), which is nobody's to read, nor older than the room's
|
|
34
|
+
* latest start-over.
|
|
33
35
|
*/
|
|
34
36
|
export class RoomReads {
|
|
35
37
|
readonly #sql: SQL;
|
|
@@ -187,13 +189,20 @@ export class RoomReads {
|
|
|
187
189
|
INSERT INTO web_room_reads ${this.#sql(empty)}
|
|
188
190
|
ON CONFLICT (room, principal_id) DO NOTHING`;
|
|
189
191
|
}
|
|
192
|
+
// What a start-over archived is out of the room's view, so none of it is left to read.
|
|
190
193
|
const rows: { room: string; unread: string | number | bigint }[] =
|
|
191
194
|
await this.#sql`
|
|
195
|
+
WITH starts AS (
|
|
196
|
+
SELECT room, max(id) AS id FROM web_room_messages
|
|
197
|
+
WHERE room IN ${this.#sql(rooms)} AND kind = 'system' AND text = ${START_OVER_TEXT}
|
|
198
|
+
GROUP BY room)
|
|
192
199
|
SELECT m.room, count(*) AS unread
|
|
193
200
|
FROM web_room_messages m
|
|
194
201
|
LEFT JOIN web_room_reads r ON r.room = m.room AND r.principal_id = ${principalId}
|
|
202
|
+
LEFT JOIN starts s ON s.room = m.room
|
|
195
203
|
WHERE m.room IN ${this.#sql(rooms)} AND m.kind <> 'system'
|
|
196
204
|
AND m.id > COALESCE(r.last_read_id, 0)
|
|
205
|
+
AND m.id > COALESCE(s.id, 0)
|
|
197
206
|
GROUP BY m.room`;
|
|
198
207
|
for (const row of rows) counts.set(row.room, Number(row.unread));
|
|
199
208
|
return counts;
|