pi-roundtable-web 0.7.2 → 0.7.3
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 +4 -0
- package/README.md +58 -8
- package/dist/chunk-32sbjm56.js +36 -0
- package/dist/index.html +1 -1
- package/package.json +4 -2
- package/src/api-types.ts +66 -1
- package/src/cleanup-api.ts +54 -0
- package/src/connectors-api.ts +26 -0
- package/src/console-api.ts +108 -22
- package/src/console-server.ts +35 -11
- package/src/conversations.ts +27 -21
- package/src/features.ts +59 -0
- package/src/http.ts +11 -0
- package/src/index.ts +8 -0
- package/src/options.ts +32 -2
- package/src/page-presentation.ts +30 -0
- package/src/skills-api.ts +77 -0
- package/src/web-plugin.ts +14 -1
- package/dist/chunk-gkh6x5ag.js +0 -9
package/src/index.ts
CHANGED
|
@@ -5,16 +5,21 @@ export type {
|
|
|
5
5
|
ApiError,
|
|
6
6
|
ChannelName,
|
|
7
7
|
ConfigView,
|
|
8
|
+
ConnectorsView,
|
|
8
9
|
ContextView,
|
|
9
10
|
ConversationKind,
|
|
10
11
|
ConversationsView,
|
|
11
12
|
ConversationView,
|
|
13
|
+
DashboardView,
|
|
12
14
|
GroupView,
|
|
13
15
|
NoteInput,
|
|
14
16
|
NoteKind,
|
|
15
17
|
NoteView,
|
|
16
18
|
OverviewView,
|
|
17
19
|
PaneName,
|
|
20
|
+
PartyView,
|
|
21
|
+
SkillDetailView,
|
|
22
|
+
SkillView,
|
|
18
23
|
TranscriptEntry,
|
|
19
24
|
TranscriptRole,
|
|
20
25
|
TranscriptView,
|
|
@@ -22,6 +27,9 @@ export type {
|
|
|
22
27
|
export { PANES } from "./api-types.ts";
|
|
23
28
|
export type { CloudflareAccessOptions } from "./cloudflare-access.ts";
|
|
24
29
|
export { cloudflareAccess } from "./cloudflare-access.ts";
|
|
30
|
+
/** Summary for a trusted host's separately stored party sessions. */
|
|
31
|
+
export { conversationFiles as sessionSummary } from "./conversations.ts";
|
|
32
|
+
export type { ConsoleFeatures, ConsolePresentation } from "./features.ts";
|
|
25
33
|
export type { WebConsoleOptions } from "./options.ts";
|
|
26
34
|
export { DEFAULT_RELAY_NOTE } from "./options.ts";
|
|
27
35
|
export type { RequestVerifier, Verdict } from "./verifier.ts";
|
package/src/options.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import type { ChannelKey } from "pi-roundtable";
|
|
2
|
+
import type { ChannelKey, RoundtablePlugin } from "pi-roundtable";
|
|
3
3
|
import { PluginError } from "pi-roundtable";
|
|
4
4
|
import { PANES, type PaneName } from "./api-types.ts";
|
|
5
|
+
import type { ConsoleFeatures, ConsolePresentation } from "./features.ts";
|
|
5
6
|
import type { RequestVerifier } from "./verifier.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -17,6 +18,14 @@ export interface WebConsoleOptions {
|
|
|
17
18
|
* built-in one. Without it the plugin refuses to start rather than serve without authentication.
|
|
18
19
|
*/
|
|
19
20
|
verifier: RequestVerifier;
|
|
21
|
+
/** Host-specific data and actions, resolved against public plugin setup context. */
|
|
22
|
+
features?:
|
|
23
|
+
| ConsoleFeatures
|
|
24
|
+
| ((
|
|
25
|
+
context: Parameters<RoundtablePlugin["setup"]>[0],
|
|
26
|
+
) => ConsoleFeatures | Promise<ConsoleFeatures>);
|
|
27
|
+
/** Localized labels and error text; English source strings are dictionary keys. */
|
|
28
|
+
presentation?: ConsolePresentation;
|
|
20
29
|
/**
|
|
21
30
|
* The console's own origin, such as `https://console.example.com`: requests that change data
|
|
22
31
|
* must carry it as their `Origin`, and the dashboard line links to it.
|
|
@@ -30,6 +39,8 @@ export interface WebConsoleOptions {
|
|
|
30
39
|
mountPath?: string;
|
|
31
40
|
/** The listener whose address serves it; default `public`. */
|
|
32
41
|
listener?: string;
|
|
42
|
+
/** Hash routes by default; path routes retain links such as /console/skills. */
|
|
43
|
+
routing?: "hash" | "path";
|
|
33
44
|
/** The panes to serve, in the order the page lists them; default all of `overview`, `conversations`, `notes`. */
|
|
34
45
|
panes?: readonly PaneName[];
|
|
35
46
|
/** The page's title and heading; default `Roundtable`. */
|
|
@@ -50,11 +61,14 @@ export interface ResolvedOptions {
|
|
|
50
61
|
sessionsDir: string;
|
|
51
62
|
mount: string;
|
|
52
63
|
listener: string;
|
|
64
|
+
routing: "hash" | "path";
|
|
53
65
|
panes: PaneName[];
|
|
54
66
|
title: string;
|
|
55
67
|
relayNotes: readonly string[];
|
|
56
68
|
exclude: ((key: ChannelKey) => boolean) | undefined;
|
|
57
69
|
assetDir: string | undefined;
|
|
70
|
+
features: WebConsoleOptions["features"];
|
|
71
|
+
presentation: ConsolePresentation | undefined;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
const SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
@@ -99,7 +113,7 @@ function checkOrigin(origin: string): string {
|
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
function checkPanes(panes: readonly PaneName[] | undefined): PaneName[] {
|
|
102
|
-
if (panes === undefined) return [
|
|
116
|
+
if (panes === undefined) return ["overview", "conversations", "notes"];
|
|
103
117
|
if (panes.length === 0) fail("panes is empty; serve at least one pane");
|
|
104
118
|
for (const pane of panes)
|
|
105
119
|
if (!PANES.includes(pane))
|
|
@@ -116,6 +130,19 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
116
130
|
fail(
|
|
117
131
|
"no verifier. The console serves the owner's conversations and notes, so it will not start without one that authenticates every request; pass `verifier: cloudflareAccess({...})` or your own",
|
|
118
132
|
);
|
|
133
|
+
if (
|
|
134
|
+
options.routing !== undefined &&
|
|
135
|
+
options.routing !== "path" &&
|
|
136
|
+
options.routing !== "hash"
|
|
137
|
+
)
|
|
138
|
+
fail("routing must be hash or path");
|
|
139
|
+
if (options.presentation?.locale !== undefined) {
|
|
140
|
+
try {
|
|
141
|
+
new Intl.DateTimeFormat(options.presentation.locale);
|
|
142
|
+
} catch {
|
|
143
|
+
fail("presentation.locale is not a valid locale");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
119
146
|
const panes = checkPanes(options.panes);
|
|
120
147
|
const ownerId = options.ownerId?.trim();
|
|
121
148
|
if (panes.includes("notes") && !ownerId)
|
|
@@ -131,10 +158,13 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
131
158
|
sessionsDir: join(options.dataDir ?? "data", "sessions"),
|
|
132
159
|
mount: checkMount(options.mountPath ?? "/console"),
|
|
133
160
|
listener: options.listener ?? "public",
|
|
161
|
+
routing: options.routing ?? "hash",
|
|
134
162
|
panes,
|
|
135
163
|
title,
|
|
136
164
|
relayNotes: options.relayNotes ?? [DEFAULT_RELAY_NOTE],
|
|
137
165
|
exclude: options.exclude,
|
|
138
166
|
assetDir: options.assetDir,
|
|
167
|
+
features: options.features,
|
|
168
|
+
presentation: options.presentation,
|
|
139
169
|
};
|
|
140
170
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ConsolePresentation } from "./features.ts";
|
|
2
|
+
|
|
3
|
+
const attribute = (value: string): string =>
|
|
4
|
+
value
|
|
5
|
+
.replaceAll("&", "&")
|
|
6
|
+
.replaceAll('"', """)
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll("'", "'");
|
|
10
|
+
|
|
11
|
+
/** Authenticated, inert bootstrap data localizes even a failed first API request without inline scripts. */
|
|
12
|
+
export function presentPage(
|
|
13
|
+
body: Uint8Array,
|
|
14
|
+
mount: string | undefined,
|
|
15
|
+
presentation: ConsolePresentation | undefined,
|
|
16
|
+
): Uint8Array<ArrayBuffer> | string {
|
|
17
|
+
if (!mount && !presentation) return new Uint8Array(body);
|
|
18
|
+
let head = "<head>";
|
|
19
|
+
if (mount) head += `<base href="${attribute(`${mount}/`)}">`;
|
|
20
|
+
if (presentation)
|
|
21
|
+
head += `<meta name="roundtable-console-presentation" content="${attribute(JSON.stringify(presentation))}">`;
|
|
22
|
+
let html = new TextDecoder().decode(body).replace("<head>", () => head);
|
|
23
|
+
if (presentation?.locale)
|
|
24
|
+
html = html.replace(
|
|
25
|
+
/(<html[^>]*lang=")[^"]*(")/,
|
|
26
|
+
(_all, prefix: string, suffix: string) =>
|
|
27
|
+
`${prefix}${attribute(presentation.locale ?? "en")}${suffix}`,
|
|
28
|
+
);
|
|
29
|
+
return html;
|
|
30
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { SkillDetailView, SkillView } from "./api-types.ts";
|
|
2
|
+
import type { ConsoleFeatures } from "./features.ts";
|
|
3
|
+
import { ConsoleHttpError as HttpError } from "./http.ts";
|
|
4
|
+
|
|
5
|
+
/** Strip private file paths from a host catalog explicitly. */
|
|
6
|
+
export function skillCatalog(
|
|
7
|
+
skills: NonNullable<ConsoleFeatures["skills"]>,
|
|
8
|
+
): SkillView[] {
|
|
9
|
+
return skills
|
|
10
|
+
.catalog()
|
|
11
|
+
.map(({ name, source, description, missing, groups, carriers }) => ({
|
|
12
|
+
name,
|
|
13
|
+
source,
|
|
14
|
+
...(description ? { description } : {}),
|
|
15
|
+
...(missing ? { missing } : {}),
|
|
16
|
+
groups,
|
|
17
|
+
carriers,
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read a catalog-selected skill, bound its document, and return ordered frontmatter. */
|
|
22
|
+
export async function skillDetail(
|
|
23
|
+
skills: NonNullable<ConsoleFeatures["skills"]>,
|
|
24
|
+
name: string,
|
|
25
|
+
): Promise<SkillDetailView> {
|
|
26
|
+
const entry = skills.catalog().find((s) => s.name === name);
|
|
27
|
+
if (!entry) throw new HttpError(404, "There is no such skill.");
|
|
28
|
+
if (entry.missing) throw new HttpError(404, "The skill file is missing.");
|
|
29
|
+
try {
|
|
30
|
+
const parsed = await skills.read(name);
|
|
31
|
+
if (
|
|
32
|
+
Buffer.byteLength(parsed.body) > 1024 * 1024 ||
|
|
33
|
+
Buffer.byteLength(JSON.stringify(parsed.frontmatter)) > 64 * 1024
|
|
34
|
+
)
|
|
35
|
+
throw new HttpError(413, "The skill file is too large.");
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
body: parsed.body,
|
|
39
|
+
metadata: metadataEntries(parsed.frontmatter),
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof HttpError) throw error;
|
|
43
|
+
throw new HttpError(422, "The skill frontmatter could not be read.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Flatten nested frontmatter in source order, keeping scalar/array values readable. */
|
|
48
|
+
function metadataEntries(
|
|
49
|
+
record: Record<string, unknown>,
|
|
50
|
+
prefix = "",
|
|
51
|
+
depth = 0,
|
|
52
|
+
): SkillDetailView["metadata"] {
|
|
53
|
+
return Object.entries(record).flatMap(([key, value]) => {
|
|
54
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
55
|
+
if (
|
|
56
|
+
depth < 20 &&
|
|
57
|
+
value !== null &&
|
|
58
|
+
typeof value === "object" &&
|
|
59
|
+
!Array.isArray(value)
|
|
60
|
+
)
|
|
61
|
+
return metadataEntries(value as Record<string, unknown>, path, depth + 1);
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
key: path,
|
|
65
|
+
value: Array.isArray(value)
|
|
66
|
+
? value
|
|
67
|
+
.map((v) =>
|
|
68
|
+
typeof v === "object" ? JSON.stringify(v) : String(v ?? ""),
|
|
69
|
+
)
|
|
70
|
+
.join(", ")
|
|
71
|
+
: typeof value === "object" && value !== null
|
|
72
|
+
? JSON.stringify(value)
|
|
73
|
+
: String(value ?? ""),
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
});
|
|
77
|
+
}
|
package/src/web-plugin.ts
CHANGED
|
@@ -33,8 +33,16 @@ export function webConsole(options: WebConsoleOptions): RoundtablePlugin {
|
|
|
33
33
|
|
|
34
34
|
type Context = Parameters<RoundtablePlugin["setup"]>[0];
|
|
35
35
|
|
|
36
|
-
function build(settings: ResolvedOptions, context: Context) {
|
|
36
|
+
async function build(settings: ResolvedOptions, context: Context) {
|
|
37
37
|
const { services, queue, logger, env } = context;
|
|
38
|
+
const features =
|
|
39
|
+
typeof settings.features === "function"
|
|
40
|
+
? await settings.features(context)
|
|
41
|
+
: settings.features;
|
|
42
|
+
for (const pane of ["skills", "connectors"] as const) {
|
|
43
|
+
if (settings.panes.includes(pane) && !features?.[pane])
|
|
44
|
+
throw new Error(`web-console: the ${pane} pane needs its feature port`);
|
|
45
|
+
}
|
|
38
46
|
const team = services.find(AGENTS)?.team;
|
|
39
47
|
const connection = services.find(DISCORD)?.connection;
|
|
40
48
|
const listeners: (() => void)[] = [];
|
|
@@ -60,12 +68,17 @@ function build(settings: ResolvedOptions, context: Context) {
|
|
|
60
68
|
relayNotes: settings.relayNotes,
|
|
61
69
|
changed,
|
|
62
70
|
logger,
|
|
71
|
+
...(features ? { features } : {}),
|
|
72
|
+
...(settings.routing === "path" ? { mountPath: settings.mount } : {}),
|
|
73
|
+
...(settings.presentation ? { presentation: settings.presentation } : {}),
|
|
63
74
|
});
|
|
64
75
|
const server = new ConsoleServer({
|
|
65
76
|
mount: settings.mount,
|
|
66
77
|
assets,
|
|
67
78
|
verifier: settings.verifier,
|
|
68
79
|
origin: settings.origin,
|
|
80
|
+
pathRouting: settings.routing === "path",
|
|
81
|
+
...(settings.presentation ? { presentation: settings.presentation } : {}),
|
|
69
82
|
api,
|
|
70
83
|
subscribe: (listener) => {
|
|
71
84
|
listeners.push(listener);
|