@markusylisiurunen/tau 0.3.69 → 0.3.71
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/dist/core/history/cli.js +8 -1
- package/dist/core/history/cli.js.map +1 -1
- package/dist/core/history/migrations.js +0 -55
- package/dist/core/history/migrations.js.map +1 -1
- package/dist/core/history/remote_history_client.js +14 -2
- package/dist/core/history/remote_history_client.js.map +1 -1
- package/dist/core/history/replication.js +1 -1
- package/dist/core/history/replication.js.map +1 -1
- package/dist/core/history/setup.js +31 -4
- package/dist/core/history/setup.js.map +1 -1
- package/dist/core/runtime/chat_runtime.js +3 -0
- package/dist/core/runtime/chat_runtime.js.map +1 -1
- package/dist/core/static/code_mode/history/documentation.md +3 -1
- package/dist/core/static/tau_docs/client-tools.md +1 -1
- package/dist/core/static/tau_docs/history.md +28 -6
- package/dist/core/static/tau_docs/prompts-and-project-context.md +8 -0
- package/dist/core/static/tau_docs/sessions.md +2 -0
- package/dist/core/static/tau_docs/telegram.md +5 -4
- package/dist/core/static/tau_docs/tools.md +23 -1
- package/dist/core/static/tau_docs/tui.md +1 -1
- package/dist/core/subagents/agent_supervisor.js +1 -1
- package/dist/core/subagents/agent_supervisor.js.map +1 -1
- package/dist/core/telegram/adapter.js +125 -4
- package/dist/core/telegram/adapter.js.map +1 -1
- package/dist/core/telegram/config.js +1 -1
- package/dist/core/telegram/local_session_client.js +9 -2
- package/dist/core/telegram/local_session_client.js.map +1 -1
- package/dist/core/telegram/runtime.js +19 -2
- package/dist/core/telegram/runtime.js.map +1 -1
- package/dist/core/telegram/send_image.js +120 -0
- package/dist/core/telegram/send_image.js.map +1 -0
- package/dist/core/telegram/session_manager.js +28 -0
- package/dist/core/telegram/session_manager.js.map +1 -1
- package/dist/core/tools/bash.js +41 -3
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/bash_jobs.js +301 -0
- package/dist/core/tools/bash_jobs.js.map +1 -0
- package/dist/core/tools/catalog.js +10 -3
- package/dist/core/tools/catalog.js.map +1 -1
- package/dist/core/tools/execution_backend.js +2 -0
- package/dist/core/tools/execution_backend.js.map +1 -1
- package/dist/core/tools/history.js +1 -0
- package/dist/core/tools/history.js.map +1 -1
- package/dist/core/tools/presentation.js +36 -0
- package/dist/core/tools/presentation.js.map +1 -1
- package/dist/core/tools/spawn_agent.js +1 -0
- package/dist/core/tools/spawn_agent.js.map +1 -1
- package/dist/core/tools/tool_names.js +4 -0
- package/dist/core/tools/tool_names.js.map +1 -1
- package/dist/core/utils/spawn_capture.js +2 -0
- package/dist/core/utils/spawn_capture.js.map +1 -1
- package/dist/core/version.js +2 -2
- package/dist/diff_tool/app/dist/assets/{angular-html-GYtPR4ZN.js → angular-html-D8XZ6L1W.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{angular-ts-CP8tBFCL.js → angular-ts-B2aLLY_Z.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{apl-DlYjD5SW.js → apl-CfprQVX4.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{astro-D8wjxID2.js → astro-D2f_I3HE.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{blade-DwY1HngW.js → blade-CLtAGpBc.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{c-CAILAMk6.js → c-Qn9tF0wR.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{chapel-VXP46g0K.js → chapel-DabpXN2c.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{cobol-BwAQKs_L.js → cobol-BS9rswW2.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{coffee-DeNZ8VRy.js → coffee-DJjZTCkh.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{cpp-CF9ADHZh.js → cpp-6G1npUMW.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{crystal-Dlr9u7o6.js → crystal-yGprvv5C.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{css-DqAHzxWB.js → css-BnlmtDZM.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{edge-ds7vvsur.js → edge-DSSWy6df.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{elixir-Es7Jrt6x.js → elixir-Drn1Bw4g.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{elm-D11ntXWx.js → elm-BtjpPFL1.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{erb-BqfIFYyn.js → erb-I91oznQj.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{git-rebase-BNJHcUvp.js → git-rebase-B6EXosZ2.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{glimmer-js-DNpIS5IN.js → glimmer-js-Brv1Xv0-.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{glimmer-ts-Bv4OLkK5.js → glimmer-ts-hSHEQSwK.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{glsl-BrwGmSBp.js → glsl-Bztv6m8n.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{graphql-BNCK5rQk.js → graphql-D6eZu1mk.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{hack-DuFQc5W0.js → hack-MG8_CyJU.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{haml-DfSIBmXD.js → haml-DOpHECVn.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{handlebars-97zL-LWI.js → handlebars-CXn0JyYm.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{html-derivative-BtDKJ__D.js → html-derivative-D_UZAUld.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{html-5cQu52Yo.js → html-yv5WBZF_.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{http-DDVZeUrK.js → http-Cuq8OMRz.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{hurl-jYWcmpTH.js → hurl-BpL1rvQi.js} +1 -1
- package/dist/diff_tool/app/dist/assets/index-B4p26B5b.js +1559 -0
- package/dist/diff_tool/app/dist/assets/{java-DztsnWnX.js → java-lOL4pIA5.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{javascript-B9237BHu.js → javascript-24hXqooT.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{jinja-8JSK8Y6a.js → jinja-CcJm4wgQ.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{jison-B5IknAGj.js → jison-CLWGaM3Z.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{json-DRnIZCq8.js → json-Q4rTF_eD.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{jsx-CFiWn8fZ.js → jsx-DgKO46Ra.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{julia-BtFBdMTG.js → julia-CcL4q47t.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{just-C14ODGGY.js → just-CSIuzTSo.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{latex-DeaaLUFy.js → latex-Ciy3mJH4.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{liquid-HEBlYFrk.js → liquid-OONbg0h-.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{lua-CR87cBpR.js → lua-Btm14Bzf.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{marko-Eh01TuqB.js → marko-BmewWoW6.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{mdc-B6RhKbU2.js → mdc-sWVstZzQ.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{nginx-VWZdkDqr.js → nginx-Dcc3mAzJ.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{nim-CHz86e8d.js → nim-vtS9CCAW.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{org-VhIfCSLb.js → org-B7feKU2q.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{perl-aD0uyR3K.js → perl-7rFQ23Cs.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{php-DgAIzA1E.js → php-AmgTWRKZ.js} +1 -1
- package/dist/diff_tool/app/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/dist/diff_tool/app/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/dist/diff_tool/app/dist/assets/{pug-DrOOTekJ.js → pug-eaJXnOWe.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{qml-CBowC-In.js → qml-BBoySWOD.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{r-BDVQ17do.js → r-B2GBKjgG.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{razor-3Cui7XJA.js → razor-D8emi4Jp.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{regexp-CGlnek7r.js → regexp-CKBgZg4Y.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{rst-BJ8-NYKF.js → rst-CVpwV-bV.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{ruby-CYg2GUU3.js → ruby-DmnwJ-ac.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{sas-DfcBvB-I.js → sas-evRrvJrO.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{scss-S7r2xNkU.js → scss-Do3O6tsR.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{shellscript-U4EOW3nV.js → shellscript-D7jRMvC2.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{shellsession-DYrNCgPk.js → shellsession-BiBPKva9.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{soy-B25GTE01.js → soy-CR-Q7WHe.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{sql-CrqkYif6.js → sql-CoBwCDCB.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{stata-CMX2LAnU.js → stata-D6KdJ8PZ.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{surrealql-DDFKL5G0.js → surrealql-Cmqi02II.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{svelte-CLZCBtO7.js → svelte-DdZNRb2C.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{templ-BKLVAgyX.js → templ-CFz1LMKK.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{tex-DbqBQGS7.js → tex-m3WJ8u0a.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{ts-tags-B9kA6WQP.js → ts-tags-fLY9Y9r5.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{tsx-CxQG0405.js → tsx-B6WfBUEX.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{twig-0z1Xause.js → twig-D9vOrthV.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{typescript-R3tpeET4.js → typescript-BZTxacEG.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{typst-DiHSix3p.js → typst-JP3so6Z2.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{vue-C-Pb_COm.js → vue-gWDhG9Fb.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{vue-html-CA18u2Ne.js → vue-html-CaRyGpdm.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{vue-vine-CMnXmPqz.js → vue-vine-DJXQ6msH.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{xml-5qEjbdVg.js → xml-Cvn8jjwq.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{xsl-CeJj5xD1.js → xsl-BpNEamvK.js} +1 -1
- package/dist/diff_tool/app/dist/assets/{yaml-C2k4Cqy8.js → yaml-BQh9PoCE.js} +1 -1
- package/dist/diff_tool/app/dist/index.html +1 -1
- package/dist/execution/cloudflare_sandbox_execution_environment.js +18 -6
- package/dist/execution/cloudflare_sandbox_execution_environment.js.map +1 -1
- package/dist/execution/fly_sprite_execution_environment.js +27 -4
- package/dist/execution/fly_sprite_execution_environment.js.map +1 -1
- package/dist/history/worker/index.js +369 -35
- package/dist/history/worker/index.js.map +1 -1
- package/dist/history/worker/migrations/0001_initial.sql +54 -0
- package/dist/history/worker/viewer.css +72 -0
- package/dist/history/worker/viewer.js +116 -0
- package/dist/host/hosted_ephemeral_agent_session.js +2 -1
- package/dist/host/hosted_ephemeral_agent_session.js.map +1 -1
- package/dist/host/local_session_host.js +14 -2
- package/dist/host/local_session_host.js.map +1 -1
- package/dist/host/session_protocol_handler.js +2 -1
- package/dist/host/session_protocol_handler.js.map +1 -1
- package/dist/protocol/session_protocol.js +46 -0
- package/dist/protocol/session_protocol.js.map +1 -1
- package/dist/sdk/session_tool_execution_backend.js +3 -0
- package/dist/sdk/session_tool_execution_backend.js.map +1 -1
- package/dist/tui/chat_view.js +4 -4
- package/dist/tui/chat_view.js.map +1 -1
- package/dist/tui/session_chat_controller.js +3 -1
- package/dist/tui/session_chat_controller.js.map +1 -1
- package/dist/tui/tool_ui_router.js +1 -1
- package/dist/tui/tool_ui_router.js.map +1 -1
- package/dist/tui/ui/chat_container.js +24 -19
- package/dist/tui/ui/chat_container.js.map +1 -1
- package/package.json +19 -14
- package/dist/diff_tool/app/dist/assets/index-B1D38aHD.js +0 -1559
- package/dist/diff_tool/app/dist/assets/theming-B5HubD5i.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-BTK0wyKj.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-BTUlzJFw.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-BURFlYfV.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-BVZjihoZ.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-BWSkzMBn.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-BchIqPOb.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-CeLfRn2P.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-DMjwMSwU.js +0 -1
- package/dist/diff_tool/app/dist/assets/theming-QWGGdlwd.js +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import VIEWER_CSS from "./viewer.css";
|
|
2
|
+
import VIEWER_JS from "./viewer.js";
|
|
1
3
|
const DIGEST_MODEL = "openai/gpt-5.6-luna";
|
|
2
4
|
const DIGEST_TEXT_CONFIG = {
|
|
3
5
|
format: {
|
|
@@ -29,6 +31,16 @@ const MAX_ENTRIES_PER_OPERATION = 25;
|
|
|
29
31
|
const MAX_SEARCH_LIMIT = 75;
|
|
30
32
|
const MAX_READ_LIMIT = 100;
|
|
31
33
|
const MAX_READ_PAGE_PAYLOAD_BYTES = 12 * 1024 * 1024;
|
|
34
|
+
const VIEWER_USERNAME = "tau";
|
|
35
|
+
const VIEWER_SEARCH_PAGE_SIZE = 20;
|
|
36
|
+
const VIEWER_READ_PAGE_PAYLOAD_BYTES = 512 * 1024;
|
|
37
|
+
const VIEWER_SECURITY_HEADERS = {
|
|
38
|
+
"cache-control": "no-store",
|
|
39
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'self'; connect-src 'self'; img-src data:; base-uri 'none'; form-action 'self'; frame-ancestors 'none'",
|
|
40
|
+
"referrer-policy": "no-referrer",
|
|
41
|
+
"x-content-type-options": "nosniff",
|
|
42
|
+
"x-frame-options": "DENY",
|
|
43
|
+
};
|
|
32
44
|
const DIGEST_NEW_ENTRY_THRESHOLD = 8;
|
|
33
45
|
const DIGEST_MAX_STALENESS_MS = 12 * 60 * 60 * 1_000;
|
|
34
46
|
const DIGEST_RETRY_BASE_MS = 5 * 60 * 1_000;
|
|
@@ -56,37 +68,66 @@ function invalidRequest(message) {
|
|
|
56
68
|
}
|
|
57
69
|
export default {
|
|
58
70
|
async fetch(request, env, _context) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
const url = new URL(request.url);
|
|
72
|
+
const viewerRoute = url.pathname === "/" ||
|
|
73
|
+
url.pathname === "/viewer.js" ||
|
|
74
|
+
/^\/sessions\/[^/]+(?:\/entries)?$/.test(url.pathname);
|
|
75
|
+
if (viewerRoute && url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
|
|
76
|
+
return redirectViewerToHttps(url);
|
|
77
|
+
}
|
|
78
|
+
const origin = viewerOrigin(url);
|
|
63
79
|
try {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
if (url.pathname.startsWith("/v1/")) {
|
|
81
|
+
if (!authorizeApi(request, env))
|
|
82
|
+
return error("unauthorized", "Invalid API key", 401);
|
|
83
|
+
if (request.method !== "POST")
|
|
84
|
+
return error("method_not_allowed", "Use POST", 405);
|
|
85
|
+
if (url.pathname === "/v1/operations") {
|
|
86
|
+
const body = await readJson(request);
|
|
87
|
+
const operations = parseOperations(body);
|
|
88
|
+
let applied = 0;
|
|
89
|
+
for (const operation of operations) {
|
|
90
|
+
if (await applyOperation(env.DB, operation))
|
|
91
|
+
applied += 1;
|
|
92
|
+
}
|
|
93
|
+
return json({ applied });
|
|
94
|
+
}
|
|
95
|
+
if (url.pathname === "/v1/search") {
|
|
96
|
+
return json(await search(env.DB, await readJson(request), origin));
|
|
71
97
|
}
|
|
72
|
-
|
|
98
|
+
if (url.pathname === "/v1/read") {
|
|
99
|
+
return json(await read(env.DB, await readJson(request), origin));
|
|
100
|
+
}
|
|
101
|
+
return error("not_found", "Not found", 404);
|
|
73
102
|
}
|
|
74
|
-
if (
|
|
75
|
-
return
|
|
103
|
+
if (!viewerRoute)
|
|
104
|
+
return viewerError("Not found", 404);
|
|
105
|
+
if (!authorizeViewer(request, env))
|
|
106
|
+
return viewerUnauthorized();
|
|
107
|
+
if (request.method !== "GET") {
|
|
108
|
+
return viewerError("Use GET", 405, { allow: "GET" });
|
|
76
109
|
}
|
|
77
|
-
if (
|
|
78
|
-
return
|
|
110
|
+
if (url.pathname === "/viewer.js") {
|
|
111
|
+
return new Response(VIEWER_JS, {
|
|
112
|
+
headers: { ...VIEWER_SECURITY_HEADERS, "content-type": "text/javascript; charset=utf-8" },
|
|
113
|
+
});
|
|
79
114
|
}
|
|
80
|
-
|
|
115
|
+
if (url.pathname === "/")
|
|
116
|
+
return await renderViewerIndex(env.DB, url);
|
|
117
|
+
if (/^\/sessions\/[^/]+\/entries$/.test(url.pathname))
|
|
118
|
+
return await readViewerEntries(env.DB, url);
|
|
119
|
+
return await renderViewerSession(env.DB, url);
|
|
81
120
|
}
|
|
82
121
|
catch (caught) {
|
|
83
122
|
if (caught instanceof HistoryApiError) {
|
|
84
|
-
return
|
|
123
|
+
return viewerRoute
|
|
124
|
+
? viewerError(caught.message, caught.status)
|
|
125
|
+
: error(caught.code, caught.message, caught.status);
|
|
85
126
|
}
|
|
86
|
-
logWorkerError("history_request_failed", caught, {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
logWorkerError("history_request_failed", caught, { pathname: url.pathname });
|
|
128
|
+
return viewerRoute
|
|
129
|
+
? viewerError("Internal server error", 500)
|
|
130
|
+
: error("internal_error", "Internal server error", 500);
|
|
90
131
|
}
|
|
91
132
|
},
|
|
92
133
|
async scheduled(_controller, env, _context) {
|
|
@@ -248,12 +289,57 @@ function providerErrorDetails(error, depth = 0) {
|
|
|
248
289
|
}
|
|
249
290
|
return details;
|
|
250
291
|
}
|
|
251
|
-
function
|
|
292
|
+
function isLoopbackHost(hostname) {
|
|
293
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
294
|
+
}
|
|
295
|
+
function viewerOrigin(url) {
|
|
296
|
+
if (url.protocol !== "http:" || isLoopbackHost(url.hostname))
|
|
297
|
+
return url.origin;
|
|
298
|
+
const secure = new URL(url.origin);
|
|
299
|
+
secure.protocol = "https:";
|
|
300
|
+
return secure.origin;
|
|
301
|
+
}
|
|
302
|
+
function redirectViewerToHttps(url) {
|
|
303
|
+
const location = new URL(url);
|
|
304
|
+
location.protocol = "https:";
|
|
305
|
+
return new Response(null, {
|
|
306
|
+
status: 308,
|
|
307
|
+
headers: { ...VIEWER_SECURITY_HEADERS, location: location.toString() },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function authorizeApi(request, env) {
|
|
252
311
|
const expected = env.API_KEY?.trim();
|
|
253
312
|
if (!expected)
|
|
254
313
|
return false;
|
|
255
314
|
return request.headers.get("authorization") === `Bearer ${expected}`;
|
|
256
315
|
}
|
|
316
|
+
function authorizeViewer(request, env) {
|
|
317
|
+
const expected = env.VIEWER_PASSWORD?.trim();
|
|
318
|
+
const authorization = request.headers.get("authorization");
|
|
319
|
+
if (!expected || !authorization?.startsWith("Basic "))
|
|
320
|
+
return false;
|
|
321
|
+
try {
|
|
322
|
+
const encoded = authorization.slice("Basic ".length);
|
|
323
|
+
const bytes = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
|
|
324
|
+
const credentials = new TextDecoder().decode(bytes);
|
|
325
|
+
const separator = credentials.indexOf(":");
|
|
326
|
+
return (separator >= 0 &&
|
|
327
|
+
credentials.slice(0, separator) === VIEWER_USERNAME &&
|
|
328
|
+
secretsEqual(credentials.slice(separator + 1), expected));
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function secretsEqual(actual, expected) {
|
|
335
|
+
if (actual.length !== expected.length)
|
|
336
|
+
return false;
|
|
337
|
+
let difference = 0;
|
|
338
|
+
for (let index = 0; index < actual.length; index += 1) {
|
|
339
|
+
difference |= actual.charCodeAt(index) ^ expected.charCodeAt(index);
|
|
340
|
+
}
|
|
341
|
+
return difference === 0;
|
|
342
|
+
}
|
|
257
343
|
export async function applyOperation(database, operation) {
|
|
258
344
|
if (await operationExists(database, operation.id))
|
|
259
345
|
return false;
|
|
@@ -363,7 +449,7 @@ async function operationExists(database, operationId) {
|
|
|
363
449
|
.first();
|
|
364
450
|
return Boolean(existing);
|
|
365
451
|
}
|
|
366
|
-
async function search(database, raw) {
|
|
452
|
+
async function search(database, raw, origin) {
|
|
367
453
|
const input = asRecord(raw, "search input");
|
|
368
454
|
const query = optionalString(input.query, "query", 1_000)?.trim();
|
|
369
455
|
const attributes = parseAttributeFilters(input.attributes);
|
|
@@ -387,7 +473,7 @@ async function search(database, raw) {
|
|
|
387
473
|
.bind(...attributeValues, limit + 1, offset)
|
|
388
474
|
.all();
|
|
389
475
|
return {
|
|
390
|
-
sessions: rows.results.slice(0, limit).map((row) => descriptor(row)),
|
|
476
|
+
sessions: rows.results.slice(0, limit).map((row) => descriptor(row, origin)),
|
|
391
477
|
...(rows.results.length > limit
|
|
392
478
|
? { nextCursor: encodeCursor({ offset: offset + limit }) }
|
|
393
479
|
: {}),
|
|
@@ -417,7 +503,7 @@ async function search(database, raw) {
|
|
|
417
503
|
.bind(ftsQuery, ftsQuery, ...attributeValues, limit + 1, offset)
|
|
418
504
|
.all();
|
|
419
505
|
const selectedRows = candidates.results.slice(0, limit);
|
|
420
|
-
const sessions = selectedRows.map((row) => descriptor(row));
|
|
506
|
+
const sessions = selectedRows.map((row) => descriptor(row, origin));
|
|
421
507
|
if (selectedRows.length > 0) {
|
|
422
508
|
const sessionIds = selectedRows.map((row) => String(row.session_id));
|
|
423
509
|
const snippets = await database
|
|
@@ -453,7 +539,7 @@ async function search(database, raw) {
|
|
|
453
539
|
: {}),
|
|
454
540
|
};
|
|
455
541
|
}
|
|
456
|
-
async function read(database, raw) {
|
|
542
|
+
async function read(database, raw, origin, maxPayloadBytes = MAX_READ_PAGE_PAYLOAD_BYTES) {
|
|
457
543
|
const input = asRecord(raw, "read input");
|
|
458
544
|
const sessionId = requiredString(input.sessionId, "sessionId", 256);
|
|
459
545
|
const limit = boundedInteger(input.limit ?? 50, "limit", 1, MAX_READ_LIMIT);
|
|
@@ -469,31 +555,31 @@ async function read(database, raw) {
|
|
|
469
555
|
.prepare("SELECT position, LENGTH(CAST(payload_json AS BLOB)) AS payload_bytes FROM entries WHERE session_id = ? AND position > ? ORDER BY position LIMIT ?")
|
|
470
556
|
.bind(sessionId, position, limit + 1)
|
|
471
557
|
.all();
|
|
472
|
-
const page = selectHistoryReadPage(candidates.results, limit);
|
|
558
|
+
const page = selectHistoryReadPage(candidates.results, limit, maxPayloadBytes);
|
|
473
559
|
const rows = await database
|
|
474
560
|
.prepare("SELECT position, payload_json FROM entries WHERE session_id = ? AND position > ? ORDER BY position LIMIT ?")
|
|
475
561
|
.bind(sessionId, position, page.count)
|
|
476
562
|
.all();
|
|
477
563
|
const last = rows.results.at(-1);
|
|
478
564
|
return {
|
|
479
|
-
session: descriptor(session),
|
|
565
|
+
session: descriptor(session, origin),
|
|
480
566
|
entries: rows.results.map((row) => JSON.parse(row.payload_json)),
|
|
481
567
|
...(page.hasMore && last ? { nextCursor: encodeCursor({ position: last.position }) } : {}),
|
|
482
568
|
};
|
|
483
569
|
}
|
|
484
|
-
export function selectHistoryReadPage(candidates, limit) {
|
|
570
|
+
export function selectHistoryReadPage(candidates, limit, maxPayloadBytes = MAX_READ_PAGE_PAYLOAD_BYTES) {
|
|
485
571
|
let count = 0;
|
|
486
572
|
let bytes = 0;
|
|
487
573
|
for (const candidate of candidates.slice(0, limit)) {
|
|
488
574
|
const nextBytes = bytes + Number(candidate.payload_bytes) + 1;
|
|
489
|
-
if (count > 0 && nextBytes >
|
|
575
|
+
if (count > 0 && nextBytes > maxPayloadBytes)
|
|
490
576
|
break;
|
|
491
577
|
count += 1;
|
|
492
578
|
bytes = nextBytes;
|
|
493
579
|
}
|
|
494
580
|
return { count, hasMore: candidates.length > count };
|
|
495
581
|
}
|
|
496
|
-
function descriptor(row) {
|
|
582
|
+
function descriptor(row, origin) {
|
|
497
583
|
const title = typeof row.digest_title === "string" ? row.digest_title : undefined;
|
|
498
584
|
const summary = typeof row.digest_summary === "string" ? row.digest_summary : undefined;
|
|
499
585
|
const through = typeof row.digest_through_entry_id === "string" ? row.digest_through_entry_id : undefined;
|
|
@@ -502,12 +588,229 @@ function descriptor(row) {
|
|
|
502
588
|
attributes: JSON.parse(String(row.attributes_json)),
|
|
503
589
|
createdAt: Number(row.created_at),
|
|
504
590
|
updatedAt: Number(row.updated_at),
|
|
591
|
+
webUrl: `${origin}/sessions/${encodeURIComponent(String(row.session_id))}`,
|
|
505
592
|
...(title && summary && through
|
|
506
593
|
? { digest: { title, summary, updatedThroughEntryId: through } }
|
|
507
594
|
: {}),
|
|
508
595
|
snippets: [],
|
|
509
596
|
};
|
|
510
597
|
}
|
|
598
|
+
async function renderViewerIndex(database, url) {
|
|
599
|
+
const query = url.searchParams.get("q")?.trim() || undefined;
|
|
600
|
+
if (query && query.length > 1_000)
|
|
601
|
+
throw invalidRequest("query is too long");
|
|
602
|
+
const cursor = url.searchParams.get("cursor") || undefined;
|
|
603
|
+
if (cursor && cursor.length > 2_048)
|
|
604
|
+
throw invalidRequest("cursor is too long");
|
|
605
|
+
const attributes = Object.fromEntries(["repository", "source"].flatMap((key) => {
|
|
606
|
+
const value = url.searchParams.get(key)?.trim();
|
|
607
|
+
return value ? [[key, value]] : [];
|
|
608
|
+
}));
|
|
609
|
+
const result = await search(database, {
|
|
610
|
+
query,
|
|
611
|
+
attributes: Object.fromEntries(Object.entries(attributes).map(([key, value]) => [key, { contains: value }])),
|
|
612
|
+
limit: VIEWER_SEARCH_PAGE_SIZE,
|
|
613
|
+
cursor,
|
|
614
|
+
}, url.origin);
|
|
615
|
+
const filters = ["repository", "source"].map((key) => `<input id="${key}" name="${key}" type="text" aria-label="${key}" maxlength="1024" value="${escapeHtml(attributes[key] ?? "")}" placeholder="${key === "repository" ? "Repository contains…" : "Source contains…"}">`);
|
|
616
|
+
const sessions = result.sessions.map(renderSessionCard).join("");
|
|
617
|
+
const nextUrl = new URL("/", url.origin);
|
|
618
|
+
if (query)
|
|
619
|
+
nextUrl.searchParams.set("q", query);
|
|
620
|
+
for (const [key, value] of Object.entries(attributes))
|
|
621
|
+
nextUrl.searchParams.set(key, value);
|
|
622
|
+
if (result.nextCursor)
|
|
623
|
+
nextUrl.searchParams.set("cursor", result.nextCursor);
|
|
624
|
+
const pagination = result.nextCursor
|
|
625
|
+
? `<form class="pagination" action="/" method="get">
|
|
626
|
+
${[...nextUrl.searchParams].map(([key, value]) => `<input type="hidden" name="${escapeHtml(key)}" value="${escapeHtml(value)}">`).join("")}
|
|
627
|
+
<button type="submit">Older sessions</button>
|
|
628
|
+
</form>`
|
|
629
|
+
: "";
|
|
630
|
+
return viewerPage("History", `<form class="search" action="/" method="get" role="search">
|
|
631
|
+
<div class="search-row">
|
|
632
|
+
<input id="q" name="q" type="search" aria-label="Search conversations" maxlength="1000" value="${escapeHtml(query ?? "")}" placeholder="Search titles, summaries, and transcript text">
|
|
633
|
+
<button type="submit">Search</button>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="search-filters">
|
|
636
|
+
${filters.join("")}
|
|
637
|
+
</div>
|
|
638
|
+
</form>
|
|
639
|
+
<section aria-label="Conversations">
|
|
640
|
+
${query || Object.keys(attributes).length > 0 ? '<p><a href="/">Clear filters</a></p>' : ""}
|
|
641
|
+
<div class="session-list">${sessions || '<p class="empty">No conversations found.</p>'}</div>
|
|
642
|
+
${pagination}
|
|
643
|
+
</section>`);
|
|
644
|
+
}
|
|
645
|
+
function renderSessionCard(session) {
|
|
646
|
+
const digest = session.digest;
|
|
647
|
+
const snippets = session.snippets
|
|
648
|
+
.map((snippet) => `<blockquote>${escapeHtml(snippet)}</blockquote>`)
|
|
649
|
+
.join("");
|
|
650
|
+
return `<article class="session-card">
|
|
651
|
+
<div class="session-card-heading">
|
|
652
|
+
<div>
|
|
653
|
+
<h3><a href="${escapeHtml(session.webUrl)}">${escapeHtml(digest?.title ?? "Untitled session")}</a></h3>
|
|
654
|
+
<p class="session-id">${escapeHtml(session.sessionId)}</p>
|
|
655
|
+
</div>
|
|
656
|
+
${digest ? "" : '<span class="pending">Digest pending</span>'}
|
|
657
|
+
</div>
|
|
658
|
+
${digest ? `<p class="summary">${escapeHtml(digest.summary)}</p>` : ""}
|
|
659
|
+
${snippets}
|
|
660
|
+
${renderAttributes(session.attributes)}
|
|
661
|
+
<p class="timestamps">Created ${renderTime(session.createdAt)} · Updated ${renderTime(session.updatedAt)}</p>
|
|
662
|
+
</article>`;
|
|
663
|
+
}
|
|
664
|
+
function viewerSessionId(url) {
|
|
665
|
+
try {
|
|
666
|
+
return decodeURIComponent(url.pathname.split("/")[2]);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
throw invalidRequest("invalid session URL");
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function readViewerEntries(database, url) {
|
|
673
|
+
const result = await read(database, {
|
|
674
|
+
sessionId: viewerSessionId(url),
|
|
675
|
+
limit: MAX_READ_LIMIT,
|
|
676
|
+
cursor: url.searchParams.get("cursor") ?? undefined,
|
|
677
|
+
}, url.origin, VIEWER_READ_PAGE_PAYLOAD_BYTES);
|
|
678
|
+
return viewerResponse(JSON.stringify({
|
|
679
|
+
html: result.entries.map(renderViewerEntry).join(""),
|
|
680
|
+
nextCursor: result.nextCursor ?? null,
|
|
681
|
+
}), 200, { "content-type": "application/json; charset=utf-8" });
|
|
682
|
+
}
|
|
683
|
+
async function renderViewerSession(database, url) {
|
|
684
|
+
const sessionId = viewerSessionId(url);
|
|
685
|
+
const row = await database
|
|
686
|
+
.prepare("SELECT * FROM sessions WHERE session_id = ?")
|
|
687
|
+
.bind(sessionId)
|
|
688
|
+
.first();
|
|
689
|
+
if (!row)
|
|
690
|
+
throw new HistoryApiError("not_found", `history session '${sessionId}' was not found`, 404);
|
|
691
|
+
const session = descriptor(row, url.origin);
|
|
692
|
+
return viewerPage(session.digest?.title ?? "Untitled session", `<nav class="back"><a href="/">← All conversations</a></nav>
|
|
693
|
+
<header class="conversation-header">
|
|
694
|
+
<h1>${escapeHtml(session.digest?.title ?? "Untitled session")}</h1>
|
|
695
|
+
<p class="session-id">${escapeHtml(session.sessionId)}</p>
|
|
696
|
+
${session.digest ? `<p class="summary">${escapeHtml(session.digest.summary)}</p>` : ""}
|
|
697
|
+
${renderAttributes(session.attributes)}
|
|
698
|
+
<p class="timestamps">Created ${renderTime(session.createdAt)} · Updated ${renderTime(session.updatedAt)}</p>
|
|
699
|
+
</header>
|
|
700
|
+
<div class="transcript-actions">
|
|
701
|
+
<button type="button" data-copy="conversation" disabled>Copy conversation</button>
|
|
702
|
+
<button type="button" data-tools="open" disabled>Expand tools</button>
|
|
703
|
+
<button type="button" data-tools="close" disabled>Collapse tools</button>
|
|
704
|
+
</div>
|
|
705
|
+
<p data-transcript-status role="status">Loading conversation…</p>
|
|
706
|
+
<button type="button" data-transcript-retry hidden>Retry loading</button>
|
|
707
|
+
<noscript>JavaScript is required to load the conversation.</noscript>
|
|
708
|
+
<main class="transcript" aria-label="Conversation transcript" aria-busy="true" data-transcript-url="/sessions/${escapeHtml(encodeURIComponent(sessionId))}/entries"></main>`);
|
|
709
|
+
}
|
|
710
|
+
function renderViewerEntry(entry) {
|
|
711
|
+
if (entry.type === "tool") {
|
|
712
|
+
return `<article class="entry tool-card">
|
|
713
|
+
<div class="entry-actions"><button type="button" data-copy="entry">Copy</button></div>
|
|
714
|
+
<details class="tool-entry">
|
|
715
|
+
<summary>
|
|
716
|
+
<span>Tool · ${escapeHtml(entry.name)}</span>
|
|
717
|
+
<span class="outcome">${escapeHtml(entry.outcome)}</span>
|
|
718
|
+
</summary>
|
|
719
|
+
<div class="tool-section"><h3>Arguments</h3><pre>${escapeHtml(formatViewerValue(entry.arguments))}</pre></div>
|
|
720
|
+
<div class="tool-section"><h3>Result</h3><pre>${escapeHtml(formatViewerValue(entry.result))}</pre></div>
|
|
721
|
+
<p class="entry-time">${renderTime(entry.timestamp)}</p>
|
|
722
|
+
</details>
|
|
723
|
+
</article>`;
|
|
724
|
+
}
|
|
725
|
+
return `<article class="entry ${entry.type}">
|
|
726
|
+
<div class="entry-heading"><h2>${entry.type === "user" ? "User" : "Assistant"}</h2><span>${renderTime(entry.timestamp)}</span><button type="button" data-copy="entry">Copy</button></div>
|
|
727
|
+
${renderViewerContent(entry.content)}
|
|
728
|
+
</article>`;
|
|
729
|
+
}
|
|
730
|
+
function renderAttributes(attributes) {
|
|
731
|
+
const ordered = Object.entries(attributes).sort(([left], [right]) => {
|
|
732
|
+
const priority = (key) => (key === "repository" ? 0 : key === "source" ? 1 : 2);
|
|
733
|
+
return priority(left) - priority(right) || left.localeCompare(right);
|
|
734
|
+
});
|
|
735
|
+
if (ordered.length === 0)
|
|
736
|
+
return "";
|
|
737
|
+
return `<table class="metadata"><tbody>${ordered
|
|
738
|
+
.map(([key, value]) => `<tr><th scope="row">${escapeHtml(key)}</th><td>${escapeHtml(value)}</td></tr>`)
|
|
739
|
+
.join("")}</tbody></table>`;
|
|
740
|
+
}
|
|
741
|
+
function renderViewerContent(content) {
|
|
742
|
+
if (!Array.isArray(content))
|
|
743
|
+
return `<pre>${escapeHtml(formatViewerValue(content))}</pre>`;
|
|
744
|
+
const blocks = content.map((block) => {
|
|
745
|
+
if (typeof block !== "object" || block === null) {
|
|
746
|
+
return `<pre>${escapeHtml(formatViewerValue(block))}</pre>`;
|
|
747
|
+
}
|
|
748
|
+
const value = block;
|
|
749
|
+
if (value.type === "text" && typeof value.text === "string") {
|
|
750
|
+
return `<pre>${escapeHtml(value.text)}</pre>`;
|
|
751
|
+
}
|
|
752
|
+
if (value.type === "image" &&
|
|
753
|
+
typeof value.mimeType === "string" &&
|
|
754
|
+
/^(image\/(?:png|jpeg|gif|webp))$/.test(value.mimeType) &&
|
|
755
|
+
typeof value.data === "string" &&
|
|
756
|
+
/^[a-zA-Z0-9+/]*={0,2}$/.test(value.data)) {
|
|
757
|
+
return `<figure class="image-attachment" data-markdown="[image ${escapeHtml(value.mimeType)}]"><img src="data:${value.mimeType};base64,${value.data}" alt="Attached ${escapeHtml(value.mimeType)} image"><figcaption>${escapeHtml(value.mimeType)}</figcaption></figure>`;
|
|
758
|
+
}
|
|
759
|
+
return `<pre>${escapeHtml(formatViewerValue(value))}</pre>`;
|
|
760
|
+
});
|
|
761
|
+
return `<div class="content-blocks">${blocks.join("")}</div>`;
|
|
762
|
+
}
|
|
763
|
+
function formatViewerValue(value) {
|
|
764
|
+
if (typeof value === "string")
|
|
765
|
+
return value;
|
|
766
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
767
|
+
}
|
|
768
|
+
function renderTime(timestamp) {
|
|
769
|
+
const date = new Date(timestamp);
|
|
770
|
+
if (Number.isNaN(date.getTime()))
|
|
771
|
+
return "Unknown time";
|
|
772
|
+
const value = date.toISOString();
|
|
773
|
+
return `<time datetime="${value}">${value.replace("T", " ").replace(".000Z", " UTC")}</time>`;
|
|
774
|
+
}
|
|
775
|
+
function viewerPage(title, content) {
|
|
776
|
+
return viewerResponse(`<!doctype html>
|
|
777
|
+
<html lang="en">
|
|
778
|
+
<head>
|
|
779
|
+
<meta charset="utf-8">
|
|
780
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
781
|
+
<title>${escapeHtml(title)} · Tau history</title>
|
|
782
|
+
<style>${VIEWER_CSS}</style>
|
|
783
|
+
<script src="/viewer.js" defer></script>
|
|
784
|
+
</head>
|
|
785
|
+
<body><div class="shell">${content}</div></body>
|
|
786
|
+
</html>`);
|
|
787
|
+
}
|
|
788
|
+
function viewerResponse(body, status = 200, headers = {}) {
|
|
789
|
+
return new Response(body, {
|
|
790
|
+
status,
|
|
791
|
+
headers: {
|
|
792
|
+
...VIEWER_SECURITY_HEADERS,
|
|
793
|
+
"content-type": "text/html; charset=utf-8",
|
|
794
|
+
...headers,
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function viewerUnauthorized() {
|
|
799
|
+
return viewerError("Authentication required", 401, {
|
|
800
|
+
"www-authenticate": 'Basic realm="Tau history", charset="UTF-8"',
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
function viewerError(message, status, headers = {}) {
|
|
804
|
+
return viewerResponse(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${status} · Tau history</title><style>${VIEWER_CSS}</style></head><body><div class="shell"><main class="error-page"><h1>${status}</h1><p>${escapeHtml(message)}</p><a href="/">Return to conversations</a></main></div></body></html>`, status, headers);
|
|
805
|
+
}
|
|
806
|
+
function escapeHtml(value) {
|
|
807
|
+
return value
|
|
808
|
+
.replaceAll("&", "&")
|
|
809
|
+
.replaceAll("<", "<")
|
|
810
|
+
.replaceAll(">", ">")
|
|
811
|
+
.replaceAll('"', """)
|
|
812
|
+
.replaceAll("'", "'");
|
|
813
|
+
}
|
|
511
814
|
export async function refreshDigestIfNeeded(env, sessionId) {
|
|
512
815
|
const session = await env.DB.prepare(`SELECT s.digest_title,
|
|
513
816
|
s.digest_summary,
|
|
@@ -778,12 +1081,15 @@ function parseEntry(raw) {
|
|
|
778
1081
|
sourceIds: entry.sourceIds.map((value) => requiredString(value, "entry.sourceId", 512)),
|
|
779
1082
|
timestamp: finiteNumber(entry.timestamp, "entry.timestamp"),
|
|
780
1083
|
};
|
|
781
|
-
if (type === "
|
|
782
|
-
if (
|
|
783
|
-
throw invalidRequest(
|
|
1084
|
+
if (type === "assistant") {
|
|
1085
|
+
if (typeof entry.content !== "string") {
|
|
1086
|
+
throw invalidRequest("assistant entry.content must be a string");
|
|
784
1087
|
}
|
|
785
1088
|
return validateEntrySize({ ...base, type, content: entry.content });
|
|
786
1089
|
}
|
|
1090
|
+
if (type === "user") {
|
|
1091
|
+
return validateEntrySize({ ...base, type, content: parseUserContent(entry.content) });
|
|
1092
|
+
}
|
|
787
1093
|
if (!Object.hasOwn(entry, "arguments") || !Object.hasOwn(entry, "result")) {
|
|
788
1094
|
throw invalidRequest("tool entry.arguments and entry.result are required");
|
|
789
1095
|
}
|
|
@@ -803,6 +1109,34 @@ function parseEntry(raw) {
|
|
|
803
1109
|
outcome,
|
|
804
1110
|
});
|
|
805
1111
|
}
|
|
1112
|
+
function parseUserContent(raw) {
|
|
1113
|
+
if (typeof raw === "string")
|
|
1114
|
+
return raw;
|
|
1115
|
+
if (!Array.isArray(raw)) {
|
|
1116
|
+
throw invalidRequest("user entry.content must be a string or an array of text and image blocks");
|
|
1117
|
+
}
|
|
1118
|
+
return raw.map((rawBlock) => {
|
|
1119
|
+
const block = asRecord(rawBlock, "user content block");
|
|
1120
|
+
if (block.type === "text" &&
|
|
1121
|
+
typeof block.text === "string" &&
|
|
1122
|
+
(block.textSignature === undefined || typeof block.textSignature === "string") &&
|
|
1123
|
+
Object.keys(block).every((key) => ["type", "text", "textSignature"].includes(key))) {
|
|
1124
|
+
return {
|
|
1125
|
+
type: "text",
|
|
1126
|
+
text: block.text,
|
|
1127
|
+
...(typeof block.textSignature === "string" ? { textSignature: block.textSignature } : {}),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
if (block.type === "image" &&
|
|
1131
|
+
typeof block.data === "string" &&
|
|
1132
|
+
typeof block.mimeType === "string" &&
|
|
1133
|
+
block.mimeType.length > 0 &&
|
|
1134
|
+
Object.keys(block).every((key) => ["type", "data", "mimeType"].includes(key))) {
|
|
1135
|
+
return { type: "image", data: block.data, mimeType: block.mimeType };
|
|
1136
|
+
}
|
|
1137
|
+
throw invalidRequest("user content block must be a valid text or image block");
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
806
1140
|
function validateEntrySize(entry) {
|
|
807
1141
|
if (utf8ByteLength(JSON.stringify(entry)) > MAX_ENTRY_BYTES) {
|
|
808
1142
|
throw invalidRequest(`entries must not exceed ${MAX_ENTRY_BYTES} serialized bytes`);
|