@branch-fiction/extension-sdk 0.1.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 +18 -0
- package/dist/db/boolean-plugin.d.ts +10 -0
- package/dist/db/boolean-plugin.js +24 -0
- package/dist/db/boolean-plugin.js.map +1 -0
- package/dist/db/iframe.d.ts +15 -0
- package/dist/db/iframe.js +78 -0
- package/dist/db/iframe.js.map +1 -0
- package/dist/db/types.d.ts +247 -0
- package/dist/db/types.js +1 -0
- package/dist/dev-cli.d.ts +1 -0
- package/dist/dev-cli.js +297 -0
- package/dist/dev-cli.js.map +1 -0
- package/dist/dev.d.ts +46 -0
- package/dist/dev.js +2 -0
- package/dist/extension-host.bundle.js +122607 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/manifest-CQaa55kR.mjs +200 -0
- package/dist/manifest-CQaa55kR.mjs.map +1 -0
- package/dist/manifest.d.ts +97 -0
- package/dist/manifest.js +2 -0
- package/dist/models-catalog.d.ts +12 -0
- package/dist/models-catalog.js +58 -0
- package/dist/models-catalog.js.map +1 -0
- package/dist/pi-handle.d.ts +26 -0
- package/dist/pi-handle.js +108 -0
- package/dist/pi-handle.js.map +1 -0
- package/dist/sdk-source.d.ts +69 -0
- package/dist/sdk-source.js +205 -0
- package/dist/sdk-source.js.map +1 -0
- package/dist/server-BcwliPFy.mjs +752 -0
- package/dist/server-BcwliPFy.mjs.map +1 -0
- package/dist/types-ZCFYu2MY.d.mts +23 -0
- package/dist/vite.d.ts +53 -0
- package/dist/vite.js +83 -0
- package/dist/vite.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import { c as optionURL, o as isUseSlotRequirement, u as validateManifest } from "./manifest-CQaa55kR.mjs";
|
|
2
|
+
import { extensionSdkSource } from "./sdk-source.js";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { Hono } from "hono";
|
|
7
|
+
import { cors } from "hono/cors";
|
|
8
|
+
import { streamSSE } from "hono/streaming";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
//#region src/dev/config.ts
|
|
13
|
+
function readDevConfig(path) {
|
|
14
|
+
if (!existsSync(path)) return {};
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeDevConfig(path, config) {
|
|
22
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
|
|
23
|
+
}
|
|
24
|
+
function checkDevConfig(manifest, config) {
|
|
25
|
+
const missing = [];
|
|
26
|
+
const reqs = manifest.providers ?? [];
|
|
27
|
+
for (const req of reqs) {
|
|
28
|
+
const binding = config.providers?.[req.key];
|
|
29
|
+
if (!binding) {
|
|
30
|
+
missing.push(`providers.${req.key}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (isUseSlotRequirement(req)) {
|
|
34
|
+
if (binding.kind !== "useSlot") {
|
|
35
|
+
missing.push(`providers.${req.key} (expected useSlot binding)`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!binding.providerType || !binding.modelKey || !binding.baseURL) missing.push(`providers.${req.key} (incomplete useSlot binding)`);
|
|
39
|
+
if (binding.auth?.kind !== "none" && !binding.apiKey) missing.push(`providers.${req.key}.apiKey`);
|
|
40
|
+
} else {
|
|
41
|
+
if (binding.kind !== "options") {
|
|
42
|
+
missing.push(`providers.${req.key} (expected options binding)`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const opt = req.options[binding.useIndex];
|
|
46
|
+
if (!opt) {
|
|
47
|
+
missing.push(`providers.${req.key}.useIndex out of range`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (opt.auth.kind !== "none" && !binding.apiKey) missing.push(`providers.${req.key}.apiKey`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
ok: missing.length === 0,
|
|
55
|
+
missing
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/dev/db.ts
|
|
60
|
+
let cached = null;
|
|
61
|
+
function open(path) {
|
|
62
|
+
if (cached && cached.path === path) return cached.db;
|
|
63
|
+
if (cached) cached.db.close();
|
|
64
|
+
const db = new Database(path);
|
|
65
|
+
db.pragma("journal_mode = WAL");
|
|
66
|
+
db.pragma("busy_timeout = 5000");
|
|
67
|
+
db.pragma("foreign_keys = ON");
|
|
68
|
+
cached = {
|
|
69
|
+
path,
|
|
70
|
+
db
|
|
71
|
+
};
|
|
72
|
+
return db;
|
|
73
|
+
}
|
|
74
|
+
function dbQuery(dbPath, req) {
|
|
75
|
+
const db = open(dbPath);
|
|
76
|
+
const trimmed = req.sql.trimStart().toLowerCase();
|
|
77
|
+
const isQuery = trimmed.startsWith("select") || trimmed.startsWith("with") || trimmed.startsWith("pragma") || trimmed.startsWith("explain");
|
|
78
|
+
const stmt = db.prepare(req.sql);
|
|
79
|
+
if (isQuery) return {
|
|
80
|
+
rows: stmt.all(...req.params ?? []),
|
|
81
|
+
changes: 0
|
|
82
|
+
};
|
|
83
|
+
const info = stmt.run(...req.params ?? []);
|
|
84
|
+
return {
|
|
85
|
+
rows: [],
|
|
86
|
+
changes: Number(info.changes ?? 0)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/dev/fs.ts
|
|
91
|
+
function safeJoin(root, rel) {
|
|
92
|
+
if (rel.startsWith("/") || isAbsolute(rel)) throw new Error(`absolute path not allowed: ${rel}`);
|
|
93
|
+
const joined = normalize(join(root, rel));
|
|
94
|
+
const inside = relative(root, joined);
|
|
95
|
+
if (inside.startsWith("..") || isAbsolute(inside)) throw new Error(`path escapes assets dir: ${rel}`);
|
|
96
|
+
return joined;
|
|
97
|
+
}
|
|
98
|
+
function fsRead(assetsDir, relPath) {
|
|
99
|
+
return { bytesBase64: readFileSync(safeJoin(assetsDir, relPath)).toString("base64") };
|
|
100
|
+
}
|
|
101
|
+
function fsWrite(assetsDir, relPath, bytesBase64) {
|
|
102
|
+
const path = safeJoin(assetsDir, relPath);
|
|
103
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
104
|
+
writeFileSync(path, Buffer.from(bytesBase64, "base64"));
|
|
105
|
+
return { ok: true };
|
|
106
|
+
}
|
|
107
|
+
function fsList(assetsDir, relPath) {
|
|
108
|
+
const path = safeJoin(assetsDir, relPath ?? "");
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = readdirSync(path);
|
|
112
|
+
} catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return entries.map((name) => {
|
|
116
|
+
let isDirectory = false;
|
|
117
|
+
try {
|
|
118
|
+
isDirectory = statSync(join(path, name)).isDirectory();
|
|
119
|
+
} catch {}
|
|
120
|
+
return {
|
|
121
|
+
name,
|
|
122
|
+
isDirectory
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/dev/proxy.ts
|
|
128
|
+
const STRIP_HEADERS = new Set([
|
|
129
|
+
"host",
|
|
130
|
+
"authorization",
|
|
131
|
+
"x-goog-api-key",
|
|
132
|
+
"content-length",
|
|
133
|
+
"connection"
|
|
134
|
+
]);
|
|
135
|
+
async function proxyToProvider(c, label, resolved, rest) {
|
|
136
|
+
const target = buildTargetUrl(resolved.baseURL, rest, c.req.url);
|
|
137
|
+
const outbound = sanitizeHeaders(c.req.raw.headers, resolved);
|
|
138
|
+
if (resolved.apiKey) applyAuth(resolved, outbound, target);
|
|
139
|
+
const init = {
|
|
140
|
+
method: c.req.method,
|
|
141
|
+
headers: outbound,
|
|
142
|
+
redirect: "manual"
|
|
143
|
+
};
|
|
144
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") init.body = await c.req.raw.arrayBuffer();
|
|
145
|
+
if (resolved.auth.kind === "body") {
|
|
146
|
+
const rewrite = applyBodyAuth(resolved.auth.field, resolved.apiKey ?? "", c.req.method, outbound, init.body);
|
|
147
|
+
if (!rewrite.ok) return new Response(rewrite.error, { status: 400 });
|
|
148
|
+
init.body = rewrite.body;
|
|
149
|
+
}
|
|
150
|
+
let upstream;
|
|
151
|
+
try {
|
|
152
|
+
upstream = await fetch(target, init);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`[proxy] ${label} ${c.req.method} ${target} -> send error:`, e);
|
|
155
|
+
throw e;
|
|
156
|
+
}
|
|
157
|
+
console.log(`[proxy] ${label} ${c.req.method} ${target} -> ${upstream.status}`);
|
|
158
|
+
const headers = new Headers(upstream.headers);
|
|
159
|
+
headers.delete("content-length");
|
|
160
|
+
headers.delete("content-encoding");
|
|
161
|
+
headers.delete("transfer-encoding");
|
|
162
|
+
return new Response(upstream.body, {
|
|
163
|
+
status: upstream.status,
|
|
164
|
+
headers
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function buildTargetUrl(baseURL, rest, reqUrl) {
|
|
168
|
+
const trimmedBase = baseURL.replace(/\/+$/, "");
|
|
169
|
+
const trimmedRest = rest.replace(/^\/+/, "");
|
|
170
|
+
const joined = trimmedRest ? `${trimmedBase}/${trimmedRest}` : trimmedBase;
|
|
171
|
+
const url = new URL(joined);
|
|
172
|
+
const callerQuery = new URL(reqUrl).search;
|
|
173
|
+
if (callerQuery) {
|
|
174
|
+
const callerParams = new URLSearchParams(callerQuery);
|
|
175
|
+
for (const [k, v] of callerParams) url.searchParams.append(k, v);
|
|
176
|
+
}
|
|
177
|
+
return url;
|
|
178
|
+
}
|
|
179
|
+
function sanitizeHeaders(incoming, resolved) {
|
|
180
|
+
const out = new Headers();
|
|
181
|
+
const stripCustom = resolved.auth.kind === "header" ? resolved.auth.header.toLowerCase() : null;
|
|
182
|
+
for (const [name, value] of incoming) {
|
|
183
|
+
const n = name.toLowerCase();
|
|
184
|
+
if (STRIP_HEADERS.has(n)) continue;
|
|
185
|
+
if (stripCustom && n === stripCustom) continue;
|
|
186
|
+
out.append(name, value);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function applyAuth(resolved, headers, url) {
|
|
191
|
+
const apiKey = resolved.apiKey ?? "";
|
|
192
|
+
switch (resolved.auth.kind) {
|
|
193
|
+
case "none": return;
|
|
194
|
+
case "bearer":
|
|
195
|
+
headers.set("authorization", `Bearer ${apiKey}`);
|
|
196
|
+
return;
|
|
197
|
+
case "header":
|
|
198
|
+
headers.set(resolved.auth.header, apiKey);
|
|
199
|
+
return;
|
|
200
|
+
case "queryParam":
|
|
201
|
+
url.searchParams.set(resolved.auth.param, apiKey);
|
|
202
|
+
return;
|
|
203
|
+
case "body": return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function applyBodyAuth(field, apiKey, method, headers, body) {
|
|
207
|
+
if (method === "GET" || method === "HEAD" || !body || body.byteLength === 0) return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error: "body auth requires a JSON request body"
|
|
210
|
+
};
|
|
211
|
+
const contentType = headers.get("content-type") ?? "";
|
|
212
|
+
if (contentType && !/^application\/json\b/i.test(contentType)) return {
|
|
213
|
+
ok: false,
|
|
214
|
+
error: "body auth requires application/json content-type"
|
|
215
|
+
};
|
|
216
|
+
let parsed;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(new TextDecoder().decode(body));
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
error: `body auth: invalid json: ${e.message}`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
|
|
226
|
+
ok: false,
|
|
227
|
+
error: "body auth: JSON root must be an object"
|
|
228
|
+
};
|
|
229
|
+
parsed[field] = apiKey;
|
|
230
|
+
headers.set("content-type", "application/json");
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
body: JSON.stringify(parsed)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region \0@oxc-project+runtime@0.135.0/helpers/esm/taggedTemplateLiteral.js
|
|
238
|
+
function _taggedTemplateLiteral(e, t) {
|
|
239
|
+
return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } }));
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/dev/setup-ui.ts
|
|
243
|
+
var _templateObject;
|
|
244
|
+
const SETUP_UI_HTML = String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<title>Extension dev setup</title>\n<style>\n :root {\n color-scheme: dark light;\n --fg: #1a1a1a;\n --bg: #fafafa;\n --muted: #707070;\n --border: #ddd;\n --primary: #2658d3;\n --warn: #b04a00;\n --ok: #1a7a3a;\n }\n @media (prefers-color-scheme: dark) {\n :root {\n --fg: #eee;\n --bg: #18181a;\n --muted: #999;\n --border: #333;\n --primary: #6691ff;\n }\n }\n * { box-sizing: border-box; }\n body {\n font: 14px/1.45 system-ui, -apple-system, sans-serif;\n margin: 0; padding: 32px 24px; max-width: 760px; margin-inline: auto;\n color: var(--fg); background: var(--bg);\n }\n h1 { font-size: 22px; margin: 0 0 4px; }\n h2 { font-size: 16px; margin: 24px 0 8px; }\n p.sub { color: var(--muted); margin: 0 0 24px; }\n .card { border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin: 12px 0; }\n .row { display: grid; grid-template-columns: 160px 1fr; gap: 12px; margin: 8px 0; align-items: center; }\n label { color: var(--muted); }\n input, select, textarea {\n font: inherit; padding: 6px 8px; border: 1px solid var(--border);\n border-radius: 4px; background: var(--bg); color: var(--fg); width: 100%;\n }\n input[type=checkbox] { width: auto; }\n button {\n font: inherit; padding: 8px 14px; border-radius: 4px; border: 1px solid var(--primary);\n background: var(--primary); color: white; cursor: pointer;\n }\n button.secondary { background: transparent; color: var(--primary); }\n button:disabled, button.secondary:disabled {\n background: transparent; color: var(--muted); border-color: var(--border);\n cursor: not-allowed; opacity: 0.7;\n }\n .small { font-size: 12px; color: var(--muted); }\n code { font: 12px/1.4 ui-monospace, \"SF Mono\", Menlo, monospace; padding: 1px 4px;\n background: var(--border); border-radius: 3px; }\n .actions { display: flex; gap: 8px; margin-top: 24px; }\n .err { color: var(--warn); margin: 8px 0; }\n</style>\n</head>\n<body>\n<h1>Extension dev setup</h1>\n<p class=\"sub\">Configure provider bindings + pick a book. Saved to <code>dev.config.json</code> in your extension dir.</p>\n<div id=\"root\">Loading…</div>\n<script type=\"module\">\nconst root = document.getElementById('root');\n\nasync function fetchStatus() {\n const r = await fetch('/__dev__/api/status');\n if (!r.ok) throw new Error('status: ' + r.status);\n return r.json();\n}\n\nfunction inputRow(label, value, onInput, opts = {}) {\n const wrap = document.createElement('div');\n wrap.className = 'row';\n const lab = document.createElement('label');\n lab.textContent = label;\n const input = document.createElement('input');\n input.type = opts.type ?? 'text';\n if (opts.placeholder) input.placeholder = opts.placeholder;\n input.value = value ?? '';\n input.addEventListener('input', () => onInput(input.value));\n wrap.append(lab, input);\n return wrap;\n}\n\nfunction selectRow(label, value, options, onChange) {\n const wrap = document.createElement('div');\n wrap.className = 'row';\n const lab = document.createElement('label');\n lab.textContent = label;\n const sel = document.createElement('select');\n for (const opt of options) {\n const o = document.createElement('option');\n o.value = opt.value;\n o.textContent = opt.label;\n if (opt.value === value) o.selected = true;\n sel.appendChild(o);\n }\n sel.addEventListener('change', () => onChange(sel.value));\n wrap.append(lab, sel);\n return wrap;\n}\n\nconst PROVIDER_TYPES = [\n { value: 'google_gemini', label: 'Google Gemini', baseURL: 'https://generativelanguage.googleapis.com/v1beta', auth: { kind: 'header', header: 'x-goog-api-key' } },\n { value: 'openai', label: 'OpenAI', baseURL: 'https://api.openai.com/v1', auth: { kind: 'bearer' } },\n { value: 'anthropic', label: 'Anthropic', baseURL: 'https://api.anthropic.com', auth: { kind: 'header', header: 'x-api-key' } },\n { value: 'openrouter', label: 'OpenRouter', baseURL: 'https://openrouter.ai/api/v1', auth: { kind: 'bearer' } },\n { value: 'xai', label: 'xAI', baseURL: 'https://api.x.ai/v1', auth: { kind: 'bearer' } },\n { value: 'fal', label: 'Fal', baseURL: 'https://fal.run', auth: { kind: 'bearer', headerPrefix: 'Key' } },\n { value: 'ollama', label: 'Ollama', baseURL: 'http://localhost:11434', auth: { kind: 'none' } },\n { value: 'openai_compatible', label: 'OpenAI Compatible', baseURL: '', auth: { kind: 'bearer' } }\n];\n\nfunction defaultBindingForReq(req, existing) {\n if ('useSlot' in req) {\n const e = existing && existing.kind === 'useSlot' ? existing : null;\n const preset = PROVIDER_TYPES[0];\n return {\n kind: 'useSlot',\n providerType: e?.providerType ?? preset.value,\n modelKey: e?.modelKey ?? '',\n baseURL: e?.baseURL ?? preset.baseURL,\n auth: e?.auth ?? preset.auth,\n apiKey: e?.apiKey ?? '',\n reasoning: e?.reasoning\n };\n }\n const e = existing && existing.kind === 'options' ? existing : null;\n return {\n kind: 'options',\n useIndex: e?.useIndex ?? 0,\n fullURL: e?.fullURL,\n apiKey: e?.apiKey ?? ''\n };\n}\n\nfunction renderRequirement(req, binding, onChange) {\n const card = document.createElement('div');\n card.className = 'card';\n const h = document.createElement('h2');\n h.textContent = req.role ? req.role + ' (' + req.key + ')' : req.key;\n card.appendChild(h);\n\n if ('useSlot' in req) {\n const small = document.createElement('p');\n small.className = 'small';\n small.textContent = 'useSlot: ' + req.useSlot + ' — pick any provider+model that fits this slot.';\n card.appendChild(small);\n\n card.appendChild(selectRow('Provider type', binding.providerType,\n PROVIDER_TYPES.map((p) => ({ value: p.value, label: p.label })),\n (v) => {\n const preset = PROVIDER_TYPES.find((p) => p.value === v);\n binding.providerType = v;\n if (preset) {\n binding.baseURL = preset.baseURL;\n binding.auth = preset.auth;\n }\n onChange();\n }));\n card.appendChild(inputRow('Model id', binding.modelKey, (v) => { binding.modelKey = v; onChange(); }, { placeholder: 'e.g. gemini-2.5-flash' }));\n card.appendChild(inputRow('Base URL', binding.baseURL, (v) => { binding.baseURL = v; onChange(); }));\n card.appendChild(inputRow('API key', binding.apiKey, (v) => { binding.apiKey = v; onChange(); }, { type: 'password' }));\n } else {\n card.appendChild(selectRow('Option', String(binding.useIndex),\n req.options.map((o, i) => ({ value: String(i), label: (o.providerName ?? '') + ' ' + (o.baseURL ?? o.fullURL) + (o.model ? ' / ' + o.model : '') })),\n (v) => { binding.useIndex = Number(v); onChange(); }));\n const opt = req.options[binding.useIndex];\n if (opt && 'fullURL' in opt && opt.fullURL) {\n card.appendChild(inputRow('Endpoint URL', binding.fullURL ?? opt.fullURL, (v) => { binding.fullURL = v; onChange(); }));\n }\n if (opt && opt.auth.kind !== 'none') {\n card.appendChild(inputRow('API key', binding.apiKey, (v) => { binding.apiKey = v; onChange(); }, { type: 'password' }));\n }\n }\n return card;\n}\n\nfunction renderConfigField(field, value, onChange) {\n const wrap = document.createElement('div');\n if (field.type === 'boolean') {\n wrap.className = 'row';\n const lab = document.createElement('label');\n lab.textContent = field.label;\n const cb = document.createElement('input');\n cb.type = 'checkbox';\n cb.checked = !!value;\n cb.addEventListener('change', () => onChange(cb.checked));\n wrap.append(lab, cb);\n } else if (field.type === 'select') {\n wrap.appendChild(selectRow(field.label, String(value ?? field.default ?? ''),\n field.options.map((o) => ({ value: o.value, label: o.label })),\n (v) => onChange(v)));\n } else {\n wrap.appendChild(inputRow(field.label, value ?? field.default ?? '', (v) => onChange(v), { placeholder: field.placeholder }));\n }\n return wrap;\n}\n\nlet manifest, config;\nlet books = [];\n\nasync function saveConfig() {\n const r = await fetch('/__dev__/api/save', {\n method: 'POST', headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(config)\n });\n if (!r.ok) throw new Error(await r.text());\n}\n\nasync function init() {\n let status;\n try {\n status = await fetchStatus();\n } catch (e) {\n root.innerHTML = '<div class=\"err\">Failed to load status: ' + e.message + '</div>';\n return;\n }\n\n manifest = status.manifest;\n config = status.config ?? {};\n config.providers ??= {};\n config.config ??= {};\n books = status.books ?? [];\n\n for (const req of manifest.providers ?? []) {\n config.providers[req.key] = defaultBindingForReq(req, config.providers[req.key]);\n }\n\n const ready = status.ok && books.length > 0 && !!config.bookId\n && books.some((b) => b.id === config.bookId);\n\n // Auto-launch when Vite redirected us here (?auto=1) and there's nothing\n // left to configure. Manual visits always show the wizard.\n const params = new URLSearchParams(window.location.search);\n if (params.has('auto') && ready) {\n root.innerHTML = '<p class=\"small\">Launching…</p>';\n try {\n const r = await fetch('/__dev__/api/launch-url');\n const j = await r.json().catch(() => ({}));\n if (r.ok && j.ok !== false && j.url) {\n window.location.replace(j.url);\n return;\n }\n console.warn('[extension-dev] auto-launch failed:', j.error ?? r.statusText);\n } catch (e) {\n console.warn('[extension-dev] auto-launch failed:', e);\n }\n }\n\n render();\n}\n\nlet darkMode = false;\n\nfunction render() {\n root.innerHTML = '';\n\n if ((manifest.providers ?? []).length > 0) {\n const head = document.createElement('h2');\n head.textContent = 'Provider requirements';\n root.appendChild(head);\n for (const req of manifest.providers ?? []) {\n root.appendChild(renderRequirement(req, config.providers[req.key], () => undefined));\n }\n }\n\n if ((manifest.config ?? []).length > 0) {\n const head = document.createElement('h2');\n head.textContent = 'Extension config';\n root.appendChild(head);\n const card = document.createElement('div');\n card.className = 'card';\n for (const field of manifest.config) {\n card.appendChild(renderConfigField(field, config.config[field.key], (v) => {\n config.config[field.key] = v;\n }));\n }\n root.appendChild(card);\n }\n\n const bookHead = document.createElement('h2');\n bookHead.textContent = 'Book context';\n const bookCard = document.createElement('div');\n bookCard.className = 'card';\n if (books.length === 0) {\n bookCard.textContent = 'No books found in your app DB. Import a book in Branch Fiction first, then reload.';\n } else {\n if (!config.bookId || !books.some((b) => b.id === config.bookId)) {\n config.bookId = books[0].id;\n }\n bookCard.appendChild(selectRow('Book', config.bookId,\n books.map((b) => ({ value: b.id, label: b.title + ' (' + b.id + ')' })),\n (v) => { config.bookId = v; }));\n }\n root.append(bookHead, bookCard);\n\n const darkRow = document.createElement('div');\n darkRow.className = 'row';\n const darkLab = document.createElement('label');\n darkLab.style.display = 'flex';\n darkLab.style.alignItems = 'center';\n darkLab.style.gap = '8px';\n darkLab.style.cursor = 'pointer';\n const darkCb = document.createElement('input');\n darkCb.type = 'checkbox';\n darkCb.checked = darkMode;\n darkCb.addEventListener('change', () => { darkMode = darkCb.checked; });\n darkLab.append(darkCb, document.createTextNode('Dark mode?'));\n darkRow.appendChild(darkLab);\n root.appendChild(darkRow);\n\n const actions = document.createElement('div');\n actions.className = 'actions';\n const launch = document.createElement('button');\n launch.textContent = 'Save & launch';\n launch.disabled = books.length === 0;\n actions.append(launch);\n root.appendChild(actions);\n\n const err = document.createElement('div');\n err.className = 'err';\n root.appendChild(err);\n\n launch.addEventListener('click', async () => {\n err.textContent = '';\n if (!config.bookId) {\n err.textContent = 'Pick a book before launching.';\n return;\n }\n try {\n await saveConfig();\n } catch (e) {\n err.textContent = 'Save failed: ' + e.message;\n return;\n }\n const r = await fetch('/__dev__/api/launch-url');\n const j = await r.json().catch(() => ({}));\n if (!r.ok || j.ok === false) {\n err.textContent = 'Launch failed: ' + (j.error ?? r.statusText);\n return;\n }\n const u = new URL(j.url, window.location.href);\n if (darkMode) u.searchParams.set('dark', '1');\n window.location.href = u.toString();\n });\n}\n\nvoid init();\n<\/script>\n</body>\n</html>"])));
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/dev/tasks.ts
|
|
247
|
+
function spawnWorker(args) {
|
|
248
|
+
const taskId = `ptk_${randomUUID().replace(/-/g, "")}`;
|
|
249
|
+
const ac = new AbortController();
|
|
250
|
+
args.controllers.set(taskId, ac);
|
|
251
|
+
const allowRead = [
|
|
252
|
+
args.extensionDataRoot,
|
|
253
|
+
args.extensionDir,
|
|
254
|
+
dirname$1(args.extensionHostBundle)
|
|
255
|
+
].filter(Boolean).join(",");
|
|
256
|
+
const allowWrite = args.extensionDataRoot;
|
|
257
|
+
const allowNet = [
|
|
258
|
+
`127.0.0.1:${args.hostPort}`,
|
|
259
|
+
`localhost:${args.hostPort}`,
|
|
260
|
+
...args.netAllowlist
|
|
261
|
+
].join(",");
|
|
262
|
+
const denoArgs = [
|
|
263
|
+
"run",
|
|
264
|
+
"--no-config",
|
|
265
|
+
`--allow-read=${allowRead}`,
|
|
266
|
+
`--allow-write=${allowWrite}`,
|
|
267
|
+
`--allow-net=${allowNet}`,
|
|
268
|
+
args.extensionHostBundle
|
|
269
|
+
];
|
|
270
|
+
return {
|
|
271
|
+
taskId,
|
|
272
|
+
events: pump(spawn(args.denoBin, denoArgs, {
|
|
273
|
+
stdio: [
|
|
274
|
+
"pipe",
|
|
275
|
+
"pipe",
|
|
276
|
+
"pipe"
|
|
277
|
+
],
|
|
278
|
+
signal: ac.signal
|
|
279
|
+
}), args, ac, args.controllers, taskId),
|
|
280
|
+
cancel: () => ac.abort()
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function dirname$1(p) {
|
|
284
|
+
const idx = p.lastIndexOf("/");
|
|
285
|
+
return idx === -1 ? p : p.slice(0, idx);
|
|
286
|
+
}
|
|
287
|
+
const INIT_REQ_ID = 1;
|
|
288
|
+
const RUN_REQ_ID = 2;
|
|
289
|
+
async function* pump(child, args, _ac, controllers, taskId) {
|
|
290
|
+
const buffer = [];
|
|
291
|
+
let resolve = null;
|
|
292
|
+
let done = false;
|
|
293
|
+
let error = null;
|
|
294
|
+
const onErr = (e) => {
|
|
295
|
+
if (e.name === "AbortError") return;
|
|
296
|
+
error = e;
|
|
297
|
+
done = true;
|
|
298
|
+
resolve?.();
|
|
299
|
+
};
|
|
300
|
+
const initReq = JSON.stringify({
|
|
301
|
+
jsonrpc: "2.0",
|
|
302
|
+
id: INIT_REQ_ID,
|
|
303
|
+
method: "init",
|
|
304
|
+
params: [{
|
|
305
|
+
extensionId: args.extensionId,
|
|
306
|
+
bookId: args.bookId,
|
|
307
|
+
providers: serializableProviders(args.providers),
|
|
308
|
+
config: args.config,
|
|
309
|
+
dbPath: args.dbPath,
|
|
310
|
+
dataDir: args.assetsDir,
|
|
311
|
+
extensionWorkerPath: `${args.extensionDir.replace(/\/+$/, "")}/${args.workerEntry.replace(/^\.?\/+/, "")}`
|
|
312
|
+
}]
|
|
313
|
+
}) + "\n";
|
|
314
|
+
child.stdin?.write(initReq);
|
|
315
|
+
if (child.stdout) createInterface({ input: child.stdout }).on("line", (line) => {
|
|
316
|
+
const trimmed = line.trim();
|
|
317
|
+
if (!trimmed) return;
|
|
318
|
+
let msg;
|
|
319
|
+
try {
|
|
320
|
+
msg = JSON.parse(trimmed);
|
|
321
|
+
} catch {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const id = typeof msg.id === "number" ? msg.id : null;
|
|
325
|
+
if (id === null) {
|
|
326
|
+
if (msg.method === "host.log") {
|
|
327
|
+
const params = msg.params ?? {};
|
|
328
|
+
buffer.push({
|
|
329
|
+
kind: "log",
|
|
330
|
+
args: params.args ?? []
|
|
331
|
+
});
|
|
332
|
+
resolve?.();
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (id === INIT_REQ_ID) {
|
|
337
|
+
if (msg.error) {
|
|
338
|
+
buffer.push({
|
|
339
|
+
kind: "error",
|
|
340
|
+
message: errorMessage(msg.error) || "init failed"
|
|
341
|
+
});
|
|
342
|
+
done = true;
|
|
343
|
+
resolve?.();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const runReq = JSON.stringify({
|
|
347
|
+
jsonrpc: "2.0",
|
|
348
|
+
id: RUN_REQ_ID,
|
|
349
|
+
method: "runTask",
|
|
350
|
+
params: [{
|
|
351
|
+
task: args.task,
|
|
352
|
+
payload: args.payload
|
|
353
|
+
}]
|
|
354
|
+
}) + "\n";
|
|
355
|
+
child.stdin?.write(runReq);
|
|
356
|
+
} else if (id === RUN_REQ_ID) {
|
|
357
|
+
if (msg.error) buffer.push({
|
|
358
|
+
kind: "error",
|
|
359
|
+
message: errorMessage(msg.error)
|
|
360
|
+
});
|
|
361
|
+
else {
|
|
362
|
+
const result = msg.result?.result ?? null;
|
|
363
|
+
buffer.push({
|
|
364
|
+
kind: "result",
|
|
365
|
+
value: result
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
done = true;
|
|
369
|
+
resolve?.();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (child.stderr) child.stderr.on("data", (chunk) => {
|
|
373
|
+
const text = chunk.toString("utf8").trimEnd();
|
|
374
|
+
if (text) console.error(`[extension-host:${args.extensionId}]`, text);
|
|
375
|
+
});
|
|
376
|
+
child.on("error", onErr);
|
|
377
|
+
child.on("exit", () => {
|
|
378
|
+
if (!done) {
|
|
379
|
+
buffer.push({
|
|
380
|
+
kind: "error",
|
|
381
|
+
message: "extension worker exited without returning a result"
|
|
382
|
+
});
|
|
383
|
+
done = true;
|
|
384
|
+
}
|
|
385
|
+
resolve?.();
|
|
386
|
+
});
|
|
387
|
+
try {
|
|
388
|
+
while (true) {
|
|
389
|
+
while (buffer.length > 0) {
|
|
390
|
+
const ev = buffer.shift();
|
|
391
|
+
yield ev;
|
|
392
|
+
if (ev.kind === "result" || ev.kind === "error") return;
|
|
393
|
+
}
|
|
394
|
+
if (done) {
|
|
395
|
+
if (error) throw error;
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await new Promise((r) => {
|
|
399
|
+
resolve = r;
|
|
400
|
+
});
|
|
401
|
+
resolve = null;
|
|
402
|
+
}
|
|
403
|
+
} finally {
|
|
404
|
+
controllers.delete(taskId);
|
|
405
|
+
if (!child.killed) try {
|
|
406
|
+
child.kill();
|
|
407
|
+
} catch {}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function serializableProviders(providers) {
|
|
411
|
+
const out = {};
|
|
412
|
+
for (const [k, v] of Object.entries(providers)) out[k] = { ...v };
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
function errorMessage(err) {
|
|
416
|
+
if (err && typeof err === "object" && "message" in err) return String(err.message);
|
|
417
|
+
return "unknown error";
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/dev/tokens.ts
|
|
421
|
+
var TokenRegistry = class {
|
|
422
|
+
dataToken = null;
|
|
423
|
+
proxyTokens = /* @__PURE__ */ new Map();
|
|
424
|
+
tokenByKey = /* @__PURE__ */ new Map();
|
|
425
|
+
resolved = /* @__PURE__ */ new Map();
|
|
426
|
+
handles = {};
|
|
427
|
+
mintDataToken() {
|
|
428
|
+
if (!this.dataToken) this.dataToken = `pdt_${randomUUID().replace(/-/g, "")}`;
|
|
429
|
+
return this.dataToken;
|
|
430
|
+
}
|
|
431
|
+
getDataToken() {
|
|
432
|
+
return this.mintDataToken();
|
|
433
|
+
}
|
|
434
|
+
buildProviders(manifest, config, hostOrigin) {
|
|
435
|
+
this.resolved.clear();
|
|
436
|
+
this.handles = {};
|
|
437
|
+
const reqs = manifest.providers ?? [];
|
|
438
|
+
for (const req of reqs) {
|
|
439
|
+
const binding = config.providers?.[req.key];
|
|
440
|
+
if (!binding) continue;
|
|
441
|
+
const resolved = resolveBinding(req, binding);
|
|
442
|
+
if (!resolved) continue;
|
|
443
|
+
let token = this.tokenByKey.get(req.key);
|
|
444
|
+
if (!token) {
|
|
445
|
+
token = `pps_${randomUUID().replace(/-/g, "")}`;
|
|
446
|
+
this.tokenByKey.set(req.key, token);
|
|
447
|
+
this.proxyTokens.set(token, { providerKey: req.key });
|
|
448
|
+
}
|
|
449
|
+
this.resolved.set(req.key, resolved);
|
|
450
|
+
const proxyBaseURL = `${hostOrigin}/extension-providers/${token}/${encodeURIComponent(req.key)}`;
|
|
451
|
+
const handle = {
|
|
452
|
+
baseURL: resolved.baseURL,
|
|
453
|
+
proxyBaseURL
|
|
454
|
+
};
|
|
455
|
+
if (isUseSlotRequirement(req) && binding.kind === "useSlot") {
|
|
456
|
+
handle.modelKey = binding.modelKey;
|
|
457
|
+
handle.providerType = binding.providerType;
|
|
458
|
+
if (binding.reasoning) handle.reasoning = binding.reasoning;
|
|
459
|
+
} else if (binding.kind === "options" && !isUseSlotRequirement(req)) {
|
|
460
|
+
const opt = req.options[binding.useIndex];
|
|
461
|
+
if (opt?.model) handle.modelKey = opt.model;
|
|
462
|
+
}
|
|
463
|
+
this.handles[req.key] = handle;
|
|
464
|
+
}
|
|
465
|
+
return this.handles;
|
|
466
|
+
}
|
|
467
|
+
resolveProxyForKey(token, providerKey) {
|
|
468
|
+
const session = this.proxyTokens.get(token);
|
|
469
|
+
if (!session || session.providerKey !== providerKey) return null;
|
|
470
|
+
return this.resolved.get(providerKey) ?? null;
|
|
471
|
+
}
|
|
472
|
+
isValidDataToken(token) {
|
|
473
|
+
return this.dataToken !== null && token === this.dataToken;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
function resolveBinding(req, binding) {
|
|
477
|
+
if (isUseSlotRequirement(req)) {
|
|
478
|
+
if (binding.kind !== "useSlot") return null;
|
|
479
|
+
return {
|
|
480
|
+
baseURL: binding.baseURL,
|
|
481
|
+
auth: binding.auth,
|
|
482
|
+
apiKey: binding.apiKey
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (binding.kind !== "options") return null;
|
|
486
|
+
const opt = req.options[binding.useIndex];
|
|
487
|
+
if (!opt) return null;
|
|
488
|
+
return {
|
|
489
|
+
baseURL: binding.fullURL ?? optionURL(opt),
|
|
490
|
+
auth: opt.auth,
|
|
491
|
+
apiKey: binding.apiKey
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const registry = new TokenRegistry();
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/dev/server.ts
|
|
497
|
+
const ASSET_MIME = {
|
|
498
|
+
png: "image/png",
|
|
499
|
+
jpg: "image/jpeg",
|
|
500
|
+
jpeg: "image/jpeg",
|
|
501
|
+
webp: "image/webp",
|
|
502
|
+
gif: "image/gif",
|
|
503
|
+
svg: "image/svg+xml",
|
|
504
|
+
mp4: "video/mp4",
|
|
505
|
+
webm: "video/webm"
|
|
506
|
+
};
|
|
507
|
+
function createDevServer(opts) {
|
|
508
|
+
const app = new Hono();
|
|
509
|
+
app.use("*", cors({
|
|
510
|
+
origin: "*",
|
|
511
|
+
allowHeaders: ["*"],
|
|
512
|
+
allowMethods: ["*"]
|
|
513
|
+
}));
|
|
514
|
+
const dataDir = opts.dataDir;
|
|
515
|
+
const configPath = opts.configPath ?? join(opts.extensionDir, "dev.config.json");
|
|
516
|
+
const dbPath = opts.dbPath;
|
|
517
|
+
const assetsDir = opts.assetsDir;
|
|
518
|
+
const taskControllers = /* @__PURE__ */ new Map();
|
|
519
|
+
const singletonTasks = /* @__PURE__ */ new Map();
|
|
520
|
+
const hostOrigin = `http://localhost:${opts.hostPort}`;
|
|
521
|
+
function loadManifest() {
|
|
522
|
+
const raw = readFileSync(join(opts.extensionDir, "manifest.json"), "utf8");
|
|
523
|
+
const manifest = JSON.parse(raw);
|
|
524
|
+
validateManifest(manifest);
|
|
525
|
+
return manifest;
|
|
526
|
+
}
|
|
527
|
+
function listBooks() {
|
|
528
|
+
if (!existsSync(dbPath)) return [];
|
|
529
|
+
const db = new Database(dbPath, {
|
|
530
|
+
readonly: true,
|
|
531
|
+
fileMustExist: true
|
|
532
|
+
});
|
|
533
|
+
try {
|
|
534
|
+
return db.prepare("SELECT id, title FROM books WHERE status = 'completed' ORDER BY title").all();
|
|
535
|
+
} catch {
|
|
536
|
+
return [];
|
|
537
|
+
} finally {
|
|
538
|
+
db.close();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
app.get("/extension-sdk.js", (c) => {
|
|
542
|
+
c.header("Content-Type", "application/javascript; charset=utf-8");
|
|
543
|
+
return c.body(extensionSdkSource());
|
|
544
|
+
});
|
|
545
|
+
app.get("/extension-data/:token/context", (c) => {
|
|
546
|
+
const token = c.req.param("token");
|
|
547
|
+
if (!registry.isValidDataToken(token)) return c.text("unauthorized", 401);
|
|
548
|
+
const manifest = loadManifest();
|
|
549
|
+
const config = readDevConfig(configPath);
|
|
550
|
+
const providers = registry.buildProviders(manifest, config, hostOrigin);
|
|
551
|
+
return c.json({
|
|
552
|
+
extensionId: manifest.id,
|
|
553
|
+
bookId: config.bookId ?? null,
|
|
554
|
+
providers,
|
|
555
|
+
config: config.config ?? {}
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
app.post("/extension-data/:token/db/query", async (c) => {
|
|
559
|
+
const token = c.req.param("token");
|
|
560
|
+
if (!registry.isValidDataToken(token)) return c.text("unauthorized", 401);
|
|
561
|
+
const body = await c.req.json();
|
|
562
|
+
try {
|
|
563
|
+
return c.json(dbQuery(dbPath, body));
|
|
564
|
+
} catch (e) {
|
|
565
|
+
return c.text(`db.query: ${e.message}`, 400);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
app.post("/extension-data/:token/fs/read", async (c) => {
|
|
569
|
+
const token = c.req.param("token");
|
|
570
|
+
if (!registry.isValidDataToken(token)) return c.text("unauthorized", 401);
|
|
571
|
+
const body = await c.req.json();
|
|
572
|
+
try {
|
|
573
|
+
return c.json(fsRead(assetsDir, body.relPath));
|
|
574
|
+
} catch (e) {
|
|
575
|
+
return c.text(`fs.read: ${e.message}`, 400);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
app.post("/extension-data/:token/fs/write", async (c) => {
|
|
579
|
+
const token = c.req.param("token");
|
|
580
|
+
if (!registry.isValidDataToken(token)) return c.text("unauthorized", 401);
|
|
581
|
+
const body = await c.req.json();
|
|
582
|
+
try {
|
|
583
|
+
return c.json(fsWrite(assetsDir, body.relPath, body.bytesBase64));
|
|
584
|
+
} catch (e) {
|
|
585
|
+
return c.text(`fs.write: ${e.message}`, 400);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
app.post("/extension-data/:token/fs/list", async (c) => {
|
|
589
|
+
const token = c.req.param("token");
|
|
590
|
+
if (!registry.isValidDataToken(token)) return c.text("unauthorized", 401);
|
|
591
|
+
const body = await c.req.json().catch(() => ({}));
|
|
592
|
+
return c.json(fsList(assetsDir, body.relPath ?? null));
|
|
593
|
+
});
|
|
594
|
+
app.post("/extension-data/:token/task/start", async (c) => {
|
|
595
|
+
const token = c.req.param("token");
|
|
596
|
+
if (!registry.isValidDataToken(token)) return c.text("unauthorized", 401);
|
|
597
|
+
const body = await c.req.json();
|
|
598
|
+
const manifest = loadManifest();
|
|
599
|
+
const workerEntry = manifest.path?.worker;
|
|
600
|
+
if (!workerEntry) return c.text("extension has no path.worker entry", 400);
|
|
601
|
+
const config = readDevConfig(configPath);
|
|
602
|
+
const providers = registry.buildProviders(manifest, config, hostOrigin);
|
|
603
|
+
const singletonKey = body.singletonKey ? `${manifest.id}|${config.bookId ?? ""}|${body.singletonKey}` : null;
|
|
604
|
+
if (singletonKey && singletonTasks.has(singletonKey)) return c.text(`task already running: ${body.singletonKey}`, 409);
|
|
605
|
+
const handle = spawnWorker({
|
|
606
|
+
extensionId: manifest.id,
|
|
607
|
+
bookId: config.bookId ?? null,
|
|
608
|
+
extensionDir: opts.extensionDir,
|
|
609
|
+
workerEntry,
|
|
610
|
+
extensionHostBundle: opts.extensionHostBundle,
|
|
611
|
+
denoBin: opts.denoBin,
|
|
612
|
+
extensionDataRoot: dataDir,
|
|
613
|
+
assetsDir,
|
|
614
|
+
dbPath,
|
|
615
|
+
providers,
|
|
616
|
+
config: config.config ?? {},
|
|
617
|
+
netAllowlist: manifest.net ?? [],
|
|
618
|
+
hostPort: opts.hostPort,
|
|
619
|
+
task: body.task,
|
|
620
|
+
payload: body.payload ?? null,
|
|
621
|
+
controllers: taskControllers
|
|
622
|
+
});
|
|
623
|
+
if (singletonKey) singletonTasks.set(singletonKey, handle.taskId);
|
|
624
|
+
return streamSSE(c, async (stream) => {
|
|
625
|
+
await stream.writeSSE({
|
|
626
|
+
event: "started",
|
|
627
|
+
data: JSON.stringify({ taskId: handle.taskId })
|
|
628
|
+
});
|
|
629
|
+
try {
|
|
630
|
+
for await (const ev of handle.events) if (ev.kind === "log") await stream.writeSSE({
|
|
631
|
+
event: "log",
|
|
632
|
+
data: JSON.stringify({ args: ev.args })
|
|
633
|
+
});
|
|
634
|
+
else if (ev.kind === "result") {
|
|
635
|
+
await stream.writeSSE({
|
|
636
|
+
event: "result",
|
|
637
|
+
data: JSON.stringify({ value: ev.value })
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
} else {
|
|
641
|
+
await stream.writeSSE({
|
|
642
|
+
event: "error",
|
|
643
|
+
data: JSON.stringify({ message: ev.message })
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
} finally {
|
|
648
|
+
handle.cancel();
|
|
649
|
+
if (singletonKey && singletonTasks.get(singletonKey) === handle.taskId) singletonTasks.delete(singletonKey);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
app.post("/extension-data/:token/task/:taskId/cancel", (c) => {
|
|
654
|
+
const token = c.req.param("token");
|
|
655
|
+
if (!registry.isValidDataToken(token)) return c.text("unauthorized", 401);
|
|
656
|
+
const taskId = c.req.param("taskId");
|
|
657
|
+
const ac = taskControllers.get(taskId);
|
|
658
|
+
if (ac) ac.abort();
|
|
659
|
+
return c.body(null, 204);
|
|
660
|
+
});
|
|
661
|
+
app.all("/extension-providers/:token/:providerKey/*", async (c) => {
|
|
662
|
+
const token = c.req.param("token");
|
|
663
|
+
const providerKey = c.req.param("providerKey");
|
|
664
|
+
const path = c.req.path;
|
|
665
|
+
const prefix = `/extension-providers/${token}/${providerKey}/`;
|
|
666
|
+
const rest = path.startsWith(prefix) ? path.slice(prefix.length) : "";
|
|
667
|
+
const found = registry.resolveProxyForKey(token, providerKey);
|
|
668
|
+
if (!found) {
|
|
669
|
+
console.warn(`[proxy] (unknown token ${token.slice(0, 12)}… or key ${providerKey}) ${c.req.method} /${rest} -> 401`);
|
|
670
|
+
return c.text("unknown proxy token", 401);
|
|
671
|
+
}
|
|
672
|
+
return proxyToProvider(c, providerKey, found, rest);
|
|
673
|
+
});
|
|
674
|
+
app.get("/extension-assets/:extensionId/*", (c) => {
|
|
675
|
+
if (c.req.param("extensionId") !== loadManifest().id) return c.text("unknown extension", 404);
|
|
676
|
+
const idx = c.req.path.indexOf("/assets/");
|
|
677
|
+
const relPath = idx >= 0 ? decodeURIComponent(c.req.path.slice(idx + 8)) : "";
|
|
678
|
+
if (!relPath) return c.text("missing path", 400);
|
|
679
|
+
let fullPath;
|
|
680
|
+
try {
|
|
681
|
+
fullPath = safeJoin(assetsDir, relPath);
|
|
682
|
+
} catch (e) {
|
|
683
|
+
return c.text(e.message, 400);
|
|
684
|
+
}
|
|
685
|
+
if (!existsSync(fullPath)) return c.text("not found", 404);
|
|
686
|
+
const mime = ASSET_MIME[relPath.slice(relPath.lastIndexOf(".") + 1).toLowerCase()] ?? "application/octet-stream";
|
|
687
|
+
c.header("Content-Type", mime);
|
|
688
|
+
c.header("Cache-Control", "no-cache");
|
|
689
|
+
return c.body(readFileSync(fullPath));
|
|
690
|
+
});
|
|
691
|
+
app.get("/extension-data/:extensionId/assets/*", (c) => {
|
|
692
|
+
if (c.req.param("extensionId") !== loadManifest().id) return c.text("unknown extension", 404);
|
|
693
|
+
const idx = c.req.path.indexOf("/assets/");
|
|
694
|
+
const relPath = idx >= 0 ? decodeURIComponent(c.req.path.slice(idx + 8)) : "";
|
|
695
|
+
if (!relPath) return c.text("missing path", 400);
|
|
696
|
+
let fullPath;
|
|
697
|
+
try {
|
|
698
|
+
fullPath = safeJoin(assetsDir, relPath);
|
|
699
|
+
} catch (e) {
|
|
700
|
+
return c.text(e.message, 400);
|
|
701
|
+
}
|
|
702
|
+
if (!existsSync(fullPath)) return c.text("not found", 404);
|
|
703
|
+
const mime = ASSET_MIME[relPath.slice(relPath.lastIndexOf(".") + 1).toLowerCase()] ?? "application/octet-stream";
|
|
704
|
+
c.header("Content-Type", mime);
|
|
705
|
+
c.header("Cache-Control", "no-cache");
|
|
706
|
+
return c.body(readFileSync(fullPath));
|
|
707
|
+
});
|
|
708
|
+
app.get("/__dev__/setup", (c) => {
|
|
709
|
+
c.header("Content-Type", "text/html; charset=utf-8");
|
|
710
|
+
return c.body(SETUP_UI_HTML);
|
|
711
|
+
});
|
|
712
|
+
app.get("/__dev__/api/status", (c) => {
|
|
713
|
+
const manifest = loadManifest();
|
|
714
|
+
const config = readDevConfig(configPath);
|
|
715
|
+
const status = checkDevConfig(manifest, config);
|
|
716
|
+
return c.json({
|
|
717
|
+
extensionId: manifest.id,
|
|
718
|
+
manifest,
|
|
719
|
+
config,
|
|
720
|
+
ok: status.ok,
|
|
721
|
+
missing: status.missing,
|
|
722
|
+
configPath,
|
|
723
|
+
books: listBooks()
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
app.post("/__dev__/api/save", async (c) => {
|
|
727
|
+
writeDevConfig(configPath, await c.req.json());
|
|
728
|
+
return c.json({ ok: true });
|
|
729
|
+
});
|
|
730
|
+
app.get("/__dev__/api/launch-url", (c) => {
|
|
731
|
+
const config = readDevConfig(configPath);
|
|
732
|
+
if (!config.bookId) return c.json({
|
|
733
|
+
ok: false,
|
|
734
|
+
error: "select a book before launching"
|
|
735
|
+
}, 400);
|
|
736
|
+
if (!listBooks().some((b) => b.id === config.bookId)) return c.json({
|
|
737
|
+
ok: false,
|
|
738
|
+
error: `selected book ${config.bookId} is not in the dev DB — re-seed?`
|
|
739
|
+
}, 400);
|
|
740
|
+
const token = registry.mintDataToken();
|
|
741
|
+
const url = `${opts.viteOrigin}?token=${encodeURIComponent(token)}&host=${encodeURIComponent(hostOrigin)}`;
|
|
742
|
+
return c.json({
|
|
743
|
+
ok: true,
|
|
744
|
+
url
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
return { app };
|
|
748
|
+
}
|
|
749
|
+
//#endregion
|
|
750
|
+
export { readDevConfig as n, writeDevConfig as r, createDevServer as t };
|
|
751
|
+
|
|
752
|
+
//# sourceMappingURL=server-BcwliPFy.mjs.map
|