@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.
Files changed (178) hide show
  1. package/dist/core/history/cli.js +8 -1
  2. package/dist/core/history/cli.js.map +1 -1
  3. package/dist/core/history/migrations.js +0 -55
  4. package/dist/core/history/migrations.js.map +1 -1
  5. package/dist/core/history/remote_history_client.js +14 -2
  6. package/dist/core/history/remote_history_client.js.map +1 -1
  7. package/dist/core/history/replication.js +1 -1
  8. package/dist/core/history/replication.js.map +1 -1
  9. package/dist/core/history/setup.js +31 -4
  10. package/dist/core/history/setup.js.map +1 -1
  11. package/dist/core/runtime/chat_runtime.js +3 -0
  12. package/dist/core/runtime/chat_runtime.js.map +1 -1
  13. package/dist/core/static/code_mode/history/documentation.md +3 -1
  14. package/dist/core/static/tau_docs/client-tools.md +1 -1
  15. package/dist/core/static/tau_docs/history.md +28 -6
  16. package/dist/core/static/tau_docs/prompts-and-project-context.md +8 -0
  17. package/dist/core/static/tau_docs/sessions.md +2 -0
  18. package/dist/core/static/tau_docs/telegram.md +5 -4
  19. package/dist/core/static/tau_docs/tools.md +23 -1
  20. package/dist/core/static/tau_docs/tui.md +1 -1
  21. package/dist/core/subagents/agent_supervisor.js +1 -1
  22. package/dist/core/subagents/agent_supervisor.js.map +1 -1
  23. package/dist/core/telegram/adapter.js +125 -4
  24. package/dist/core/telegram/adapter.js.map +1 -1
  25. package/dist/core/telegram/config.js +1 -1
  26. package/dist/core/telegram/local_session_client.js +9 -2
  27. package/dist/core/telegram/local_session_client.js.map +1 -1
  28. package/dist/core/telegram/runtime.js +19 -2
  29. package/dist/core/telegram/runtime.js.map +1 -1
  30. package/dist/core/telegram/send_image.js +120 -0
  31. package/dist/core/telegram/send_image.js.map +1 -0
  32. package/dist/core/telegram/session_manager.js +28 -0
  33. package/dist/core/telegram/session_manager.js.map +1 -1
  34. package/dist/core/tools/bash.js +41 -3
  35. package/dist/core/tools/bash.js.map +1 -1
  36. package/dist/core/tools/bash_jobs.js +301 -0
  37. package/dist/core/tools/bash_jobs.js.map +1 -0
  38. package/dist/core/tools/catalog.js +10 -3
  39. package/dist/core/tools/catalog.js.map +1 -1
  40. package/dist/core/tools/execution_backend.js +2 -0
  41. package/dist/core/tools/execution_backend.js.map +1 -1
  42. package/dist/core/tools/history.js +1 -0
  43. package/dist/core/tools/history.js.map +1 -1
  44. package/dist/core/tools/presentation.js +36 -0
  45. package/dist/core/tools/presentation.js.map +1 -1
  46. package/dist/core/tools/spawn_agent.js +1 -0
  47. package/dist/core/tools/spawn_agent.js.map +1 -1
  48. package/dist/core/tools/tool_names.js +4 -0
  49. package/dist/core/tools/tool_names.js.map +1 -1
  50. package/dist/core/utils/spawn_capture.js +2 -0
  51. package/dist/core/utils/spawn_capture.js.map +1 -1
  52. package/dist/core/version.js +2 -2
  53. package/dist/diff_tool/app/dist/assets/{angular-html-GYtPR4ZN.js → angular-html-D8XZ6L1W.js} +1 -1
  54. package/dist/diff_tool/app/dist/assets/{angular-ts-CP8tBFCL.js → angular-ts-B2aLLY_Z.js} +1 -1
  55. package/dist/diff_tool/app/dist/assets/{apl-DlYjD5SW.js → apl-CfprQVX4.js} +1 -1
  56. package/dist/diff_tool/app/dist/assets/{astro-D8wjxID2.js → astro-D2f_I3HE.js} +1 -1
  57. package/dist/diff_tool/app/dist/assets/{blade-DwY1HngW.js → blade-CLtAGpBc.js} +1 -1
  58. package/dist/diff_tool/app/dist/assets/{c-CAILAMk6.js → c-Qn9tF0wR.js} +1 -1
  59. package/dist/diff_tool/app/dist/assets/{chapel-VXP46g0K.js → chapel-DabpXN2c.js} +1 -1
  60. package/dist/diff_tool/app/dist/assets/{cobol-BwAQKs_L.js → cobol-BS9rswW2.js} +1 -1
  61. package/dist/diff_tool/app/dist/assets/{coffee-DeNZ8VRy.js → coffee-DJjZTCkh.js} +1 -1
  62. package/dist/diff_tool/app/dist/assets/{cpp-CF9ADHZh.js → cpp-6G1npUMW.js} +1 -1
  63. package/dist/diff_tool/app/dist/assets/{crystal-Dlr9u7o6.js → crystal-yGprvv5C.js} +1 -1
  64. package/dist/diff_tool/app/dist/assets/{css-DqAHzxWB.js → css-BnlmtDZM.js} +1 -1
  65. package/dist/diff_tool/app/dist/assets/{edge-ds7vvsur.js → edge-DSSWy6df.js} +1 -1
  66. package/dist/diff_tool/app/dist/assets/{elixir-Es7Jrt6x.js → elixir-Drn1Bw4g.js} +1 -1
  67. package/dist/diff_tool/app/dist/assets/{elm-D11ntXWx.js → elm-BtjpPFL1.js} +1 -1
  68. package/dist/diff_tool/app/dist/assets/{erb-BqfIFYyn.js → erb-I91oznQj.js} +1 -1
  69. package/dist/diff_tool/app/dist/assets/{git-rebase-BNJHcUvp.js → git-rebase-B6EXosZ2.js} +1 -1
  70. package/dist/diff_tool/app/dist/assets/{glimmer-js-DNpIS5IN.js → glimmer-js-Brv1Xv0-.js} +1 -1
  71. package/dist/diff_tool/app/dist/assets/{glimmer-ts-Bv4OLkK5.js → glimmer-ts-hSHEQSwK.js} +1 -1
  72. package/dist/diff_tool/app/dist/assets/{glsl-BrwGmSBp.js → glsl-Bztv6m8n.js} +1 -1
  73. package/dist/diff_tool/app/dist/assets/{graphql-BNCK5rQk.js → graphql-D6eZu1mk.js} +1 -1
  74. package/dist/diff_tool/app/dist/assets/{hack-DuFQc5W0.js → hack-MG8_CyJU.js} +1 -1
  75. package/dist/diff_tool/app/dist/assets/{haml-DfSIBmXD.js → haml-DOpHECVn.js} +1 -1
  76. package/dist/diff_tool/app/dist/assets/{handlebars-97zL-LWI.js → handlebars-CXn0JyYm.js} +1 -1
  77. package/dist/diff_tool/app/dist/assets/{html-derivative-BtDKJ__D.js → html-derivative-D_UZAUld.js} +1 -1
  78. package/dist/diff_tool/app/dist/assets/{html-5cQu52Yo.js → html-yv5WBZF_.js} +1 -1
  79. package/dist/diff_tool/app/dist/assets/{http-DDVZeUrK.js → http-Cuq8OMRz.js} +1 -1
  80. package/dist/diff_tool/app/dist/assets/{hurl-jYWcmpTH.js → hurl-BpL1rvQi.js} +1 -1
  81. package/dist/diff_tool/app/dist/assets/index-B4p26B5b.js +1559 -0
  82. package/dist/diff_tool/app/dist/assets/{java-DztsnWnX.js → java-lOL4pIA5.js} +1 -1
  83. package/dist/diff_tool/app/dist/assets/{javascript-B9237BHu.js → javascript-24hXqooT.js} +1 -1
  84. package/dist/diff_tool/app/dist/assets/{jinja-8JSK8Y6a.js → jinja-CcJm4wgQ.js} +1 -1
  85. package/dist/diff_tool/app/dist/assets/{jison-B5IknAGj.js → jison-CLWGaM3Z.js} +1 -1
  86. package/dist/diff_tool/app/dist/assets/{json-DRnIZCq8.js → json-Q4rTF_eD.js} +1 -1
  87. package/dist/diff_tool/app/dist/assets/{jsx-CFiWn8fZ.js → jsx-DgKO46Ra.js} +1 -1
  88. package/dist/diff_tool/app/dist/assets/{julia-BtFBdMTG.js → julia-CcL4q47t.js} +1 -1
  89. package/dist/diff_tool/app/dist/assets/{just-C14ODGGY.js → just-CSIuzTSo.js} +1 -1
  90. package/dist/diff_tool/app/dist/assets/{latex-DeaaLUFy.js → latex-Ciy3mJH4.js} +1 -1
  91. package/dist/diff_tool/app/dist/assets/{liquid-HEBlYFrk.js → liquid-OONbg0h-.js} +1 -1
  92. package/dist/diff_tool/app/dist/assets/{lua-CR87cBpR.js → lua-Btm14Bzf.js} +1 -1
  93. package/dist/diff_tool/app/dist/assets/{marko-Eh01TuqB.js → marko-BmewWoW6.js} +1 -1
  94. package/dist/diff_tool/app/dist/assets/{mdc-B6RhKbU2.js → mdc-sWVstZzQ.js} +1 -1
  95. package/dist/diff_tool/app/dist/assets/{nginx-VWZdkDqr.js → nginx-Dcc3mAzJ.js} +1 -1
  96. package/dist/diff_tool/app/dist/assets/{nim-CHz86e8d.js → nim-vtS9CCAW.js} +1 -1
  97. package/dist/diff_tool/app/dist/assets/{org-VhIfCSLb.js → org-B7feKU2q.js} +1 -1
  98. package/dist/diff_tool/app/dist/assets/{perl-aD0uyR3K.js → perl-7rFQ23Cs.js} +1 -1
  99. package/dist/diff_tool/app/dist/assets/{php-DgAIzA1E.js → php-AmgTWRKZ.js} +1 -1
  100. package/dist/diff_tool/app/dist/assets/pierre-dark-CpLgRqie.js +1 -0
  101. package/dist/diff_tool/app/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  102. package/dist/diff_tool/app/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  103. package/dist/diff_tool/app/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  104. package/dist/diff_tool/app/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  105. package/dist/diff_tool/app/dist/assets/pierre-light-CoaEpmwp.js +1 -0
  106. package/dist/diff_tool/app/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  107. package/dist/diff_tool/app/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  108. package/dist/diff_tool/app/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  109. package/dist/diff_tool/app/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  110. package/dist/diff_tool/app/dist/assets/{pug-DrOOTekJ.js → pug-eaJXnOWe.js} +1 -1
  111. package/dist/diff_tool/app/dist/assets/{qml-CBowC-In.js → qml-BBoySWOD.js} +1 -1
  112. package/dist/diff_tool/app/dist/assets/{r-BDVQ17do.js → r-B2GBKjgG.js} +1 -1
  113. package/dist/diff_tool/app/dist/assets/{razor-3Cui7XJA.js → razor-D8emi4Jp.js} +1 -1
  114. package/dist/diff_tool/app/dist/assets/{regexp-CGlnek7r.js → regexp-CKBgZg4Y.js} +1 -1
  115. package/dist/diff_tool/app/dist/assets/{rst-BJ8-NYKF.js → rst-CVpwV-bV.js} +1 -1
  116. package/dist/diff_tool/app/dist/assets/{ruby-CYg2GUU3.js → ruby-DmnwJ-ac.js} +1 -1
  117. package/dist/diff_tool/app/dist/assets/{sas-DfcBvB-I.js → sas-evRrvJrO.js} +1 -1
  118. package/dist/diff_tool/app/dist/assets/{scss-S7r2xNkU.js → scss-Do3O6tsR.js} +1 -1
  119. package/dist/diff_tool/app/dist/assets/{shellscript-U4EOW3nV.js → shellscript-D7jRMvC2.js} +1 -1
  120. package/dist/diff_tool/app/dist/assets/{shellsession-DYrNCgPk.js → shellsession-BiBPKva9.js} +1 -1
  121. package/dist/diff_tool/app/dist/assets/{soy-B25GTE01.js → soy-CR-Q7WHe.js} +1 -1
  122. package/dist/diff_tool/app/dist/assets/{sql-CrqkYif6.js → sql-CoBwCDCB.js} +1 -1
  123. package/dist/diff_tool/app/dist/assets/{stata-CMX2LAnU.js → stata-D6KdJ8PZ.js} +1 -1
  124. package/dist/diff_tool/app/dist/assets/{surrealql-DDFKL5G0.js → surrealql-Cmqi02II.js} +1 -1
  125. package/dist/diff_tool/app/dist/assets/{svelte-CLZCBtO7.js → svelte-DdZNRb2C.js} +1 -1
  126. package/dist/diff_tool/app/dist/assets/{templ-BKLVAgyX.js → templ-CFz1LMKK.js} +1 -1
  127. package/dist/diff_tool/app/dist/assets/{tex-DbqBQGS7.js → tex-m3WJ8u0a.js} +1 -1
  128. package/dist/diff_tool/app/dist/assets/{ts-tags-B9kA6WQP.js → ts-tags-fLY9Y9r5.js} +1 -1
  129. package/dist/diff_tool/app/dist/assets/{tsx-CxQG0405.js → tsx-B6WfBUEX.js} +1 -1
  130. package/dist/diff_tool/app/dist/assets/{twig-0z1Xause.js → twig-D9vOrthV.js} +1 -1
  131. package/dist/diff_tool/app/dist/assets/{typescript-R3tpeET4.js → typescript-BZTxacEG.js} +1 -1
  132. package/dist/diff_tool/app/dist/assets/{typst-DiHSix3p.js → typst-JP3so6Z2.js} +1 -1
  133. package/dist/diff_tool/app/dist/assets/{vue-C-Pb_COm.js → vue-gWDhG9Fb.js} +1 -1
  134. package/dist/diff_tool/app/dist/assets/{vue-html-CA18u2Ne.js → vue-html-CaRyGpdm.js} +1 -1
  135. package/dist/diff_tool/app/dist/assets/{vue-vine-CMnXmPqz.js → vue-vine-DJXQ6msH.js} +1 -1
  136. package/dist/diff_tool/app/dist/assets/{xml-5qEjbdVg.js → xml-Cvn8jjwq.js} +1 -1
  137. package/dist/diff_tool/app/dist/assets/{xsl-CeJj5xD1.js → xsl-BpNEamvK.js} +1 -1
  138. package/dist/diff_tool/app/dist/assets/{yaml-C2k4Cqy8.js → yaml-BQh9PoCE.js} +1 -1
  139. package/dist/diff_tool/app/dist/index.html +1 -1
  140. package/dist/execution/cloudflare_sandbox_execution_environment.js +18 -6
  141. package/dist/execution/cloudflare_sandbox_execution_environment.js.map +1 -1
  142. package/dist/execution/fly_sprite_execution_environment.js +27 -4
  143. package/dist/execution/fly_sprite_execution_environment.js.map +1 -1
  144. package/dist/history/worker/index.js +369 -35
  145. package/dist/history/worker/index.js.map +1 -1
  146. package/dist/history/worker/migrations/0001_initial.sql +54 -0
  147. package/dist/history/worker/viewer.css +72 -0
  148. package/dist/history/worker/viewer.js +116 -0
  149. package/dist/host/hosted_ephemeral_agent_session.js +2 -1
  150. package/dist/host/hosted_ephemeral_agent_session.js.map +1 -1
  151. package/dist/host/local_session_host.js +14 -2
  152. package/dist/host/local_session_host.js.map +1 -1
  153. package/dist/host/session_protocol_handler.js +2 -1
  154. package/dist/host/session_protocol_handler.js.map +1 -1
  155. package/dist/protocol/session_protocol.js +46 -0
  156. package/dist/protocol/session_protocol.js.map +1 -1
  157. package/dist/sdk/session_tool_execution_backend.js +3 -0
  158. package/dist/sdk/session_tool_execution_backend.js.map +1 -1
  159. package/dist/tui/chat_view.js +4 -4
  160. package/dist/tui/chat_view.js.map +1 -1
  161. package/dist/tui/session_chat_controller.js +3 -1
  162. package/dist/tui/session_chat_controller.js.map +1 -1
  163. package/dist/tui/tool_ui_router.js +1 -1
  164. package/dist/tui/tool_ui_router.js.map +1 -1
  165. package/dist/tui/ui/chat_container.js +24 -19
  166. package/dist/tui/ui/chat_container.js.map +1 -1
  167. package/package.json +19 -14
  168. package/dist/diff_tool/app/dist/assets/index-B1D38aHD.js +0 -1559
  169. package/dist/diff_tool/app/dist/assets/theming-B5HubD5i.js +0 -1
  170. package/dist/diff_tool/app/dist/assets/theming-BTK0wyKj.js +0 -1
  171. package/dist/diff_tool/app/dist/assets/theming-BTUlzJFw.js +0 -1
  172. package/dist/diff_tool/app/dist/assets/theming-BURFlYfV.js +0 -1
  173. package/dist/diff_tool/app/dist/assets/theming-BVZjihoZ.js +0 -1
  174. package/dist/diff_tool/app/dist/assets/theming-BWSkzMBn.js +0 -1
  175. package/dist/diff_tool/app/dist/assets/theming-BchIqPOb.js +0 -1
  176. package/dist/diff_tool/app/dist/assets/theming-CeLfRn2P.js +0 -1
  177. package/dist/diff_tool/app/dist/assets/theming-DMjwMSwU.js +0 -1
  178. 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
- if (!authorize(request, env))
60
- return error("unauthorized", "Invalid API key", 401);
61
- if (request.method !== "POST")
62
- return error("method_not_allowed", "Use POST", 405);
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 (new URL(request.url).pathname === "/v1/operations") {
65
- const body = await readJson(request);
66
- const operations = parseOperations(body);
67
- let applied = 0;
68
- for (const operation of operations) {
69
- if (await applyOperation(env.DB, operation))
70
- applied += 1;
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
- return json({ applied });
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 (new URL(request.url).pathname === "/v1/search") {
75
- return json(await search(env.DB, await readJson(request)));
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 (new URL(request.url).pathname === "/v1/read") {
78
- return json(await read(env.DB, await readJson(request)));
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
- return error("not_found", "Not found", 404);
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 error(caught.code, caught.message, caught.status);
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
- pathname: new URL(request.url).pathname,
88
- });
89
- return error("internal_error", "Internal server error", 500);
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 authorize(request, env) {
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 > MAX_READ_PAGE_PAYLOAD_BYTES)
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("&", "&amp;")
809
+ .replaceAll("<", "&lt;")
810
+ .replaceAll(">", "&gt;")
811
+ .replaceAll('"', "&quot;")
812
+ .replaceAll("'", "&#39;");
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 === "user" || type === "assistant") {
782
- if (!Object.hasOwn(entry, "content")) {
783
- throw invalidRequest(`${type} entry.content is required`);
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`);