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.
Files changed (253) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +23 -7
  3. package/dist/chunk-account-678dgbww.js +1 -0
  4. package/dist/chunk-action-menu-qpp9bdxh.js +1 -0
  5. package/dist/chunk-agent-x0yvvdq2.js +1 -0
  6. package/dist/chunk-arranged-rooms-6rdkk8t1.js +1 -0
  7. package/dist/chunk-bulk-actions-zbwdaxtc.js +1 -0
  8. package/dist/chunk-character-creator-215psy17.js +1 -0
  9. package/dist/chunk-codeeditor-jgj32247.js +2 -0
  10. package/dist/chunk-confirm-2pzrt272.js +1 -0
  11. package/dist/chunk-connectors-zha2d834.js +37 -0
  12. package/dist/chunk-conversations-fb071rez.js +1 -0
  13. package/dist/chunk-council-sheet-k5fycefd.js +2 -0
  14. package/dist/chunk-drag-04qfef58.js +1 -0
  15. package/dist/chunk-entry-mws704mc.js +1 -0
  16. package/dist/chunk-folder-list-qxhmczwv.js +1 -0
  17. package/dist/chunk-folds-b7asegfs.js +1 -0
  18. package/dist/chunk-gate-vxetvn8t.js +1 -0
  19. package/dist/chunk-group-scene-63yhh1mh.js +1 -0
  20. package/dist/chunk-group-table-zy4hmgk3.js +1 -0
  21. package/dist/chunk-host-icons-tyktazzz.js +1 -0
  22. package/dist/chunk-host-picture-ebhejn2k.js +1 -0
  23. package/dist/chunk-host-state-wajep0yr.js +1 -0
  24. package/dist/chunk-host-ui-pc9r24e3.js +1 -0
  25. package/dist/chunk-index-1af8vsyb.js +2 -0
  26. package/dist/{chunk-index-g1awv7rk.js → chunk-index-20xgx0k2.js} +2 -2
  27. package/dist/chunk-index-254701sm.js +3 -0
  28. package/dist/{chunk-index-ffbrm6tr.js → chunk-index-2822z600.js} +3 -3
  29. package/dist/{chunk-index-2fdns8tn.js → chunk-index-2hvn2kte.js} +2 -2
  30. package/dist/chunk-index-2syyyjnq.js +2 -0
  31. package/dist/chunk-index-2tr41qht.js +2 -0
  32. package/dist/chunk-index-2y8r4s4f.js +2 -0
  33. package/dist/chunk-index-31m1b2f5.js +2 -0
  34. package/dist/{chunk-index-ak6ecqy3.js → chunk-index-3gfbhmpb.js} +2 -2
  35. package/dist/chunk-index-3yy10a5h.js +2 -0
  36. package/dist/{chunk-index-sm7m4fej.js → chunk-index-4t6vnwap.js} +2 -2
  37. package/dist/{chunk-index-j4x9dq30.css → chunk-index-5hbq6g2e.css} +1 -1
  38. package/dist/chunk-index-5t2jkecy.js +2 -0
  39. package/dist/chunk-index-5vatmakn.js +2 -0
  40. package/dist/{chunk-index-zyvwhxga.js → chunk-index-6cg8xp1c.js} +1 -1
  41. package/dist/chunk-index-6yra9kdq.js +2 -0
  42. package/dist/{chunk-index-ac1ktr56.js → chunk-index-7qd604rq.js} +2 -2
  43. package/dist/chunk-index-9bdstf8t.js +2 -0
  44. package/dist/chunk-index-acj4xxgq.js +100 -0
  45. package/dist/chunk-index-aexy023z.js +2 -0
  46. package/dist/chunk-index-amxa2bn3.js +2 -0
  47. package/dist/chunk-index-bef0fc2m.js +2 -0
  48. package/dist/chunk-index-c6tphrjr.js +1 -0
  49. package/dist/chunk-index-c7dbvkrq.js +2 -0
  50. package/dist/chunk-index-cn49mawx.js +50 -0
  51. package/dist/{chunk-index-3d8s3565.js → chunk-index-e2gy763y.js} +2 -2
  52. package/dist/{chunk-index-31x6krbs.js → chunk-index-e35v554x.js} +3 -3
  53. package/dist/chunk-index-fepsbhr6.js +47 -0
  54. package/dist/chunk-index-gcmmwh8t.js +2 -0
  55. package/dist/{chunk-index-kbrg32sx.js → chunk-index-gwkhs8s3.js} +2 -2
  56. package/dist/chunk-index-h2vhv4kh.js +2 -0
  57. package/dist/chunk-index-h9ccz1r3.js +2 -0
  58. package/dist/{chunk-index-0w4bfjb5.js → chunk-index-jc3r4qam.js} +2 -2
  59. package/dist/{chunk-index-8d81qy69.js → chunk-index-m81362sc.js} +2 -2
  60. package/dist/{chunk-index-xq84gg8w.js → chunk-index-n0cmnwd2.js} +2 -2
  61. package/dist/chunk-index-n0ejy0wd.js +2 -0
  62. package/dist/chunk-index-nkdg1dfz.js +7 -0
  63. package/dist/chunk-index-p7jkr7gv.js +14 -0
  64. package/dist/{chunk-index-e2dqjde8.js → chunk-index-q5fyrtx4.js} +1 -1
  65. package/dist/{chunk-index-jgzygbt8.js → chunk-index-qxynr1hf.js} +2 -2
  66. package/dist/chunk-index-r7fdgzy1.js +2 -0
  67. package/dist/chunk-index-rgf4rvns.js +4 -0
  68. package/dist/chunk-index-rsd8rftk.js +2 -0
  69. package/dist/{chunk-index-rh0x9rzf.js → chunk-index-s53bxwkk.js} +2 -2
  70. package/dist/{chunk-index-xxeb2xp0.js → chunk-index-s9p27f9b.js} +3 -3
  71. package/dist/chunk-index-sdcpf5dp.js +2 -0
  72. package/dist/chunk-index-sm7kfb07.js +2 -0
  73. package/dist/chunk-index-sw9r4aa4.js +2 -0
  74. package/dist/chunk-index-t5bmsqqd.js +1 -0
  75. package/dist/chunk-index-v8mma5np.js +2 -0
  76. package/dist/{chunk-index-q7g62sgm.js → chunk-index-v9gbt5tz.js} +3 -3
  77. package/dist/chunk-index-vsfsv8hd.js +2 -0
  78. package/dist/chunk-index-wxft0aqb.js +2 -0
  79. package/dist/chunk-index-x60n1me6.js +2 -0
  80. package/dist/chunk-index-zcfr8jdv.js +13 -0
  81. package/dist/{chunk-index-w42rcbx1.js → chunk-index-zj0gjcvr.js} +2 -2
  82. package/dist/{chunk-index-2t30xwed.js → chunk-index-zt19fdmc.js} +2 -2
  83. package/dist/chunk-index-zvvwxkev.js +2 -0
  84. package/dist/chunk-lang-css-7q1dtpgm.js +1 -0
  85. package/dist/{chunk-lang-dockerfile-cnwqn660.js → chunk-lang-dockerfile-n4bt8jbt.js} +1 -1
  86. package/dist/{chunk-lang-go-yy6kgsxv.js → chunk-lang-go-9k4y107e.js} +3 -3
  87. package/dist/chunk-lang-html-xgvba8rn.js +1 -0
  88. package/dist/chunk-lang-javascript-vgxmgd0a.js +1 -0
  89. package/dist/chunk-lang-json-hr12bzs1.js +1 -0
  90. package/dist/chunk-lang-markdown-ttzcpgkg.js +6 -0
  91. package/dist/chunk-lang-python-ntj8maem.js +3 -0
  92. package/dist/chunk-lang-rust-gv8y859d.js +1 -0
  93. package/dist/{chunk-lang-shell-1rpgtepw.js → chunk-lang-shell-5zzs09b7.js} +1 -1
  94. package/dist/chunk-lang-sql-6vmze4pg.js +2 -0
  95. package/dist/{chunk-lang-toml-tbj7467c.js → chunk-lang-toml-tt0d4rw9.js} +1 -1
  96. package/dist/chunk-lang-typescript-2v652sh5.js +1 -0
  97. package/dist/chunk-lang-xml-r2sbqnjv.js +1 -0
  98. package/dist/chunk-lang-yaml-ex31qbph.js +1 -0
  99. package/dist/chunk-layout-actions-bfqtg71d.js +1 -0
  100. package/dist/chunk-menu-items-rztgekfj.js +1 -0
  101. package/dist/chunk-mode-toggle-m5kgb7c7.js +1 -0
  102. package/dist/chunk-note-row-40ypghn9.js +204 -0
  103. package/dist/chunk-notes-a8nqffbf.js +1 -0
  104. package/dist/chunk-notice-bell-rdnfmtf2.js +1 -0
  105. package/dist/{chunk-notices-fte9f196.js → chunk-notices-tefx93s6.js} +1 -1
  106. package/dist/chunk-onboarding-wvh0s3vg.js +1 -0
  107. package/dist/chunk-overview-zrx3bpwf.js +1 -0
  108. package/dist/chunk-palette-5xtdvy9a.js +1 -0
  109. package/dist/chunk-placement-items-rgyrktvy.js +1 -0
  110. package/dist/chunk-profile-tyb48b79.js +1 -0
  111. package/dist/chunk-prompt-history-wv0vdsjv.js +1 -0
  112. package/dist/chunk-prompts-75hgb0t0.js +1 -0
  113. package/dist/chunk-room-confirms-bcvhpcry.js +1 -0
  114. package/dist/chunk-room-menu-ha1evv71.js +1 -0
  115. package/dist/chunk-rooms-menu-button-ej3a2hyc.js +1 -0
  116. package/dist/chunk-schedules-a5pewkhd.js +1 -0
  117. package/dist/{chunk-seat-menu-r0k0eawr.js → chunk-seat-menu-eh3ymmks.js} +1 -1
  118. package/dist/chunk-sheet-hosts-d5zbwbbr.js +29 -0
  119. package/dist/{chunk-shortcut-help-a6pkjcq7.js → chunk-shortcut-help-bh9ntk9s.js} +2 -2
  120. package/dist/{chunk-shortcuts-cgt87jkz.js → chunk-shortcuts-6em0atk9.js} +1 -1
  121. package/dist/chunk-skill-editor-e43mwz01.js +8 -0
  122. package/dist/chunk-skills-vp107wrp.js +1 -0
  123. package/dist/chunk-start-over-line-xd1v2p0g.js +16 -0
  124. package/dist/chunk-switcher-list-dtfh87zy.js +1 -0
  125. package/dist/{chunk-tab-dot-vr11dfrw.js → chunk-tab-dot-qd640mjr.js} +1 -1
  126. package/dist/chunk-table-svb2xhfd.js +1 -0
  127. package/dist/chunk-table2d-5nrhfwft.js +1 -0
  128. package/dist/chunk-table3d-5cb66d99.js +4220 -0
  129. package/dist/chunk-thread-actions-926eab0w.js +1 -0
  130. package/dist/{chunk-thread-cards-c2mrp2gj.js → chunk-thread-cards-kgt22sbp.js} +1 -1
  131. package/dist/chunk-thread-list-j2v1pqkz.js +217 -0
  132. package/dist/chunk-transcript-a0p3vwh6.js +1 -0
  133. package/dist/chunk-waiting-cdyz7r1g.js +1 -0
  134. package/dist/chunk-waiting-chip-58ttx9av.js +28 -0
  135. package/dist/chunk-where-9jp6g029.js +1 -0
  136. package/dist/chunk-workspace-d34njy4p.js +578 -0
  137. package/dist/chunk-zh-tw-y3jb20wc.js +1 -0
  138. package/dist/index.html +1 -1
  139. package/dist/sw.js +1 -1
  140. package/package.json +3 -3
  141. package/src/api-types.ts +98 -1
  142. package/src/app/app.ts +4 -7
  143. package/src/app/desk.ts +19 -4
  144. package/src/app/room-log.ts +66 -18
  145. package/src/app/rooms.ts +15 -5
  146. package/src/app/routes.ts +60 -9
  147. package/src/app/skill-doc-routes.ts +12 -2
  148. package/src/app/surface.ts +26 -2
  149. package/src/app/team-routes.ts +3 -7
  150. package/src/authz.ts +1 -0
  151. package/src/cleanup-api.ts +9 -2
  152. package/src/connector-manage-api.ts +257 -0
  153. package/src/console-api.ts +60 -2
  154. package/src/features.ts +91 -0
  155. package/src/host/reads.ts +10 -1
  156. package/src/i18n/zh-tw.ts +107 -4
  157. package/src/web-plugin.ts +7 -1
  158. package/dist/chunk-account-1ecx5kt7.js +0 -1
  159. package/dist/chunk-action-menu-zh2n0ahf.js +0 -1
  160. package/dist/chunk-agent-ynm2tcbx.js +0 -1
  161. package/dist/chunk-arranged-rooms-qnz8vese.js +0 -1
  162. package/dist/chunk-bulk-actions-pr0mw0kc.js +0 -1
  163. package/dist/chunk-character-creator-caj4pmg4.js +0 -1
  164. package/dist/chunk-codeeditor-dga6s78p.js +0 -2
  165. package/dist/chunk-confirm-g76v5d9r.js +0 -1
  166. package/dist/chunk-connectors-j79ze3j1.js +0 -1
  167. package/dist/chunk-conversations-2farn72g.js +0 -1
  168. package/dist/chunk-council-sheet-s9sz1bzf.js +0 -2
  169. package/dist/chunk-drag-45e8222r.js +0 -1
  170. package/dist/chunk-entry-q11n47h9.js +0 -1
  171. package/dist/chunk-folder-list-4gcj7g7h.js +0 -1
  172. package/dist/chunk-folds-ten4ykfw.js +0 -1
  173. package/dist/chunk-gate-kcv1a4t0.js +0 -1
  174. package/dist/chunk-group-scene-12ghcvzd.js +0 -1
  175. package/dist/chunk-group-table-t458kdgn.js +0 -1
  176. package/dist/chunk-host-icons-h1k9k96a.js +0 -1
  177. package/dist/chunk-host-picture-3p0wzn3v.js +0 -1
  178. package/dist/chunk-host-state-7mdve7cb.js +0 -1
  179. package/dist/chunk-host-ui-bdgwa6sf.js +0 -1
  180. package/dist/chunk-index-02vfshtt.js +0 -2
  181. package/dist/chunk-index-0a81da8z.js +0 -2
  182. package/dist/chunk-index-12zyshae.js +0 -2
  183. package/dist/chunk-index-18pgxxw3.js +0 -2
  184. package/dist/chunk-index-2e4am3hw.js +0 -50
  185. package/dist/chunk-index-5bfqek1d.js +0 -2
  186. package/dist/chunk-index-64rsz6pf.js +0 -2
  187. package/dist/chunk-index-6bkcq9ka.js +0 -2
  188. package/dist/chunk-index-92zgb7my.js +0 -2
  189. package/dist/chunk-index-aamhjg5n.js +0 -2
  190. package/dist/chunk-index-b73y2gc0.js +0 -47
  191. package/dist/chunk-index-b8cpvr7y.js +0 -2
  192. package/dist/chunk-index-bhqczxr3.js +0 -2
  193. package/dist/chunk-index-cbqhzf40.js +0 -13
  194. package/dist/chunk-index-cdctnw71.js +0 -2
  195. package/dist/chunk-index-dj0tbfyb.js +0 -2
  196. package/dist/chunk-index-dnjftvtq.js +0 -2
  197. package/dist/chunk-index-ebg48jgy.js +0 -2
  198. package/dist/chunk-index-ek5mek4j.js +0 -14
  199. package/dist/chunk-index-f6q0stqx.js +0 -2
  200. package/dist/chunk-index-g3wrs02w.js +0 -1
  201. package/dist/chunk-index-j0m16cj1.js +0 -2
  202. package/dist/chunk-index-j4kk1m59.js +0 -2
  203. package/dist/chunk-index-kb3w11mn.js +0 -7
  204. package/dist/chunk-index-msqhgvfx.js +0 -2
  205. package/dist/chunk-index-pefwfm26.js +0 -2
  206. package/dist/chunk-index-pyfx77cj.js +0 -2
  207. package/dist/chunk-index-qfgkfq86.js +0 -2
  208. package/dist/chunk-index-sv9qpmhv.js +0 -1
  209. package/dist/chunk-index-t998yg8y.js +0 -2
  210. package/dist/chunk-index-tk4se135.js +0 -4
  211. package/dist/chunk-index-v6smgw77.js +0 -2
  212. package/dist/chunk-index-x4enmj72.js +0 -2
  213. package/dist/chunk-lang-css-y9133ktr.js +0 -1
  214. package/dist/chunk-lang-html-r2xmryek.js +0 -1
  215. package/dist/chunk-lang-javascript-1r6wd177.js +0 -1
  216. package/dist/chunk-lang-json-dx56g11y.js +0 -1
  217. package/dist/chunk-lang-markdown-fpcf2dfq.js +0 -6
  218. package/dist/chunk-lang-python-zzp95955.js +0 -3
  219. package/dist/chunk-lang-rust-214fh7sp.js +0 -1
  220. package/dist/chunk-lang-sql-vvp46hpj.js +0 -2
  221. package/dist/chunk-lang-typescript-y5w4tt4g.js +0 -1
  222. package/dist/chunk-lang-xml-tbcqmedk.js +0 -1
  223. package/dist/chunk-lang-yaml-jh56t3sv.js +0 -1
  224. package/dist/chunk-layout-actions-y73mpsh1.js +0 -1
  225. package/dist/chunk-menu-items-bza16bw6.js +0 -2
  226. package/dist/chunk-mode-toggle-nj9gnt3b.js +0 -1
  227. package/dist/chunk-note-row-6fmhrdng.js +0 -75
  228. package/dist/chunk-notes-wv0e6ssf.js +0 -1
  229. package/dist/chunk-notice-bell-9z27epd2.js +0 -1
  230. package/dist/chunk-onboarding-c5twahc3.js +0 -1
  231. package/dist/chunk-overview-k4ccprax.js +0 -1
  232. package/dist/chunk-palette-9rbp2fab.js +0 -1
  233. package/dist/chunk-placement-items-me598wsb.js +0 -1
  234. package/dist/chunk-profile-s7r7fcvz.js +0 -1
  235. package/dist/chunk-prompt-history-hf296e7v.js +0 -1
  236. package/dist/chunk-prompts-c8s0nhkm.js +0 -1
  237. package/dist/chunk-room-confirms-ga2sbvhc.js +0 -1
  238. package/dist/chunk-room-menu-7h4jym2b.js +0 -1
  239. package/dist/chunk-rooms-menu-button-pgtaanb6.js +0 -1
  240. package/dist/chunk-schedules-mbnwcxpc.js +0 -1
  241. package/dist/chunk-sheet-hosts-a5dfevy9.js +0 -1
  242. package/dist/chunk-skill-editor-67yk4x7t.js +0 -8
  243. package/dist/chunk-skills-rewemey4.js +0 -1
  244. package/dist/chunk-switcher-list-t8rbay2y.js +0 -1
  245. package/dist/chunk-table-52nh0azk.js +0 -1
  246. package/dist/chunk-table2d-g57vr6h0.js +0 -1
  247. package/dist/chunk-table3d-rp0w6kt4.js +0 -4220
  248. package/dist/chunk-thread-actions-qcxt1y1g.js +0 -1
  249. package/dist/chunk-thread-list-0071q455.js +0 -217
  250. package/dist/chunk-transcript-789yqysq.js +0 -1
  251. package/dist/chunk-where-hmwmme9e.js +0 -1
  252. package/dist/chunk-workspace-wr1qrtsh.js +0 -578
  253. 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
+ }
@@ -11,7 +11,13 @@ import type {
11
11
  SpeakerMemory,
12
12
  Tier,
13
13
  } from "pi-roundtable";
14
- import { MemoryError, parseChannelKey, SYSTEM_PRINCIPAL } from "pi-roundtable";
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;