bell-reader 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +75 -2
- package/README.zh-TW.md +55 -0
- package/bin/bell-reader.mjs +202 -0
- package/dist/assets/caveat-cyrillic-400-normal-9cDH9rLW.woff2 +0 -0
- package/dist/assets/caveat-cyrillic-400-normal-CebvvJET.woff +0 -0
- package/dist/assets/caveat-cyrillic-500-normal-DLSfp5IU.woff +0 -0
- package/dist/assets/caveat-cyrillic-500-normal-DpMZbbjM.woff2 +0 -0
- package/dist/assets/caveat-cyrillic-ext-400-normal-3iEGd-c5.woff2 +0 -0
- package/dist/assets/caveat-cyrillic-ext-400-normal-Cg0RnRQ5.woff +0 -0
- package/dist/assets/caveat-cyrillic-ext-500-normal-DcrB4vVx.woff2 +0 -0
- package/dist/assets/caveat-cyrillic-ext-500-normal-DqTXtm5N.woff +0 -0
- package/dist/assets/caveat-latin-400-normal-BzhAQZkN.woff +0 -0
- package/dist/assets/caveat-latin-400-normal-D6LQsQ_v.woff2 +0 -0
- package/dist/assets/caveat-latin-500-normal-B9SDL8cy.woff2 +0 -0
- package/dist/assets/caveat-latin-500-normal-Do9veZfZ.woff +0 -0
- package/dist/assets/caveat-latin-ext-400-normal-D7hBUiug.woff2 +0 -0
- package/dist/assets/caveat-latin-ext-400-normal-DtiRFvw0.woff +0 -0
- package/dist/assets/caveat-latin-ext-500-normal-jKqJW7bC.woff2 +0 -0
- package/dist/assets/caveat-latin-ext-500-normal-wUUXxlbS.woff +0 -0
- package/dist/assets/index-D9OudjOw.js +84 -0
- package/dist/assets/index-KEOK2cPN.css +1 -0
- package/dist/assets/newsreader-latin-ext-opsz-normal-BQn1nviT.woff2 +0 -0
- package/dist/assets/newsreader-latin-opsz-normal-s-izfB6B.woff2 +0 -0
- package/dist/assets/newsreader-vietnamese-opsz-normal-BjhtXyW2.woff2 +0 -0
- package/dist/fonts/LICENSE-geist-mono.txt +93 -0
- package/dist/fonts/LICENSE-geist.txt +93 -0
- package/dist/fonts/geist-latin-wght.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-wght.woff2 +0 -0
- package/dist/fonts/iansui-tc.ttf +0 -0
- package/dist/fonts/naikai-tc.woff2 +0 -0
- package/dist/index.html +13 -0
- package/package.json +77 -4
- package/server/ai.ts +1391 -0
- package/server/app.ts +644 -0
- package/server/config.ts +140 -0
- package/server/errors.ts +22 -0
- package/server/explain-bridge.ts +320 -0
- package/server/extractor.ts +548 -0
- package/server/index.ts +28 -0
- package/server/security.ts +78 -0
- package/server/store.ts +595 -0
- package/shared/suite.ts +248 -0
- package/shared/types.ts +132 -0
package/shared/suite.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* suite-contract v1 (rev. f: adds "campaign" as the 5th BELL Suite product)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SUITE_APP_IDS = ["explain", "personal", "pr", "reader", "campaign"] as const;
|
|
6
|
+
export type SuiteAppId = (typeof SUITE_APP_IDS)[number];
|
|
7
|
+
export type SuiteLinks = Partial<Record<SuiteAppId, string>>;
|
|
8
|
+
|
|
9
|
+
export const SUITE_LIMITS = {
|
|
10
|
+
title: 120,
|
|
11
|
+
detail: 80,
|
|
12
|
+
id: 128,
|
|
13
|
+
href: 512,
|
|
14
|
+
items: 5,
|
|
15
|
+
counts: 8,
|
|
16
|
+
countMax: 100_000,
|
|
17
|
+
capabilities: 16,
|
|
18
|
+
displayName: 40,
|
|
19
|
+
summary: 40,
|
|
20
|
+
links: 5,
|
|
21
|
+
handoffText: 2000,
|
|
22
|
+
handoffTitle: 120,
|
|
23
|
+
back: 512,
|
|
24
|
+
fetchTimeoutMs: 3000,
|
|
25
|
+
fetchMaxBytes: 64 * 1024,
|
|
26
|
+
cacheFreshMs: 60_000,
|
|
27
|
+
staleDropMs: 30 * 60_000,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const SUITE_OBJECT_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
31
|
+
const HREF = /^#\/[A-Za-z0-9/_\-.~%:]*(?:\?[A-Za-z0-9_\-.~%=&+]*)?$/;
|
|
32
|
+
export const SUITE_UNSAFE_TEXT = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
|
|
33
|
+
const UNSAFE_HANDOFF = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
|
|
34
|
+
const SINGLE_HANDOFF_PARAMS = ["text", "title", "draft", "from", "back"] as const;
|
|
35
|
+
|
|
36
|
+
export const SUITE_ROUTES: Record<SuiteAppId, readonly string[]> = {
|
|
37
|
+
explain: ["#/", "#/library", "#/graph", "#/favorites", "#/repositories", "#/review", "#/note/{id}"],
|
|
38
|
+
personal: ["#/today", "#/tasks", "#/tasks/{id}", "#/events", "#/events/{id}", "#/ideas", "#/ideas/{id}", "#/inbox", "#/inbox/{id}"],
|
|
39
|
+
pr: ["#/", "#/home", "#/demo", "#/repos", "#/repos/{id}", "#/discover", "#/team", "#/group", "#/learning", "#/settings", "#/runs/{id}"],
|
|
40
|
+
reader: ["#/", "#/library", "#/topics", "#/page/{id}"],
|
|
41
|
+
campaign: [
|
|
42
|
+
"#/",
|
|
43
|
+
"#/schedule",
|
|
44
|
+
"#/tasks",
|
|
45
|
+
"#/partners",
|
|
46
|
+
"#/forms",
|
|
47
|
+
"#/files",
|
|
48
|
+
"#/meetings",
|
|
49
|
+
"#/inspirations",
|
|
50
|
+
"#/notes",
|
|
51
|
+
"#/bot",
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface SuiteCapability {
|
|
56
|
+
id: string;
|
|
57
|
+
kind: "accept-handoff" | "today-feed" | "object-route";
|
|
58
|
+
route?: string;
|
|
59
|
+
api?: string;
|
|
60
|
+
accepts?: ["text"];
|
|
61
|
+
maxChars?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SuiteManifest {
|
|
65
|
+
schemaVersion: 1;
|
|
66
|
+
appId: SuiteAppId;
|
|
67
|
+
displayName: string;
|
|
68
|
+
summary: string;
|
|
69
|
+
capabilities: SuiteCapability[];
|
|
70
|
+
links: SuiteLinks;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SuiteTodayItem {
|
|
74
|
+
sourceApp: SuiteAppId;
|
|
75
|
+
type: string;
|
|
76
|
+
id: string;
|
|
77
|
+
title: string;
|
|
78
|
+
href: string;
|
|
79
|
+
state: "due" | "new" | "waiting" | "needs-you";
|
|
80
|
+
updatedAt: string;
|
|
81
|
+
dueAt?: string;
|
|
82
|
+
detail?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SuiteTodayFeed {
|
|
86
|
+
schemaVersion: 1;
|
|
87
|
+
appId: SuiteAppId;
|
|
88
|
+
generatedAt: string;
|
|
89
|
+
counts: Record<string, number>;
|
|
90
|
+
items: SuiteTodayItem[];
|
|
91
|
+
truncated: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function validSuiteHref(value: unknown, max: number = SUITE_LIMITS.href): value is string {
|
|
95
|
+
if (typeof value !== "string" || value.length > max || !HREF.test(value)) return false;
|
|
96
|
+
if (value.includes("//")) return false;
|
|
97
|
+
let decoded: string;
|
|
98
|
+
try {
|
|
99
|
+
decoded = decodeURIComponent(value.replace(/\+/g, " "));
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return !SUITE_UNSAFE_TEXT.test(decoded) && !decoded.includes("\\") && !decoded.slice(2).startsWith("/");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function allowedSuiteRoute(app: SuiteAppId, value: unknown, max: number = SUITE_LIMITS.href): value is string {
|
|
107
|
+
if (!validSuiteHref(value, max)) return false;
|
|
108
|
+
return SUITE_ROUTES[app].some((route) => {
|
|
109
|
+
const at = route.indexOf("{id}");
|
|
110
|
+
if (at < 0) return value === route;
|
|
111
|
+
const id = value.slice(at);
|
|
112
|
+
return value.startsWith(route.slice(0, at)) && SUITE_OBJECT_ID.test(id) && id !== "." && id !== "..";
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function loopbackHostname(hostname: string): boolean {
|
|
117
|
+
const bare = hostname.replace(/^\[|\]$/g, "");
|
|
118
|
+
if (bare === "::1" || bare === "localhost") return true;
|
|
119
|
+
const parts = bare.split(".");
|
|
120
|
+
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function tailnetOf(hostname: string): string | undefined {
|
|
124
|
+
const labels = hostname.toLowerCase().split(".");
|
|
125
|
+
return labels.length >= 4 && labels.at(-1) === "net" && labels.at(-2) === "ts" ? labels.slice(-3).join(".") : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function safeSuiteOrigin(value: unknown, currentHostname = ""): string | undefined {
|
|
129
|
+
if (typeof value !== "string" || !/^https?:\/\/[^/?#\\\s@*]+$/i.test(value) || value.endsWith(":")) return undefined;
|
|
130
|
+
try {
|
|
131
|
+
const url = new URL(value);
|
|
132
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash || url.port === "0") return undefined;
|
|
133
|
+
if (loopbackHostname(url.hostname)) return url.origin;
|
|
134
|
+
const tailnet = tailnetOf(url.hostname);
|
|
135
|
+
if (url.protocol !== "https:" || !tailnet || tailnet !== tailnetOf(currentHostname)) return undefined;
|
|
136
|
+
return url.origin;
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ReaderHandoffDraft {
|
|
143
|
+
from: SuiteAppId;
|
|
144
|
+
text: string;
|
|
145
|
+
title?: string;
|
|
146
|
+
truncated: boolean;
|
|
147
|
+
backUrl?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cleanText(value: string, max: number): { text: string; truncated: boolean } {
|
|
151
|
+
const cleaned = value.replace(/\r\n?/g, "\n").replace(UNSAFE_HANDOFF, "").trim();
|
|
152
|
+
const chars = [...cleaned];
|
|
153
|
+
return { text: chars.slice(0, max).join(""), truncated: chars.length > max };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function isReaderHandoffAttempt(hash: string): boolean {
|
|
157
|
+
const [path, query = ""] = String(hash).replace(/^#/, "").split("?", 2);
|
|
158
|
+
if (path !== "/read" || !query) return false;
|
|
159
|
+
const params = new URLSearchParams(query);
|
|
160
|
+
return SINGLE_HANDOFF_PARAMS.some((key) => params.has(key));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function parseReaderHandoff(
|
|
164
|
+
hash: string,
|
|
165
|
+
links: SuiteLinks,
|
|
166
|
+
): { present: false } | { present: true; draft?: ReaderHandoffDraft; rejected?: "duplicate" | "missing" | "from" | "empty" } {
|
|
167
|
+
if (!isReaderHandoffAttempt(hash)) return { present: false };
|
|
168
|
+
const [, query = ""] = String(hash).replace(/^#/, "").split("?", 2);
|
|
169
|
+
const params = new URLSearchParams(query);
|
|
170
|
+
if (SINGLE_HANDOFF_PARAMS.some((key) => params.getAll(key).length > 1)) {
|
|
171
|
+
return { present: true, rejected: "duplicate" };
|
|
172
|
+
}
|
|
173
|
+
if (!params.has("text")) return { present: true, rejected: "missing" };
|
|
174
|
+
const from = params.get("from") as SuiteAppId | null;
|
|
175
|
+
if (!from || from === "reader" || !SUITE_APP_IDS.includes(from) || !links[from]) {
|
|
176
|
+
return { present: true, rejected: "from" };
|
|
177
|
+
}
|
|
178
|
+
const body = cleanText(params.get("text") ?? "", SUITE_LIMITS.handoffText);
|
|
179
|
+
if (!body.text) return { present: true, rejected: "empty" };
|
|
180
|
+
const rawTitle = params.get("title");
|
|
181
|
+
const title = rawTitle ? cleanText(rawTitle.replace(/[\r\n\t]/g, " "), SUITE_LIMITS.handoffTitle) : undefined;
|
|
182
|
+
const back = params.get("back");
|
|
183
|
+
const backUrl = back && allowedSuiteRoute(from, back, SUITE_LIMITS.back) ? `${links[from]}/${back}` : undefined;
|
|
184
|
+
return {
|
|
185
|
+
present: true,
|
|
186
|
+
draft: {
|
|
187
|
+
from,
|
|
188
|
+
text: body.text,
|
|
189
|
+
...(title?.text ? { title: title.text } : {}),
|
|
190
|
+
truncated: body.truncated || Boolean(title?.truncated),
|
|
191
|
+
...(backUrl ? { backUrl } : {}),
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function explainDraftHref(
|
|
197
|
+
explainOrigin: string | undefined,
|
|
198
|
+
input: { text: string; title?: string; back?: string },
|
|
199
|
+
currentHostname = "",
|
|
200
|
+
): string | undefined {
|
|
201
|
+
const origin = safeSuiteOrigin(explainOrigin, currentHostname);
|
|
202
|
+
const body = cleanText(input.text, SUITE_LIMITS.handoffText);
|
|
203
|
+
if (!origin || !body.text) return undefined;
|
|
204
|
+
const params = new URLSearchParams({ text: body.text, from: "reader" });
|
|
205
|
+
if (input.title) {
|
|
206
|
+
const cleanTitle = cleanText(input.title.replace(/[\r\n\t]/g, " "), SUITE_LIMITS.handoffTitle);
|
|
207
|
+
if (cleanTitle.text) params.set("title", cleanTitle.text);
|
|
208
|
+
}
|
|
209
|
+
if (input.back && allowedSuiteRoute("reader", input.back, SUITE_LIMITS.back)) {
|
|
210
|
+
params.set("back", input.back);
|
|
211
|
+
}
|
|
212
|
+
return `${origin}/#/explain?${params.toString()}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function personalTaskHref(
|
|
216
|
+
personalOrigin: string | undefined,
|
|
217
|
+
draftText: string,
|
|
218
|
+
back?: string,
|
|
219
|
+
currentHostname = "",
|
|
220
|
+
): string | undefined {
|
|
221
|
+
const origin = safeSuiteOrigin(personalOrigin, currentHostname);
|
|
222
|
+
const body = cleanText(draftText, SUITE_LIMITS.handoffText);
|
|
223
|
+
if (!origin || !body.text) return undefined;
|
|
224
|
+
const params = new URLSearchParams({ draft: body.text, from: "reader" });
|
|
225
|
+
if (back && allowedSuiteRoute("reader", back, SUITE_LIMITS.back)) {
|
|
226
|
+
params.set("back", back);
|
|
227
|
+
}
|
|
228
|
+
return `${origin}/#/tasks?${params.toString()}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function prDraftHref(
|
|
232
|
+
prOrigin: string | undefined,
|
|
233
|
+
input: { draft: string; title?: string; back?: string },
|
|
234
|
+
currentHostname = "",
|
|
235
|
+
): string | undefined {
|
|
236
|
+
const origin = safeSuiteOrigin(prOrigin, currentHostname);
|
|
237
|
+
const body = cleanText(input.draft, SUITE_LIMITS.handoffText);
|
|
238
|
+
if (!origin || !body.text) return undefined;
|
|
239
|
+
const params = new URLSearchParams({ draft: body.text, from: "reader" });
|
|
240
|
+
if (input.title) {
|
|
241
|
+
const cleanTitle = cleanText(input.title.replace(/[\r\n\t]/g, " "), SUITE_LIMITS.handoffTitle);
|
|
242
|
+
if (cleanTitle.text) params.set("title", cleanTitle.text);
|
|
243
|
+
}
|
|
244
|
+
if (input.back && allowedSuiteRoute("reader", input.back, SUITE_LIMITS.back)) {
|
|
245
|
+
params.set("back", input.back);
|
|
246
|
+
}
|
|
247
|
+
return `${origin}/#/discover?${params.toString()}`;
|
|
248
|
+
}
|
package/shared/types.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export type BlockKind = "heading" | "paragraph" | "code" | "quote" | "list" | "image" | "table";
|
|
2
|
+
|
|
3
|
+
export interface ReaderBlock {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: BlockKind;
|
|
6
|
+
text: string;
|
|
7
|
+
level?: 1 | 2 | 3;
|
|
8
|
+
language?: string;
|
|
9
|
+
imageUrl?: string;
|
|
10
|
+
alt?: string;
|
|
11
|
+
tableRows?: string[][];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const INK_STYLES = ["highlight", "underline", "box", "bracket"] as const;
|
|
15
|
+
export type InkStyle = (typeof INK_STYLES)[number];
|
|
16
|
+
|
|
17
|
+
export const INK_COLORS = ["amber", "mint", "violet", "coral", "sky"] as const;
|
|
18
|
+
export type InkColor = (typeof INK_COLORS)[number];
|
|
19
|
+
|
|
20
|
+
export const SKETCH_KINDS = [
|
|
21
|
+
"canvas",
|
|
22
|
+
"cycle",
|
|
23
|
+
"tree",
|
|
24
|
+
"venn",
|
|
25
|
+
"timeline",
|
|
26
|
+
"flow",
|
|
27
|
+
"compare",
|
|
28
|
+
"layers",
|
|
29
|
+
"axes",
|
|
30
|
+
] as const;
|
|
31
|
+
export type SketchKind = (typeof SKETCH_KINDS)[number];
|
|
32
|
+
|
|
33
|
+
export const SKETCH_CURVE_SHAPES = ["rise", "drop", "peak", "threshold"] as const;
|
|
34
|
+
export type SketchCurveShape = (typeof SKETCH_CURVE_SHAPES)[number];
|
|
35
|
+
|
|
36
|
+
export type SketchElement =
|
|
37
|
+
| { type: "rect"; x: number; y: number; w: number; h: number; fill?: boolean; dashed?: boolean; label?: string }
|
|
38
|
+
| { type: "ellipse"; x: number; y: number; rx: number; ry: number; fill?: boolean; dashed?: boolean; label?: string }
|
|
39
|
+
| { type: "arrow"; x1: number; y1: number; x2: number; y2: number; bend?: number; dashed?: boolean; label?: string }
|
|
40
|
+
| { type: "line"; x1: number; y1: number; x2: number; y2: number; dashed?: boolean; label?: string }
|
|
41
|
+
| { type: "curve"; points: Array<[number, number]>; fill?: boolean; dashed?: boolean; closed?: boolean; label?: string }
|
|
42
|
+
| { type: "dot"; x: number; y: number; label?: string }
|
|
43
|
+
| { type: "text"; x: number; y: number; text: string; size?: "sm" | "md" | "lg"; align?: "start" | "middle" | "end" };
|
|
44
|
+
|
|
45
|
+
export interface AnnotationSketch {
|
|
46
|
+
kind: SketchKind;
|
|
47
|
+
caption?: string;
|
|
48
|
+
nodes: string[];
|
|
49
|
+
shape?: SketchCurveShape;
|
|
50
|
+
elements?: SketchElement[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PageAnnotation {
|
|
54
|
+
id: string;
|
|
55
|
+
pageId: string;
|
|
56
|
+
blockId: string;
|
|
57
|
+
quote: string;
|
|
58
|
+
translatedQuote?: string;
|
|
59
|
+
startOffset: number;
|
|
60
|
+
endOffset: number;
|
|
61
|
+
style: InkStyle;
|
|
62
|
+
color: InkColor;
|
|
63
|
+
label?: string;
|
|
64
|
+
note: string;
|
|
65
|
+
topic?: string;
|
|
66
|
+
anchorX?: number;
|
|
67
|
+
anchorY?: number;
|
|
68
|
+
sketch?: AnnotationSketch;
|
|
69
|
+
source: "ai" | "user";
|
|
70
|
+
createdAt: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Folder {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
color: InkColor;
|
|
77
|
+
pageCount: number;
|
|
78
|
+
createdAt: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SavedPageSummary {
|
|
82
|
+
id: string;
|
|
83
|
+
sourceKind: "url" | "paste";
|
|
84
|
+
url?: string;
|
|
85
|
+
siteName?: string;
|
|
86
|
+
title: string;
|
|
87
|
+
excerpt: string;
|
|
88
|
+
topic: string;
|
|
89
|
+
tags: string[];
|
|
90
|
+
summary?: string;
|
|
91
|
+
folderId: string | null;
|
|
92
|
+
annotationCount: number;
|
|
93
|
+
annotatedAt?: string;
|
|
94
|
+
translatedAt?: string;
|
|
95
|
+
createdAt: string;
|
|
96
|
+
updatedAt: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SavedPage extends SavedPageSummary {
|
|
100
|
+
blocks: ReaderBlock[];
|
|
101
|
+
translatedTitle?: string;
|
|
102
|
+
translatedBlocks?: ReaderBlock[];
|
|
103
|
+
annotations: PageAnnotation[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ExplainTopicMatch {
|
|
107
|
+
id: string;
|
|
108
|
+
title: string;
|
|
109
|
+
category?: string;
|
|
110
|
+
tags: string[];
|
|
111
|
+
href: string;
|
|
112
|
+
reason: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ExplainBridgeStatus {
|
|
116
|
+
status: "ready" | "stale" | "unavailable" | "unconfigured";
|
|
117
|
+
browserOrigin?: string;
|
|
118
|
+
matches: ExplainTopicMatch[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const AI_PROVIDERS = ["codex", "claude", "antigravity", "api"] as const;
|
|
122
|
+
export type AIProviderKind = (typeof AI_PROVIDERS)[number];
|
|
123
|
+
|
|
124
|
+
export const ANNOTATION_DENSITIES = ["sparse", "balanced", "thorough"] as const;
|
|
125
|
+
export type AnnotationDensity = (typeof ANNOTATION_DENSITIES)[number];
|
|
126
|
+
|
|
127
|
+
export interface ProviderSettings {
|
|
128
|
+
provider: AIProviderKind;
|
|
129
|
+
model: string;
|
|
130
|
+
autoFolder: boolean;
|
|
131
|
+
annotationDensity?: AnnotationDensity;
|
|
132
|
+
}
|