yamlite 0.36.0 → 0.37.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/README.md +19 -1
- package/dist/cli.mjs +2 -2
- package/dist/core.d.mts +13 -1
- package/dist/core.mjs +3 -3
- package/dist/guide.md +2 -0
- package/dist/{index-BBHZ8yok.d.mts → index-OSyELQEX.d.mts} +6 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-O1FTGCot.mjs → src-DdRmp-vT.mjs} +489 -206
- package/dist/ui/assets/{ErdView-BFNQ9J0L.js → ErdView-DoSS_2hC.js} +1 -1
- package/dist/ui/assets/{MarkdownField-DctsImcK.js → MarkdownField-DGK9weUB.js} +1 -1
- package/dist/ui/assets/{PageView-ApxooX20.js → PageView-BNleODcZ.js} +1 -1
- package/dist/ui/assets/index-rdl0LZp6.css +2 -0
- package/dist/ui/assets/index-yVyR96tO.js +125 -0
- package/dist/ui/assets/{load-C74BIjnc.js → load-DvgVYICA.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/ui-embed/embed.js +5270 -5096
- package/dist/ui-embed/style.css +1 -1
- package/dist/{workspace-1vCIMAfl.mjs → workspace-B4WnIeHR.mjs} +24 -10
- package/package.json +1 -1
- package/dist/ui/assets/index-BIkG4xcY.css +0 -2
- package/dist/ui/assets/index-ryUZiqoX.js +0 -125
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename, delimiter, dirname, extname, isAbsolute, join, matchesGlob, normalize, posix, relative, resolve, sep } from "node:path";
|
|
2
2
|
import { accessSync, chmodSync, constants, cpSync, createReadStream, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { Document, isMap, isNode, isScalar, isSeq, parse, parseDocument, stringify } from "yaml";
|
|
4
|
+
import { Document, Scalar, isMap, isNode, isScalar, isSeq, parse, parseDocument, stringify } from "yaml";
|
|
5
5
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { DatabaseSync, constants as constants$1 } from "node:sqlite";
|
|
7
7
|
import { watch } from "chokidar";
|
|
@@ -1326,7 +1326,8 @@ function readRootConfig(root, text) {
|
|
|
1326
1326
|
const path = configPath(root) ?? (text === void 0 ? null : "yamlite.yaml");
|
|
1327
1327
|
if (path === null) return {
|
|
1328
1328
|
tables: [],
|
|
1329
|
-
pages: []
|
|
1329
|
+
pages: [],
|
|
1330
|
+
ai: NO_AI
|
|
1330
1331
|
};
|
|
1331
1332
|
return parseConfig(text ?? readFileSync(path, "utf8"), basename(path));
|
|
1332
1333
|
}
|
|
@@ -1363,7 +1364,30 @@ function parseConfig(text, name = "yamlite.yaml") {
|
|
|
1363
1364
|
})));
|
|
1364
1365
|
return {
|
|
1365
1366
|
tables,
|
|
1366
|
-
pages: toPages(raw?.pages, (p) => p)
|
|
1367
|
+
pages: toPages(raw?.pages, (p) => p),
|
|
1368
|
+
ai: toAi(raw?.ai)
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const AI_INSTRUCTIONS_MAX = 4e3;
|
|
1372
|
+
const NO_AI = {
|
|
1373
|
+
instructions: null,
|
|
1374
|
+
pullRequests: null
|
|
1375
|
+
};
|
|
1376
|
+
function toAi(raw) {
|
|
1377
|
+
if (raw === void 0 || raw === null) return NO_AI;
|
|
1378
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error("ai must be a map");
|
|
1379
|
+
const { instructions, pullRequests, ...rest } = raw;
|
|
1380
|
+
const unknown = Object.keys(rest)[0];
|
|
1381
|
+
if (unknown !== void 0) throw new Error(`unknown key "ai.${unknown}"`);
|
|
1382
|
+
const text = (key, v) => {
|
|
1383
|
+
if (v === void 0 || v === null) return null;
|
|
1384
|
+
if (typeof v !== "string") throw new Error(`ai.${key} must be text`);
|
|
1385
|
+
if (v.length > 4e3) throw new Error(`ai.${key} is longer than ${AI_INSTRUCTIONS_MAX} characters`);
|
|
1386
|
+
return v.trim() === "" ? null : v.trim();
|
|
1387
|
+
};
|
|
1388
|
+
return {
|
|
1389
|
+
instructions: text("instructions", instructions),
|
|
1390
|
+
pullRequests: text("pullRequests", pullRequests)
|
|
1367
1391
|
};
|
|
1368
1392
|
}
|
|
1369
1393
|
const placeholder = (t) => ({
|
|
@@ -1770,7 +1794,8 @@ function resolveConfig(opts, { requireConfig = true } = {}) {
|
|
|
1770
1794
|
pages: checkAccess$1(config.pages.map((p) => ({
|
|
1771
1795
|
...p,
|
|
1772
1796
|
path: expandPath(p.path, root)
|
|
1773
|
-
})), tables)
|
|
1797
|
+
})), tables),
|
|
1798
|
+
ai: config.ai
|
|
1774
1799
|
};
|
|
1775
1800
|
}
|
|
1776
1801
|
if (!opts.db) throw new Error("either root or db is required");
|
|
@@ -1779,7 +1804,8 @@ function resolveConfig(opts, { requireConfig = true } = {}) {
|
|
|
1779
1804
|
db,
|
|
1780
1805
|
stateDir: opts.stateDir ? expandPath(opts.stateDir, process.cwd()) : join(dirname(db), ".yamlite"),
|
|
1781
1806
|
tables: checkViewNames(withClaims((opts.tables ?? []).map((t) => toSpec(locateInput(t, process.cwd()), false)), [])),
|
|
1782
|
-
pages: []
|
|
1807
|
+
pages: [],
|
|
1808
|
+
ai: NO_AI
|
|
1783
1809
|
};
|
|
1784
1810
|
}
|
|
1785
1811
|
//#endregion
|
|
@@ -1978,6 +2004,36 @@ function applySchema(text, target, edit) {
|
|
|
1978
2004
|
flowCollectionPadding: padded
|
|
1979
2005
|
});
|
|
1980
2006
|
}
|
|
2007
|
+
function applyAiInstructions(text, next) {
|
|
2008
|
+
const doc = parseDocument(text);
|
|
2009
|
+
if (doc.errors.length > 0) throw new Error(`invalid yamlite.yaml: ${doc.errors[0]?.message.split("\n")[0]}`);
|
|
2010
|
+
if (!isMap(doc.contents)) doc.contents = doc.createNode({});
|
|
2011
|
+
const root = doc.contents;
|
|
2012
|
+
const current = root.get("ai", true);
|
|
2013
|
+
let changed = false;
|
|
2014
|
+
for (const key of ["instructions", "pullRequests"]) {
|
|
2015
|
+
const value = next[key];
|
|
2016
|
+
if (((isMap(current) ? current.get(key) : void 0) ?? null) === value) continue;
|
|
2017
|
+
changed = true;
|
|
2018
|
+
if (value === null) {
|
|
2019
|
+
if (isMap(current)) current.delete(key);
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
const scalar = new Scalar(value);
|
|
2023
|
+
if (value.includes("\n")) scalar.type = Scalar.BLOCK_LITERAL;
|
|
2024
|
+
const ai = blockMap(doc, root, "ai");
|
|
2025
|
+
if (scalar.type === Scalar.BLOCK_LITERAL) ai.flow = false;
|
|
2026
|
+
ai.set(key, scalar);
|
|
2027
|
+
}
|
|
2028
|
+
if (!changed) return null;
|
|
2029
|
+
const ai = root.get("ai", true);
|
|
2030
|
+
if (isMap(ai) && ai.items.length === 0) root.delete("ai");
|
|
2031
|
+
const padded = /[[{] [^\s\]}]/.test(text);
|
|
2032
|
+
return doc.toString({
|
|
2033
|
+
...stringifyOptions(text),
|
|
2034
|
+
flowCollectionPadding: padded
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
1981
2037
|
//#endregion
|
|
1982
2038
|
//#region src/value.ts
|
|
1983
2039
|
function toSafe(value) {
|
|
@@ -2232,7 +2288,7 @@ function assertRebuildable(store, table) {
|
|
|
2232
2288
|
const pointer = new RegExp(`\\bREFERENCES\\s+["\`[]?${table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
2233
2289
|
for (const other of store.query("SELECT name, sql FROM sqlite_schema WHERE type = 'table' AND name != ?", table)) if (pointer.test(String(other.sql))) throw refuse(table, `is referenced by table "${String(other.name)}"`);
|
|
2234
2290
|
}
|
|
2235
|
-
const show = (v) => typeof v === "string" ? JSON.stringify(v) : String(v);
|
|
2291
|
+
const show$1 = (v) => typeof v === "string" ? JSON.stringify(v) : String(v);
|
|
2236
2292
|
function rebuildTable(store, spec, alterations, base, inFiles, forceConvert) {
|
|
2237
2293
|
const table = spec.name;
|
|
2238
2294
|
assertRebuildable(store, table);
|
|
@@ -2255,7 +2311,7 @@ function rebuildTable(store, spec, alterations, base, inFiles, forceConvert) {
|
|
|
2255
2311
|
else {
|
|
2256
2312
|
next[column] = null;
|
|
2257
2313
|
if (key !== null) cleared.add(key);
|
|
2258
|
-
if (!synced) failures.set(column, [...failures.get(column) ?? [], `${key} (${show(row[column] ?? null)})`]);
|
|
2314
|
+
if (!synced) failures.set(column, [...failures.get(column) ?? [], `${key} (${show$1(row[column] ?? null)})`]);
|
|
2259
2315
|
}
|
|
2260
2316
|
}
|
|
2261
2317
|
converted.push(next);
|
|
@@ -4565,6 +4621,57 @@ const agentRoutes = (router, ctx) => {
|
|
|
4565
4621
|
router.add("POST", "/api/agent/proposals/:id/discard", ({ params }) => ctx.proposals.discard(params.id));
|
|
4566
4622
|
};
|
|
4567
4623
|
//#endregion
|
|
4624
|
+
//#region src/serve/routes/ai.ts
|
|
4625
|
+
function instructionsBody(b) {
|
|
4626
|
+
const text = (key) => {
|
|
4627
|
+
const v = b[key];
|
|
4628
|
+
if (v === void 0 || v === null) return null;
|
|
4629
|
+
if (typeof v !== "string") throw new HttpError(400, `${key} must be text`, { field: key });
|
|
4630
|
+
return v.trim() === "" ? null : v.trim();
|
|
4631
|
+
};
|
|
4632
|
+
return {
|
|
4633
|
+
instructions: text("instructions"),
|
|
4634
|
+
pullRequests: text("pullRequests")
|
|
4635
|
+
};
|
|
4636
|
+
}
|
|
4637
|
+
const aiRoutes = (router, ctx) => {
|
|
4638
|
+
router.add("GET", "/api/ai/instructions", () => {
|
|
4639
|
+
const text = readFileSync(ctx.configFile, "utf8");
|
|
4640
|
+
let ai;
|
|
4641
|
+
try {
|
|
4642
|
+
ai = parseConfig(text).ai;
|
|
4643
|
+
} catch (e) {
|
|
4644
|
+
throw new HttpError(409, e instanceof Error ? e.message : String(e));
|
|
4645
|
+
}
|
|
4646
|
+
return {
|
|
4647
|
+
...ai,
|
|
4648
|
+
base: configHash(text)
|
|
4649
|
+
};
|
|
4650
|
+
});
|
|
4651
|
+
router.add("PUT", "/api/ai/instructions", ({ body }) => {
|
|
4652
|
+
ctx.proposals.assertIdle();
|
|
4653
|
+
const b = objectBody(body, "body");
|
|
4654
|
+
if (typeof b.base !== "string") throw new HttpError(400, "base is required", { field: "base" });
|
|
4655
|
+
const next = instructionsBody(b);
|
|
4656
|
+
const text = readFileSync(ctx.configFile, "utf8");
|
|
4657
|
+
if (configHash(text) !== b.base) throw new HttpError(409, "yamlite.yaml changed since editing started", { current: configHash(text) });
|
|
4658
|
+
const written = applyAiInstructions(text, next);
|
|
4659
|
+
if (written !== null) {
|
|
4660
|
+
try {
|
|
4661
|
+
parseConfig(written);
|
|
4662
|
+
} catch (e) {
|
|
4663
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
4664
|
+
}
|
|
4665
|
+
writeAtomic(ctx.configFile, written);
|
|
4666
|
+
ctx.reloadConfig();
|
|
4667
|
+
}
|
|
4668
|
+
return {
|
|
4669
|
+
...next,
|
|
4670
|
+
base: configHash(written ?? text)
|
|
4671
|
+
};
|
|
4672
|
+
});
|
|
4673
|
+
};
|
|
4674
|
+
//#endregion
|
|
4568
4675
|
//#region src/serve/conflicts.ts
|
|
4569
4676
|
const conflictsDir = (stateDir) => join(stateDir, "conflicts");
|
|
4570
4677
|
function entryFor(table, file, text, mtime) {
|
|
@@ -5632,6 +5739,18 @@ async function findOpenPr(repo, branch, gh) {
|
|
|
5632
5739
|
return null;
|
|
5633
5740
|
}
|
|
5634
5741
|
}
|
|
5742
|
+
const TEMPLATE_DIRS = [
|
|
5743
|
+
".github/",
|
|
5744
|
+
"",
|
|
5745
|
+
"docs/"
|
|
5746
|
+
];
|
|
5747
|
+
function templatePath(paths) {
|
|
5748
|
+
for (const dir of TEMPLATE_DIRS) {
|
|
5749
|
+
const found = paths.find((p) => p.toLowerCase() === `${dir}pull_request_template.md`);
|
|
5750
|
+
if (found !== void 0) return found;
|
|
5751
|
+
}
|
|
5752
|
+
return null;
|
|
5753
|
+
}
|
|
5635
5754
|
//#endregion
|
|
5636
5755
|
//#region src/git/steps.ts
|
|
5637
5756
|
const STEP_KINDS = [
|
|
@@ -6109,6 +6228,27 @@ async function localDriver(root, gh) {
|
|
|
6109
6228
|
},
|
|
6110
6229
|
history: (t, extract, o) => recordHistory(t, extract, o),
|
|
6111
6230
|
fileDiff: (t, rev) => fileDiff(t, rev),
|
|
6231
|
+
async openPr() {
|
|
6232
|
+
const f = await lookup();
|
|
6233
|
+
if (!f) return null;
|
|
6234
|
+
const branch = await currentBranch(f.repo);
|
|
6235
|
+
if (branch === null || branch === await defaultName()) return null;
|
|
6236
|
+
return findOpenPr(f.repo, branch, await gh());
|
|
6237
|
+
},
|
|
6238
|
+
async pullRequestTemplate() {
|
|
6239
|
+
const f = await lookup();
|
|
6240
|
+
if (!f) return null;
|
|
6241
|
+
const path = templatePath((await runGit(f.repo.top, [
|
|
6242
|
+
"ls-tree",
|
|
6243
|
+
"--name-only",
|
|
6244
|
+
"HEAD",
|
|
6245
|
+
"--",
|
|
6246
|
+
".",
|
|
6247
|
+
".github/",
|
|
6248
|
+
"docs/"
|
|
6249
|
+
]).catch(() => "")).split("\n"));
|
|
6250
|
+
return path === null ? null : runGit(f.repo.top, ["show", `HEAD:${path}`]).catch(() => null);
|
|
6251
|
+
},
|
|
6112
6252
|
async review(req, o) {
|
|
6113
6253
|
if (req.target !== null) throw new ReviewRefused(400, "this server cannot add to an existing pull request");
|
|
6114
6254
|
const { repo, steps } = await need();
|
|
@@ -6159,6 +6299,78 @@ async function blobAt(repo, rev, rootPath) {
|
|
|
6159
6299
|
], { timeoutMs: SLOW_MS });
|
|
6160
6300
|
}
|
|
6161
6301
|
//#endregion
|
|
6302
|
+
//#region src/agent/reviewdraft.ts
|
|
6303
|
+
const VALUE_CHARS = 1e3;
|
|
6304
|
+
const PROMPT_CHARS = 24e3;
|
|
6305
|
+
const MAX_OUTPUT_TOKENS = 1e3;
|
|
6306
|
+
const clip = (s, n) => s.length > n ? `${s.slice(0, n)}… (${s.length - n} more characters)` : s;
|
|
6307
|
+
const show = (v) => clip(typeof v === "string" ? v : JSON.stringify(v), VALUE_CHARS);
|
|
6308
|
+
function describeRecord(r) {
|
|
6309
|
+
if (r.kind === "added") return ` + ${r.key}: ${show(r.after)}`;
|
|
6310
|
+
if (r.kind === "deleted") return ` - ${r.key}: ${show(r.before)}`;
|
|
6311
|
+
if (r.fields.length === 0) return ` ~ ${r.key}: only formatting or comments`;
|
|
6312
|
+
const fields = r.fields.map((f) => ` ${f}: ${show(r.before?.[f] ?? null)} → ${show(r.after?.[f] ?? null)}`);
|
|
6313
|
+
return [` ~ ${r.key}`, ...fields].join("\n");
|
|
6314
|
+
}
|
|
6315
|
+
function describeFile(f) {
|
|
6316
|
+
const head = `${f.path} (${f.status}${f.table ? `, table ${f.table}` : ""})`;
|
|
6317
|
+
if (f.records) return [head, ...f.records.map(describeRecord)].join("\n");
|
|
6318
|
+
const parts = [head];
|
|
6319
|
+
if (f.before) parts.push(` before:\n${clip(f.before, VALUE_CHARS * 2)}`);
|
|
6320
|
+
if (f.after) parts.push(` after:\n${clip(f.after, VALUE_CHARS * 2)}`);
|
|
6321
|
+
return parts.join("\n");
|
|
6322
|
+
}
|
|
6323
|
+
function draftPrompt(input) {
|
|
6324
|
+
const pr = input.mode === "pr";
|
|
6325
|
+
const what = pr ? "a pull request title (one line, also used as the commit message) and a short description" : "a one-line commit message";
|
|
6326
|
+
const template = pr && input.template?.trim() ? input.template.trim() : null;
|
|
6327
|
+
return {
|
|
6328
|
+
system: [
|
|
6329
|
+
`You write ${what} for changes to data files in a YAMLite folder (YAML and Markdown records kept in sync with SQLite).`,
|
|
6330
|
+
"The title says what changed in the data, briefly, without file paths or a trailing period.",
|
|
6331
|
+
pr ? "The description says what changed and, when the changes make it clear, why, as a few Markdown bullet points. Do not invent reasons." : "",
|
|
6332
|
+
`Write in the language with this code: ${input.locale}.`,
|
|
6333
|
+
template ? `Fill in the description following this pull request template: keep its headings and structure, replace its comments and placeholders, and leave a section empty when the changes say nothing for it.\n<template>\n${clip(template, VALUE_CHARS * 4)}\n</template>` : "",
|
|
6334
|
+
input.ai.instructions ? `Instructions from the repository, which override the ones above:\n${input.ai.instructions}` : "",
|
|
6335
|
+
pr && input.ai.pullRequests ? `Instructions from the repository for pull requests, which override all of the above:\n${input.ai.pullRequests}` : ""
|
|
6336
|
+
].filter(Boolean).join("\n\n"),
|
|
6337
|
+
prompt: clip(`Changes:\n${input.files.map(describeFile).join("\n\n")}`, PROMPT_CHARS)
|
|
6338
|
+
};
|
|
6339
|
+
}
|
|
6340
|
+
const SCHEMA = {
|
|
6341
|
+
pr: {
|
|
6342
|
+
type: "object",
|
|
6343
|
+
properties: {
|
|
6344
|
+
title: { type: "string" },
|
|
6345
|
+
body: { type: "string" }
|
|
6346
|
+
},
|
|
6347
|
+
required: ["title", "body"],
|
|
6348
|
+
additionalProperties: false
|
|
6349
|
+
},
|
|
6350
|
+
commit: {
|
|
6351
|
+
type: "object",
|
|
6352
|
+
properties: { title: { type: "string" } },
|
|
6353
|
+
required: ["title"],
|
|
6354
|
+
additionalProperties: false
|
|
6355
|
+
}
|
|
6356
|
+
};
|
|
6357
|
+
async function draftReview(model, input, signal) {
|
|
6358
|
+
const { system, prompt } = draftPrompt(input);
|
|
6359
|
+
const r = await generateText({
|
|
6360
|
+
model,
|
|
6361
|
+
instructions: system,
|
|
6362
|
+
prompt,
|
|
6363
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
6364
|
+
output: Output.object({ schema: jsonSchema(SCHEMA[input.mode]) }),
|
|
6365
|
+
abortSignal: signal
|
|
6366
|
+
});
|
|
6367
|
+
const title = r.output.title.replace(/\s+/g, " ").trim();
|
|
6368
|
+
return input.mode === "pr" ? {
|
|
6369
|
+
title,
|
|
6370
|
+
body: (r.output.body ?? "").trim()
|
|
6371
|
+
} : { title };
|
|
6372
|
+
}
|
|
6373
|
+
//#endregion
|
|
6162
6374
|
//#region src/serve/gitrecords.ts
|
|
6163
6375
|
function ownerOf(tables, file) {
|
|
6164
6376
|
for (const t of tables) {
|
|
@@ -6288,6 +6500,204 @@ async function headFile(git, root, spec, key) {
|
|
|
6288
6500
|
return null;
|
|
6289
6501
|
}
|
|
6290
6502
|
//#endregion
|
|
6503
|
+
//#region src/serve/routes/pages.ts
|
|
6504
|
+
const MAX_PAGE_BYTES = 1048576;
|
|
6505
|
+
function readPageHtml(path, shown) {
|
|
6506
|
+
let size;
|
|
6507
|
+
try {
|
|
6508
|
+
const st = statSync(path);
|
|
6509
|
+
if (!st.isFile()) throw new HttpError(404, `page file is not a file: ${shown}`);
|
|
6510
|
+
size = st.size;
|
|
6511
|
+
} catch (e) {
|
|
6512
|
+
if (e instanceof HttpError) throw e;
|
|
6513
|
+
throw new HttpError(404, `page file not found: ${shown}`);
|
|
6514
|
+
}
|
|
6515
|
+
if (size > MAX_PAGE_BYTES) throw new HttpError(413, `${shown} is larger than 1 MB`);
|
|
6516
|
+
return readFileSync(path, "utf8");
|
|
6517
|
+
}
|
|
6518
|
+
function pageSpec(ctx, name) {
|
|
6519
|
+
const page = ctx.y.pages.find((p) => p.name === name);
|
|
6520
|
+
if (!page) throw new HttpError(404, `unknown page: ${name}`);
|
|
6521
|
+
return page;
|
|
6522
|
+
}
|
|
6523
|
+
const pageRoutes = (router, ctx) => {
|
|
6524
|
+
router.add("GET", "/api/pages/:page/html", ({ params }) => {
|
|
6525
|
+
const page = pageSpec(ctx, params.page);
|
|
6526
|
+
return { html: readPageHtml(page.path, displayPath$1(ctx.root, page.path)) };
|
|
6527
|
+
});
|
|
6528
|
+
};
|
|
6529
|
+
//#endregion
|
|
6530
|
+
//#region src/serve/routes/pageai.ts
|
|
6531
|
+
const isMap$2 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
6532
|
+
function readAsk(body, streaming) {
|
|
6533
|
+
const b = objectBody(body, "body");
|
|
6534
|
+
if (b.apiKey !== void 0) throw new HttpError(400, "pages cannot send API keys");
|
|
6535
|
+
const { system, prompt, schema, model, maxOutputTokens } = b;
|
|
6536
|
+
if (system !== void 0 && typeof system !== "string") throw new HttpError(400, "system must be a string");
|
|
6537
|
+
if (model !== void 0 && typeof model !== "string") throw new HttpError(400, "model must be a string");
|
|
6538
|
+
if (schema !== void 0 && streaming) throw new HttpError(400, "schema is not supported when streaming");
|
|
6539
|
+
if (schema !== void 0 && !isMap$2(schema)) throw new HttpError(400, "schema must be an object");
|
|
6540
|
+
if (maxOutputTokens !== void 0 && !(Number.isInteger(maxOutputTokens) && maxOutputTokens > 0)) throw new HttpError(400, "maxOutputTokens must be a positive integer");
|
|
6541
|
+
return {
|
|
6542
|
+
system,
|
|
6543
|
+
prompt: readPrompt(prompt),
|
|
6544
|
+
schema,
|
|
6545
|
+
model,
|
|
6546
|
+
maxOutputTokens
|
|
6547
|
+
};
|
|
6548
|
+
}
|
|
6549
|
+
function readPrompt(prompt) {
|
|
6550
|
+
if (typeof prompt === "string") {
|
|
6551
|
+
if (prompt.trim() === "") throw new HttpError(400, "prompt is required");
|
|
6552
|
+
return prompt;
|
|
6553
|
+
}
|
|
6554
|
+
const bad = () => new HttpError(400, "prompt must be a string or a list of { role: user | assistant, content: string }");
|
|
6555
|
+
if (!Array.isArray(prompt) || prompt.length === 0) throw bad();
|
|
6556
|
+
return prompt.map((m) => {
|
|
6557
|
+
if (!isMap$2(m) || m.role !== "user" && m.role !== "assistant" || typeof m.content !== "string") throw bad();
|
|
6558
|
+
return {
|
|
6559
|
+
role: m.role,
|
|
6560
|
+
content: m.content
|
|
6561
|
+
};
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
6564
|
+
function retryAfterSeconds(value) {
|
|
6565
|
+
if (!value) return void 0;
|
|
6566
|
+
const seconds = Number(value);
|
|
6567
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
|
|
6568
|
+
const at = Date.parse(value);
|
|
6569
|
+
return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - Date.now()) / 1e3));
|
|
6570
|
+
}
|
|
6571
|
+
function isNoKey(body) {
|
|
6572
|
+
try {
|
|
6573
|
+
const error = JSON.parse(body ?? "").error;
|
|
6574
|
+
return error?.type === "no_key" || error?.code === "no_key";
|
|
6575
|
+
} catch {
|
|
6576
|
+
return false;
|
|
6577
|
+
}
|
|
6578
|
+
}
|
|
6579
|
+
function modelError(e, provider) {
|
|
6580
|
+
const cause = RetryError.isInstance(e) ? e.lastError : e;
|
|
6581
|
+
if (cause instanceof HttpError) return cause;
|
|
6582
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
6583
|
+
if (APICallError.isInstance(cause)) {
|
|
6584
|
+
if (cause.statusCode === 429) {
|
|
6585
|
+
const retryAfter = retryAfterSeconds(cause.responseHeaders?.["retry-after"]);
|
|
6586
|
+
return new HttpError(429, message, {
|
|
6587
|
+
provider,
|
|
6588
|
+
...retryAfter === void 0 ? {} : { retryAfter }
|
|
6589
|
+
});
|
|
6590
|
+
}
|
|
6591
|
+
if (cause.statusCode === 422 && isNoKey(cause.responseBody)) return new HttpError(503, "no AI key is configured on this host", { code: "no_key" });
|
|
6592
|
+
}
|
|
6593
|
+
return new HttpError(502, message, { provider });
|
|
6594
|
+
}
|
|
6595
|
+
const KEEPALIVE_MS = 15e3;
|
|
6596
|
+
const encoder = new TextEncoder();
|
|
6597
|
+
const sseEvent = (event, data) => encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
6598
|
+
const errorEvent = (e) => sseEvent("error", {
|
|
6599
|
+
error: e.message,
|
|
6600
|
+
...e.extra,
|
|
6601
|
+
status: e.status
|
|
6602
|
+
});
|
|
6603
|
+
const pageAiRoutes = (router, ctx) => {
|
|
6604
|
+
const prepare = (name, body, streaming) => {
|
|
6605
|
+
const page = pageSpec(ctx, name);
|
|
6606
|
+
if (!page.ai) throw new HttpError(403, `page ${name} may not use AI`);
|
|
6607
|
+
if (!ctx.agent) throw new HttpError(503, "no AI key is configured on this host", { code: "no_key" });
|
|
6608
|
+
const ask = readAsk(body, streaming);
|
|
6609
|
+
const { provider, languageModel } = ctx.agent.pageModel(ask.model);
|
|
6610
|
+
return {
|
|
6611
|
+
ask,
|
|
6612
|
+
provider,
|
|
6613
|
+
release: ctx.agent.pageSlot(),
|
|
6614
|
+
call: {
|
|
6615
|
+
model: languageModel,
|
|
6616
|
+
...ask.system === void 0 || ask.system.trim() === "" ? {} : { instructions: ask.system },
|
|
6617
|
+
prompt: ask.prompt,
|
|
6618
|
+
maxOutputTokens: Math.min(ask.maxOutputTokens ?? page.ai.maxOutputTokens, page.ai.maxOutputTokens, PAGE_AI_MAX_TOKENS),
|
|
6619
|
+
maxRetries: 0
|
|
6620
|
+
}
|
|
6621
|
+
};
|
|
6622
|
+
};
|
|
6623
|
+
router.add("POST", "/api/pages/:page/ai", async ({ params, body, signal }) => {
|
|
6624
|
+
const { ask, provider, release, call } = prepare(params.page, body, false);
|
|
6625
|
+
try {
|
|
6626
|
+
if (ask.schema) return { object: (await generateText({
|
|
6627
|
+
...call,
|
|
6628
|
+
output: Output.object({ schema: jsonSchema(ask.schema) }),
|
|
6629
|
+
abortSignal: signal
|
|
6630
|
+
})).output };
|
|
6631
|
+
return { text: (await generateText({
|
|
6632
|
+
...call,
|
|
6633
|
+
abortSignal: signal
|
|
6634
|
+
})).text };
|
|
6635
|
+
} catch (e) {
|
|
6636
|
+
throw modelError(e, provider);
|
|
6637
|
+
} finally {
|
|
6638
|
+
release();
|
|
6639
|
+
}
|
|
6640
|
+
});
|
|
6641
|
+
router.add("POST", "/api/pages/:page/ai/stream", async ({ params, body, signal }) => {
|
|
6642
|
+
const { provider, release, call } = prepare(params.page, body, true);
|
|
6643
|
+
const stop = new AbortController();
|
|
6644
|
+
let ping;
|
|
6645
|
+
const finish = () => {
|
|
6646
|
+
clearInterval(ping);
|
|
6647
|
+
stop.abort();
|
|
6648
|
+
release();
|
|
6649
|
+
};
|
|
6650
|
+
const gone = () => stop.signal.aborted || signal.aborted;
|
|
6651
|
+
let parts;
|
|
6652
|
+
let head;
|
|
6653
|
+
try {
|
|
6654
|
+
parts = streamText({
|
|
6655
|
+
...call,
|
|
6656
|
+
abortSignal: AbortSignal.any([signal, stop.signal]),
|
|
6657
|
+
onError: () => {}
|
|
6658
|
+
}).fullStream[Symbol.asyncIterator]();
|
|
6659
|
+
for (;;) {
|
|
6660
|
+
head = await parts.next();
|
|
6661
|
+
if (head.done || head.value.type !== "start") break;
|
|
6662
|
+
}
|
|
6663
|
+
if (!head.done && head.value.type === "error") throw head.value.error;
|
|
6664
|
+
} catch (e) {
|
|
6665
|
+
finish();
|
|
6666
|
+
throw modelError(e, provider);
|
|
6667
|
+
}
|
|
6668
|
+
const stream = new ReadableStream({
|
|
6669
|
+
async start(k) {
|
|
6670
|
+
ping = setInterval(() => k.enqueue(encoder.encode(": ping\n\n")), KEEPALIVE_MS);
|
|
6671
|
+
try {
|
|
6672
|
+
for (let next = head; !next.done; next = await parts.next()) {
|
|
6673
|
+
const p = next.value;
|
|
6674
|
+
if (p.type === "abort") break;
|
|
6675
|
+
if (p.type === "text-delta") k.enqueue(sseEvent("delta", { text: p.text }));
|
|
6676
|
+
if (p.type === "error") {
|
|
6677
|
+
if (!gone()) k.enqueue(errorEvent(modelError(p.error, provider)));
|
|
6678
|
+
return;
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
if (!gone()) k.enqueue(sseEvent("done", {}));
|
|
6682
|
+
} catch (e) {
|
|
6683
|
+
if (!gone()) k.enqueue(errorEvent(modelError(e, provider)));
|
|
6684
|
+
} finally {
|
|
6685
|
+
finish();
|
|
6686
|
+
try {
|
|
6687
|
+
k.close();
|
|
6688
|
+
} catch {}
|
|
6689
|
+
}
|
|
6690
|
+
},
|
|
6691
|
+
cancel: finish
|
|
6692
|
+
});
|
|
6693
|
+
return new Response(stream, { headers: {
|
|
6694
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
6695
|
+
"cache-control": "no-store",
|
|
6696
|
+
connection: "keep-alive"
|
|
6697
|
+
} });
|
|
6698
|
+
});
|
|
6699
|
+
};
|
|
6700
|
+
//#endregion
|
|
6291
6701
|
//#region src/serve/routes/git.ts
|
|
6292
6702
|
function reviewBody(body) {
|
|
6293
6703
|
const b = objectBody(body, "body");
|
|
@@ -6412,6 +6822,60 @@ const gitRoutes = (router, ctx) => {
|
|
|
6412
6822
|
}
|
|
6413
6823
|
});
|
|
6414
6824
|
});
|
|
6825
|
+
router.add("GET", "/api/git/pr", async () => {
|
|
6826
|
+
const git = ctx.git;
|
|
6827
|
+
if (!git) throw new HttpError(404, "the data folder is not in a git repository");
|
|
6828
|
+
return { url: await git.openPr?.().catch(notFound) ?? null };
|
|
6829
|
+
});
|
|
6830
|
+
router.add("POST", "/api/git/review/draft", async ({ body, signal }) => {
|
|
6831
|
+
const git = ctx.git;
|
|
6832
|
+
if (!git) throw new HttpError(404, "the data folder is not in a git repository");
|
|
6833
|
+
if (!ctx.agent) throw new HttpError(404, "no agent is available");
|
|
6834
|
+
const b = objectBody(body, "body");
|
|
6835
|
+
if (!Array.isArray(b.paths) || b.paths.length === 0 || b.paths.some((p) => typeof p !== "string")) throw new HttpError(400, "choose at least one file", { field: "paths" });
|
|
6836
|
+
if (b.mode !== "pr" && b.mode !== "commit") throw new HttpError(400, "mode must be pr or commit");
|
|
6837
|
+
const locale = typeof b.locale === "string" ? b.locale : "en";
|
|
6838
|
+
const { provider, languageModel } = ctx.agent.viewerModel({
|
|
6839
|
+
provider: b.provider,
|
|
6840
|
+
model: b.model,
|
|
6841
|
+
apiKey: b.apiKey
|
|
6842
|
+
});
|
|
6843
|
+
const changed = new Map((await git.status().catch(notFound)).changes.map((c) => [c.path, c.status]));
|
|
6844
|
+
const files = await Promise.all(b.paths.map(async (path) => {
|
|
6845
|
+
const status = changed.get(path);
|
|
6846
|
+
if (status === void 0) throw new HttpError(400, `${path} has no changes`);
|
|
6847
|
+
const found = await recordChanges(git, ctx.root, ctx.y.tables, path);
|
|
6848
|
+
if (found?.records) return {
|
|
6849
|
+
path,
|
|
6850
|
+
status,
|
|
6851
|
+
table: found.table,
|
|
6852
|
+
records: found.records
|
|
6853
|
+
};
|
|
6854
|
+
return {
|
|
6855
|
+
path,
|
|
6856
|
+
status,
|
|
6857
|
+
table: found?.table ?? null,
|
|
6858
|
+
records: null,
|
|
6859
|
+
before: shownText(await git.baseContent(path)),
|
|
6860
|
+
after: shownFile(join(ctx.root, path))
|
|
6861
|
+
};
|
|
6862
|
+
}));
|
|
6863
|
+
const template = b.mode === "pr" ? await git.pullRequestTemplate?.() ?? null : null;
|
|
6864
|
+
const release = ctx.agent.pageSlot();
|
|
6865
|
+
try {
|
|
6866
|
+
return await draftReview(languageModel, {
|
|
6867
|
+
files,
|
|
6868
|
+
mode: b.mode,
|
|
6869
|
+
locale,
|
|
6870
|
+
template,
|
|
6871
|
+
ai: ctx.y.ai
|
|
6872
|
+
}, signal);
|
|
6873
|
+
} catch (e) {
|
|
6874
|
+
throw modelError(e, provider);
|
|
6875
|
+
} finally {
|
|
6876
|
+
release();
|
|
6877
|
+
}
|
|
6878
|
+
});
|
|
6415
6879
|
router.add("POST", "/api/git/revert", async ({ body }) => {
|
|
6416
6880
|
const git = ctx.git;
|
|
6417
6881
|
if (!git) throw new HttpError(404, "the data folder is not in a git repository");
|
|
@@ -6488,6 +6952,18 @@ const gitRoutes = (router, ctx) => {
|
|
|
6488
6952
|
};
|
|
6489
6953
|
});
|
|
6490
6954
|
};
|
|
6955
|
+
const DRAFT_FILE_BYTES = 65536;
|
|
6956
|
+
const OMITTED = "(not shown: a binary file or one larger than 64 KB)";
|
|
6957
|
+
function shownText(text) {
|
|
6958
|
+
if (text === null) return null;
|
|
6959
|
+
return text.length > DRAFT_FILE_BYTES || text.includes("\0") ? OMITTED : text;
|
|
6960
|
+
}
|
|
6961
|
+
function shownFile(file) {
|
|
6962
|
+
if (!existsSync(file)) return null;
|
|
6963
|
+
const s = lstatSync(file);
|
|
6964
|
+
if (!s.isFile()) return s.isSymbolicLink() ? "(a symbolic link)" : null;
|
|
6965
|
+
return s.size > DRAFT_FILE_BYTES ? OMITTED : shownText(readFileSync(file, "utf8"));
|
|
6966
|
+
}
|
|
6491
6967
|
function notFound(e) {
|
|
6492
6968
|
if (e instanceof NoRepository) throw new HttpError(404, e.message);
|
|
6493
6969
|
throw e;
|
|
@@ -6631,204 +7107,6 @@ const metaRoutes = (router, ctx) => {
|
|
|
6631
7107
|
}));
|
|
6632
7108
|
};
|
|
6633
7109
|
//#endregion
|
|
6634
|
-
//#region src/serve/routes/pages.ts
|
|
6635
|
-
const MAX_PAGE_BYTES = 1048576;
|
|
6636
|
-
function readPageHtml(path, shown) {
|
|
6637
|
-
let size;
|
|
6638
|
-
try {
|
|
6639
|
-
const st = statSync(path);
|
|
6640
|
-
if (!st.isFile()) throw new HttpError(404, `page file is not a file: ${shown}`);
|
|
6641
|
-
size = st.size;
|
|
6642
|
-
} catch (e) {
|
|
6643
|
-
if (e instanceof HttpError) throw e;
|
|
6644
|
-
throw new HttpError(404, `page file not found: ${shown}`);
|
|
6645
|
-
}
|
|
6646
|
-
if (size > MAX_PAGE_BYTES) throw new HttpError(413, `${shown} is larger than 1 MB`);
|
|
6647
|
-
return readFileSync(path, "utf8");
|
|
6648
|
-
}
|
|
6649
|
-
function pageSpec(ctx, name) {
|
|
6650
|
-
const page = ctx.y.pages.find((p) => p.name === name);
|
|
6651
|
-
if (!page) throw new HttpError(404, `unknown page: ${name}`);
|
|
6652
|
-
return page;
|
|
6653
|
-
}
|
|
6654
|
-
const pageRoutes = (router, ctx) => {
|
|
6655
|
-
router.add("GET", "/api/pages/:page/html", ({ params }) => {
|
|
6656
|
-
const page = pageSpec(ctx, params.page);
|
|
6657
|
-
return { html: readPageHtml(page.path, displayPath$1(ctx.root, page.path)) };
|
|
6658
|
-
});
|
|
6659
|
-
};
|
|
6660
|
-
//#endregion
|
|
6661
|
-
//#region src/serve/routes/pageai.ts
|
|
6662
|
-
const isMap$2 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
6663
|
-
function readAsk(body, streaming) {
|
|
6664
|
-
const b = objectBody(body, "body");
|
|
6665
|
-
if (b.apiKey !== void 0) throw new HttpError(400, "pages cannot send API keys");
|
|
6666
|
-
const { system, prompt, schema, model, maxOutputTokens } = b;
|
|
6667
|
-
if (system !== void 0 && typeof system !== "string") throw new HttpError(400, "system must be a string");
|
|
6668
|
-
if (model !== void 0 && typeof model !== "string") throw new HttpError(400, "model must be a string");
|
|
6669
|
-
if (schema !== void 0 && streaming) throw new HttpError(400, "schema is not supported when streaming");
|
|
6670
|
-
if (schema !== void 0 && !isMap$2(schema)) throw new HttpError(400, "schema must be an object");
|
|
6671
|
-
if (maxOutputTokens !== void 0 && !(Number.isInteger(maxOutputTokens) && maxOutputTokens > 0)) throw new HttpError(400, "maxOutputTokens must be a positive integer");
|
|
6672
|
-
return {
|
|
6673
|
-
system,
|
|
6674
|
-
prompt: readPrompt(prompt),
|
|
6675
|
-
schema,
|
|
6676
|
-
model,
|
|
6677
|
-
maxOutputTokens
|
|
6678
|
-
};
|
|
6679
|
-
}
|
|
6680
|
-
function readPrompt(prompt) {
|
|
6681
|
-
if (typeof prompt === "string") {
|
|
6682
|
-
if (prompt.trim() === "") throw new HttpError(400, "prompt is required");
|
|
6683
|
-
return prompt;
|
|
6684
|
-
}
|
|
6685
|
-
const bad = () => new HttpError(400, "prompt must be a string or a list of { role: user | assistant, content: string }");
|
|
6686
|
-
if (!Array.isArray(prompt) || prompt.length === 0) throw bad();
|
|
6687
|
-
return prompt.map((m) => {
|
|
6688
|
-
if (!isMap$2(m) || m.role !== "user" && m.role !== "assistant" || typeof m.content !== "string") throw bad();
|
|
6689
|
-
return {
|
|
6690
|
-
role: m.role,
|
|
6691
|
-
content: m.content
|
|
6692
|
-
};
|
|
6693
|
-
});
|
|
6694
|
-
}
|
|
6695
|
-
function retryAfterSeconds(value) {
|
|
6696
|
-
if (!value) return void 0;
|
|
6697
|
-
const seconds = Number(value);
|
|
6698
|
-
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
|
|
6699
|
-
const at = Date.parse(value);
|
|
6700
|
-
return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - Date.now()) / 1e3));
|
|
6701
|
-
}
|
|
6702
|
-
function isNoKey(body) {
|
|
6703
|
-
try {
|
|
6704
|
-
const error = JSON.parse(body ?? "").error;
|
|
6705
|
-
return error?.type === "no_key" || error?.code === "no_key";
|
|
6706
|
-
} catch {
|
|
6707
|
-
return false;
|
|
6708
|
-
}
|
|
6709
|
-
}
|
|
6710
|
-
function modelError(e, provider) {
|
|
6711
|
-
const cause = RetryError.isInstance(e) ? e.lastError : e;
|
|
6712
|
-
if (cause instanceof HttpError) return cause;
|
|
6713
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
6714
|
-
if (APICallError.isInstance(cause)) {
|
|
6715
|
-
if (cause.statusCode === 429) {
|
|
6716
|
-
const retryAfter = retryAfterSeconds(cause.responseHeaders?.["retry-after"]);
|
|
6717
|
-
return new HttpError(429, message, {
|
|
6718
|
-
provider,
|
|
6719
|
-
...retryAfter === void 0 ? {} : { retryAfter }
|
|
6720
|
-
});
|
|
6721
|
-
}
|
|
6722
|
-
if (cause.statusCode === 422 && isNoKey(cause.responseBody)) return new HttpError(503, "no AI key is configured on this host", { code: "no_key" });
|
|
6723
|
-
}
|
|
6724
|
-
return new HttpError(502, message, { provider });
|
|
6725
|
-
}
|
|
6726
|
-
const KEEPALIVE_MS = 15e3;
|
|
6727
|
-
const encoder = new TextEncoder();
|
|
6728
|
-
const sseEvent = (event, data) => encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
6729
|
-
const errorEvent = (e) => sseEvent("error", {
|
|
6730
|
-
error: e.message,
|
|
6731
|
-
...e.extra,
|
|
6732
|
-
status: e.status
|
|
6733
|
-
});
|
|
6734
|
-
const pageAiRoutes = (router, ctx) => {
|
|
6735
|
-
const prepare = (name, body, streaming) => {
|
|
6736
|
-
const page = pageSpec(ctx, name);
|
|
6737
|
-
if (!page.ai) throw new HttpError(403, `page ${name} may not use AI`);
|
|
6738
|
-
if (!ctx.agent) throw new HttpError(503, "no AI key is configured on this host", { code: "no_key" });
|
|
6739
|
-
const ask = readAsk(body, streaming);
|
|
6740
|
-
const { provider, languageModel } = ctx.agent.pageModel(ask.model);
|
|
6741
|
-
return {
|
|
6742
|
-
ask,
|
|
6743
|
-
provider,
|
|
6744
|
-
release: ctx.agent.pageSlot(),
|
|
6745
|
-
call: {
|
|
6746
|
-
model: languageModel,
|
|
6747
|
-
...ask.system === void 0 || ask.system.trim() === "" ? {} : { instructions: ask.system },
|
|
6748
|
-
prompt: ask.prompt,
|
|
6749
|
-
maxOutputTokens: Math.min(ask.maxOutputTokens ?? page.ai.maxOutputTokens, page.ai.maxOutputTokens, PAGE_AI_MAX_TOKENS),
|
|
6750
|
-
maxRetries: 0
|
|
6751
|
-
}
|
|
6752
|
-
};
|
|
6753
|
-
};
|
|
6754
|
-
router.add("POST", "/api/pages/:page/ai", async ({ params, body, signal }) => {
|
|
6755
|
-
const { ask, provider, release, call } = prepare(params.page, body, false);
|
|
6756
|
-
try {
|
|
6757
|
-
if (ask.schema) return { object: (await generateText({
|
|
6758
|
-
...call,
|
|
6759
|
-
output: Output.object({ schema: jsonSchema(ask.schema) }),
|
|
6760
|
-
abortSignal: signal
|
|
6761
|
-
})).output };
|
|
6762
|
-
return { text: (await generateText({
|
|
6763
|
-
...call,
|
|
6764
|
-
abortSignal: signal
|
|
6765
|
-
})).text };
|
|
6766
|
-
} catch (e) {
|
|
6767
|
-
throw modelError(e, provider);
|
|
6768
|
-
} finally {
|
|
6769
|
-
release();
|
|
6770
|
-
}
|
|
6771
|
-
});
|
|
6772
|
-
router.add("POST", "/api/pages/:page/ai/stream", async ({ params, body, signal }) => {
|
|
6773
|
-
const { provider, release, call } = prepare(params.page, body, true);
|
|
6774
|
-
const stop = new AbortController();
|
|
6775
|
-
let ping;
|
|
6776
|
-
const finish = () => {
|
|
6777
|
-
clearInterval(ping);
|
|
6778
|
-
stop.abort();
|
|
6779
|
-
release();
|
|
6780
|
-
};
|
|
6781
|
-
const gone = () => stop.signal.aborted || signal.aborted;
|
|
6782
|
-
let parts;
|
|
6783
|
-
let head;
|
|
6784
|
-
try {
|
|
6785
|
-
parts = streamText({
|
|
6786
|
-
...call,
|
|
6787
|
-
abortSignal: AbortSignal.any([signal, stop.signal]),
|
|
6788
|
-
onError: () => {}
|
|
6789
|
-
}).fullStream[Symbol.asyncIterator]();
|
|
6790
|
-
for (;;) {
|
|
6791
|
-
head = await parts.next();
|
|
6792
|
-
if (head.done || head.value.type !== "start") break;
|
|
6793
|
-
}
|
|
6794
|
-
if (!head.done && head.value.type === "error") throw head.value.error;
|
|
6795
|
-
} catch (e) {
|
|
6796
|
-
finish();
|
|
6797
|
-
throw modelError(e, provider);
|
|
6798
|
-
}
|
|
6799
|
-
const stream = new ReadableStream({
|
|
6800
|
-
async start(k) {
|
|
6801
|
-
ping = setInterval(() => k.enqueue(encoder.encode(": ping\n\n")), KEEPALIVE_MS);
|
|
6802
|
-
try {
|
|
6803
|
-
for (let next = head; !next.done; next = await parts.next()) {
|
|
6804
|
-
const p = next.value;
|
|
6805
|
-
if (p.type === "abort") break;
|
|
6806
|
-
if (p.type === "text-delta") k.enqueue(sseEvent("delta", { text: p.text }));
|
|
6807
|
-
if (p.type === "error") {
|
|
6808
|
-
if (!gone()) k.enqueue(errorEvent(modelError(p.error, provider)));
|
|
6809
|
-
return;
|
|
6810
|
-
}
|
|
6811
|
-
}
|
|
6812
|
-
if (!gone()) k.enqueue(sseEvent("done", {}));
|
|
6813
|
-
} catch (e) {
|
|
6814
|
-
if (!gone()) k.enqueue(errorEvent(modelError(e, provider)));
|
|
6815
|
-
} finally {
|
|
6816
|
-
finish();
|
|
6817
|
-
try {
|
|
6818
|
-
k.close();
|
|
6819
|
-
} catch {}
|
|
6820
|
-
}
|
|
6821
|
-
},
|
|
6822
|
-
cancel: finish
|
|
6823
|
-
});
|
|
6824
|
-
return new Response(stream, { headers: {
|
|
6825
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
6826
|
-
"cache-control": "no-store",
|
|
6827
|
-
connection: "keep-alive"
|
|
6828
|
-
} });
|
|
6829
|
-
});
|
|
6830
|
-
};
|
|
6831
|
-
//#endregion
|
|
6832
7110
|
//#region src/serve/routes/rules.ts
|
|
6833
7111
|
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
6834
7112
|
const scalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
@@ -7256,6 +7534,7 @@ const ROUTES = [
|
|
|
7256
7534
|
pageAiRoutes,
|
|
7257
7535
|
conflictRoutes,
|
|
7258
7536
|
agentRoutes,
|
|
7537
|
+
aiRoutes,
|
|
7259
7538
|
gitRoutes
|
|
7260
7539
|
];
|
|
7261
7540
|
//#endregion
|
|
@@ -8067,6 +8346,9 @@ async function open(options) {
|
|
|
8067
8346
|
get pages() {
|
|
8068
8347
|
return config.pages;
|
|
8069
8348
|
},
|
|
8349
|
+
get ai() {
|
|
8350
|
+
return config.ai;
|
|
8351
|
+
},
|
|
8070
8352
|
async status(opts = {}) {
|
|
8071
8353
|
if (closed) throw new Error("YAMLite is closed");
|
|
8072
8354
|
const dbTime = store.lastWriteMs();
|
|
@@ -8097,6 +8379,7 @@ async function open(options) {
|
|
|
8097
8379
|
const next = resolveConfig(options);
|
|
8098
8380
|
config.tables = next.tables;
|
|
8099
8381
|
config.pages = next.pages;
|
|
8382
|
+
config.ai = next.ai;
|
|
8100
8383
|
registerTables();
|
|
8101
8384
|
return config.tables;
|
|
8102
8385
|
}
|
|
@@ -8118,4 +8401,4 @@ async function open(options) {
|
|
|
8118
8401
|
};
|
|
8119
8402
|
}
|
|
8120
8403
|
//#endregion
|
|
8121
|
-
export {
|
|
8404
|
+
export { ProposalStore as A, CONFIG_NAMES as B, describeChange as C, isRead as D, PageSql as E, createHandler as F, declaredViews as G, parseConfig as H, HttpError as I, Store as K, isLoopback as L, assertSynced as M, toWire as N, statementCount as O, Router as P, loopbackHosts as R, which as S, worthShowing as T, readsFile as U, configPath as V, resolveConfig as W, ghReady as _, query as a, gitLog as b, exportSite as c, validateNewTable as d, ReviewRefused as f, DIFF_LIMIT as g, templatePath as h, formatTable as i, readRecord as j, EventHub as k, ROUTES as l, STEP_KINDS as m, formatCsv as n, generateConfig as o, localDriver as p, COLUMN_TYPES as q, formatJson as r, init as s, open as t, createTable as u, gitBranches as v, fieldChanges as w, gitStatus as x, gitDiff as y, check as z };
|