@k2b/cloud 0.6.0-rc.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.
Files changed (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,390 @@
1
+ import { sql } from "bun";
2
+
3
+ export { guessAiMediaType } from "./file-media-type";
4
+
5
+ /** Per-file and per-conversation caps — read once per operation from settings by the caller layer if needed. */
6
+ export const AI_FILES_MAX_FILE_BYTES_DEFAULT = 50 * 1024 * 1024;
7
+ export const AI_FILES_MAX_CONVERSATION_BYTES_DEFAULT = 250 * 1024 * 1024;
8
+
9
+ export type AiFileStat = {
10
+ path: string;
11
+ size: number;
12
+ mediaType: string;
13
+ origin: "user" | "assistant";
14
+ updatedAt: string;
15
+ version: number;
16
+ };
17
+
18
+ type FileRow = {
19
+ path: string;
20
+ size: number;
21
+ media_type: string;
22
+ origin: "user" | "assistant";
23
+ updated_at: Date | string;
24
+ version: number | string;
25
+ };
26
+
27
+ type FileContentRow = FileRow & { bytes: Uint8Array };
28
+ export type AiFileContent = AiFileStat & { bytes: Uint8Array };
29
+
30
+ const iso = (value: Date | string): string => (value instanceof Date ? value.toISOString() : new Date(value).toISOString());
31
+
32
+ const toStat = (row: FileRow): AiFileStat => ({
33
+ path: row.path,
34
+ size: Number(row.size),
35
+ mediaType: row.media_type,
36
+ origin: row.origin,
37
+ updatedAt: iso(row.updated_at),
38
+ version: Number(row.version),
39
+ });
40
+ const toContent = (row: FileContentRow): AiFileContent => ({ ...toStat(row), bytes: row.bytes });
41
+
42
+ const numberedAiFilePath = (path: string, number: number): string => {
43
+ if (number === 1) return path;
44
+ const slash = path.lastIndexOf("/");
45
+ const directory = path.slice(0, slash + 1);
46
+ const name = path.slice(slash + 1);
47
+ const dot = name.lastIndexOf(".");
48
+ return dot > 0 ? `${directory}${name.slice(0, dot)}-${number}${name.slice(dot)}` : `${directory}${name}-${number}`;
49
+ };
50
+
51
+ const createUniqueAiFile = async (input: {
52
+ conversationId: string;
53
+ path: string;
54
+ bytes: Uint8Array;
55
+ mediaType?: string;
56
+ origin: "user" | "assistant";
57
+ maxFileBytes?: number;
58
+ maxConversationBytes?: number;
59
+ }): Promise<AiFileStat> => {
60
+ const maxFile = input.maxFileBytes ?? AI_FILES_MAX_FILE_BYTES_DEFAULT;
61
+ const maxConversation = input.maxConversationBytes ?? AI_FILES_MAX_CONVERSATION_BYTES_DEFAULT;
62
+ if (input.bytes.byteLength > maxFile) {
63
+ throw new Error(`File exceeds the per-file limit of ${Math.floor(maxFile / (1024 * 1024))} MB.`);
64
+ }
65
+
66
+ return sql.begin(async (tx) => {
67
+ await tx`SELECT id FROM ai.conversations WHERE id = ${input.conversationId} FOR UPDATE`;
68
+ const totals = await tx<{ total: number | string }[]>`
69
+ SELECT COALESCE(SUM(size), 0) AS total FROM ai.files WHERE conversation_id = ${input.conversationId}
70
+ `;
71
+ if (Number(totals[0]?.total ?? 0) + input.bytes.byteLength > maxConversation) {
72
+ throw new Error(`Conversation storage limit of ${Math.floor(maxConversation / (1024 * 1024))} MB exceeded.`);
73
+ }
74
+
75
+ for (let number = 1; number <= 100; number++) {
76
+ const path = numberedAiFilePath(input.path, number);
77
+ const rows = await tx<FileRow[]>`
78
+ INSERT INTO ai.files (conversation_id, path, bytes, media_type, size, origin, updated_at)
79
+ VALUES (${input.conversationId}, ${path}, ${input.bytes}, ${input.mediaType ?? "application/octet-stream"}, ${input.bytes.byteLength}, ${input.origin}, now())
80
+ ON CONFLICT (conversation_id, path) DO NOTHING
81
+ RETURNING path, size, media_type, origin, updated_at, version
82
+ `;
83
+ if (rows[0]) return toStat(rows[0]);
84
+ }
85
+ throw new Error("Could not allocate a unique file path.");
86
+ });
87
+ };
88
+
89
+ /** Normalize a VFS path: absolute, no `.`/`..` segments, no trailing slash. */
90
+ export const normalizeAiFilePath = (path: string): string | null => {
91
+ if (!path.startsWith("/")) return null;
92
+ const segments: string[] = [];
93
+ for (const part of path.split("/")) {
94
+ if (part === "" || part === ".") continue;
95
+ if (part === "..") return null;
96
+ if (part.includes("\0")) return null;
97
+ if (/[\r\n"<>]/u.test(part)) return null;
98
+ segments.push(part);
99
+ }
100
+ if (segments.length === 0) return null;
101
+ return `/${segments.join("/")}`;
102
+ };
103
+
104
+ export const decodeAiFileContent = (content: string, encoding: "utf8" | "base64"): Uint8Array => {
105
+ if (encoding === "utf8") return new TextEncoder().encode(content);
106
+ if (content.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(content)) throw new Error("Invalid base64 file content.");
107
+ const bytes = Buffer.from(content, "base64");
108
+ if (bytes.toString("base64") !== content) throw new Error("Invalid base64 file content.");
109
+ return new Uint8Array(bytes);
110
+ };
111
+
112
+ /**
113
+ * Conversation-scoped file storage backing Assistant file tools. Every
114
+ * operation goes straight to Postgres — no rehydration, horizontal-safe,
115
+ * crash-safe. Reads support byte slices (bytea STORAGE EXTERNAL) so big
116
+ * files never load fully.
117
+ */
118
+ export const aiFileStore = {
119
+ async createUserUpload(input: {
120
+ conversationId: string;
121
+ path: string;
122
+ bytes: Uint8Array;
123
+ mediaType?: string;
124
+ maxFileBytes?: number;
125
+ maxConversationBytes?: number;
126
+ }): Promise<AiFileStat> {
127
+ return createUniqueAiFile({ ...input, origin: "user" });
128
+ },
129
+
130
+ async createAssistantFile(input: {
131
+ conversationId: string;
132
+ path: string;
133
+ bytes: Uint8Array;
134
+ mediaType?: string;
135
+ maxFileBytes?: number;
136
+ maxConversationBytes?: number;
137
+ }): Promise<AiFileStat> {
138
+ return createUniqueAiFile({ ...input, origin: "assistant" });
139
+ },
140
+
141
+ async list(input: { conversationId: string; prefix?: string }): Promise<AiFileStat[]> {
142
+ const prefix = input.prefix ?? "/";
143
+ const pattern = `${prefix.endsWith("/") ? prefix : `${prefix}/`}%`;
144
+ const rows = await sql<FileRow[]>`
145
+ SELECT path, size, media_type, origin, updated_at, version
146
+ FROM ai.files
147
+ WHERE conversation_id = ${input.conversationId}
148
+ AND (path LIKE ${pattern} OR path = ${prefix})
149
+ ORDER BY updated_at DESC, path ASC
150
+ `;
151
+ return rows.map(toStat);
152
+ },
153
+
154
+ async stat(input: { conversationId: string; path: string }): Promise<AiFileStat | null> {
155
+ const rows = await sql<FileRow[]>`
156
+ SELECT path, size, media_type, origin, updated_at, version
157
+ FROM ai.files
158
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path}
159
+ `;
160
+ return rows[0] ? toStat(rows[0]) : null;
161
+ },
162
+
163
+ async read(input: { conversationId: string; path: string }): Promise<AiFileContent | null> {
164
+ const rows = await sql<FileContentRow[]>`
165
+ SELECT path, bytes, size, media_type, origin, updated_at, version
166
+ FROM ai.files
167
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path}
168
+ `;
169
+ return rows[0] ? toContent(rows[0]) : null;
170
+ },
171
+
172
+ async readTurnFile(input: { turnId: string; path: string }): Promise<AiFileContent | null> {
173
+ const rows = await sql<FileContentRow[]>`
174
+ SELECT path, bytes, size, media_type, origin, updated_at, version
175
+ FROM ai.turn_files
176
+ WHERE turn_id = ${input.turnId}::uuid AND path = ${input.path}
177
+ `;
178
+ return rows[0] ? toContent(rows[0]) : null;
179
+ },
180
+
181
+ /** Byte slice without loading the whole value (substring on EXTERNAL bytea reads only needed chunks). */
182
+ async readSlice(input: { conversationId: string; path: string; offset: number; length: number }): Promise<Uint8Array | null> {
183
+ const offset = Math.max(0, Math.floor(input.offset));
184
+ const length = Math.max(0, Math.floor(input.length));
185
+ const rows = await sql<{ chunk: Uint8Array }[]>`
186
+ SELECT substring(bytes FROM ${offset + 1} FOR ${length}) AS chunk
187
+ FROM ai.files
188
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path}
189
+ `;
190
+ if (!rows[0]) return null;
191
+ return new Uint8Array(rows[0].chunk ?? []);
192
+ },
193
+
194
+ async readSliceWithStat(input: { conversationId: string; path: string; offset: number; length: number }): Promise<AiFileContent | null> {
195
+ const offset = Math.max(0, Math.floor(input.offset));
196
+ const length = Math.max(0, Math.floor(input.length));
197
+ const rows = await sql<FileContentRow[]>`
198
+ SELECT path, substring(bytes FROM ${offset + 1} FOR ${length}) AS bytes, size, media_type, origin, updated_at, version
199
+ FROM ai.files
200
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path}
201
+ `;
202
+ return rows[0] ? toContent(rows[0]) : null;
203
+ },
204
+
205
+ async readTurnSliceWithStat(input: { turnId: string; path: string; offset: number; length: number }): Promise<AiFileContent | null> {
206
+ const offset = Math.max(0, Math.floor(input.offset));
207
+ const length = Math.max(0, Math.floor(input.length));
208
+ const rows = await sql<FileContentRow[]>`
209
+ SELECT path, substring(bytes FROM ${offset + 1} FOR ${length}) AS bytes, size, media_type, origin, updated_at, version
210
+ FROM ai.turn_files
211
+ WHERE turn_id = ${input.turnId}::uuid AND path = ${input.path}
212
+ `;
213
+ return rows[0] ? toContent(rows[0]) : null;
214
+ },
215
+
216
+ async readAll(input: { conversationId: string; path: string }): Promise<Uint8Array | null> {
217
+ const rows = await sql<{ bytes: Uint8Array }[]>`
218
+ SELECT bytes FROM ai.files
219
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path}
220
+ `;
221
+ if (!rows[0]) return null;
222
+ return new Uint8Array(rows[0].bytes ?? []);
223
+ },
224
+
225
+ /**
226
+ * Upsert one file. Enforces the per-file and per-conversation caps —
227
+ * here in the store so no command or tool can bypass them.
228
+ */
229
+ async write(input: {
230
+ conversationId: string;
231
+ path: string;
232
+ bytes: Uint8Array;
233
+ mediaType?: string;
234
+ origin?: "user" | "assistant";
235
+ allowUserOverwrite?: boolean;
236
+ maxFileBytes?: number;
237
+ maxConversationBytes?: number;
238
+ }): Promise<void> {
239
+ const maxFile = input.maxFileBytes ?? AI_FILES_MAX_FILE_BYTES_DEFAULT;
240
+ const maxConversation = input.maxConversationBytes ?? AI_FILES_MAX_CONVERSATION_BYTES_DEFAULT;
241
+ if (input.bytes.byteLength > maxFile) {
242
+ throw new Error(`File exceeds the per-file limit of ${Math.floor(maxFile / (1024 * 1024))} MB.`);
243
+ }
244
+
245
+ await sql.begin(async (tx) => {
246
+ await tx`SELECT id FROM ai.conversations WHERE id = ${input.conversationId} FOR UPDATE`;
247
+ const totals = await tx<{ total: number | string }[]>`
248
+ SELECT COALESCE(SUM(size), 0) AS total
249
+ FROM ai.files
250
+ WHERE conversation_id = ${input.conversationId} AND path <> ${input.path}
251
+ `;
252
+ const otherBytes = Number(totals[0]?.total ?? 0);
253
+ if (otherBytes + input.bytes.byteLength > maxConversation) {
254
+ throw new Error(`Conversation storage limit of ${Math.floor(maxConversation / (1024 * 1024))} MB exceeded.`);
255
+ }
256
+ if (input.origin === "user") {
257
+ if (input.allowUserOverwrite) {
258
+ const written = await tx<{ id: string }[]>`
259
+ UPDATE ai.files
260
+ SET bytes = ${input.bytes}, media_type = ${input.mediaType ?? "application/octet-stream"}, size = ${input.bytes.byteLength}, updated_at = now(), version = version + 1
261
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path} AND origin = 'user'
262
+ RETURNING id
263
+ `;
264
+ if (!written[0]) throw new Error(`User-uploaded file does not exist: ${input.path}.`);
265
+ } else {
266
+ await tx`
267
+ INSERT INTO ai.files (conversation_id, path, bytes, media_type, size, origin, updated_at)
268
+ VALUES (${input.conversationId}, ${input.path}, ${input.bytes}, ${input.mediaType ?? "application/octet-stream"}, ${input.bytes.byteLength}, 'user', now())
269
+ `;
270
+ }
271
+ } else {
272
+ const written = await tx<{ id: string }[]>`
273
+ INSERT INTO ai.files (conversation_id, path, bytes, media_type, size, origin, updated_at)
274
+ VALUES (${input.conversationId}, ${input.path}, ${input.bytes}, ${input.mediaType ?? "application/octet-stream"}, ${input.bytes.byteLength}, 'assistant', now())
275
+ ON CONFLICT (conversation_id, path) DO UPDATE SET
276
+ bytes = EXCLUDED.bytes,
277
+ media_type = EXCLUDED.media_type,
278
+ size = EXCLUDED.size,
279
+ updated_at = now(),
280
+ version = ai.files.version + 1
281
+ WHERE ai.files.origin = 'assistant'
282
+ RETURNING id
283
+ `;
284
+ if (!written[0]) throw new Error(`Cannot overwrite user-uploaded file ${input.path}.`);
285
+ }
286
+ });
287
+ },
288
+
289
+ async append(input: {
290
+ conversationId: string;
291
+ path: string;
292
+ bytes: Uint8Array;
293
+ mediaType?: string;
294
+ maxFileBytes?: number;
295
+ maxConversationBytes?: number;
296
+ }): Promise<void> {
297
+ const maxFile = input.maxFileBytes ?? AI_FILES_MAX_FILE_BYTES_DEFAULT;
298
+ const maxConversation = input.maxConversationBytes ?? AI_FILES_MAX_CONVERSATION_BYTES_DEFAULT;
299
+ await sql.begin(async (tx) => {
300
+ await tx`SELECT id FROM ai.conversations WHERE id = ${input.conversationId} FOR UPDATE`;
301
+ const current = await tx<{ size: number }[]>`
302
+ SELECT size FROM ai.files
303
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path}
304
+ `;
305
+ const nextSize = Number(current[0]?.size ?? 0) + input.bytes.byteLength;
306
+ if (nextSize > maxFile) {
307
+ throw new Error(`File exceeds the per-file limit of ${Math.floor(maxFile / (1024 * 1024))} MB.`);
308
+ }
309
+ const totals = await tx<{ total: number | string }[]>`
310
+ SELECT COALESCE(SUM(size), 0) AS total FROM ai.files WHERE conversation_id = ${input.conversationId}
311
+ `;
312
+ if (Number(totals[0]?.total ?? 0) + input.bytes.byteLength > maxConversation) {
313
+ throw new Error(`Conversation storage limit of ${Math.floor(maxConversation / (1024 * 1024))} MB exceeded.`);
314
+ }
315
+ const appended = await tx<{ id: string }[]>`
316
+ INSERT INTO ai.files (conversation_id, path, bytes, media_type, size, origin, updated_at)
317
+ VALUES (${input.conversationId}, ${input.path}, ${input.bytes}, ${input.mediaType ?? "application/octet-stream"}, ${input.bytes.byteLength}, 'assistant', now())
318
+ ON CONFLICT (conversation_id, path) DO UPDATE SET
319
+ bytes = ai.files.bytes || EXCLUDED.bytes,
320
+ size = ai.files.size + EXCLUDED.size,
321
+ updated_at = now(),
322
+ version = ai.files.version + 1
323
+ WHERE ai.files.origin = 'assistant'
324
+ RETURNING id
325
+ `;
326
+ if (!appended[0]) throw new Error(`Cannot append to user-uploaded file ${input.path}.`);
327
+ });
328
+ },
329
+
330
+ async remove(input: { conversationId: string; path: string; recursive?: boolean }): Promise<number> {
331
+ return sql.begin(async (tx) => {
332
+ await tx`SELECT id FROM ai.conversations WHERE id = ${input.conversationId} FOR UPDATE`;
333
+ if (input.recursive) {
334
+ const pattern = `${input.path.endsWith("/") ? input.path : `${input.path}/`}%`;
335
+ const rows = await tx<{ id: string }[]>`
336
+ DELETE FROM ai.files
337
+ WHERE conversation_id = ${input.conversationId} AND (path = ${input.path} OR path LIKE ${pattern})
338
+ RETURNING id
339
+ `;
340
+ return rows.length;
341
+ }
342
+ const rows = await tx<{ id: string }[]>`
343
+ DELETE FROM ai.files
344
+ WHERE conversation_id = ${input.conversationId} AND path = ${input.path}
345
+ RETURNING id
346
+ `;
347
+ return rows.length;
348
+ });
349
+ },
350
+
351
+ async rename(input: { conversationId: string; from: string; to: string }): Promise<"renamed" | "not_found" | "conflict"> {
352
+ return sql.begin(async (tx) => {
353
+ await tx`SELECT id FROM ai.conversations WHERE id = ${input.conversationId} FOR UPDATE`;
354
+ const source = await tx<{ id: string }[]>`
355
+ SELECT id FROM ai.files WHERE conversation_id = ${input.conversationId} AND path = ${input.from}
356
+ `;
357
+ if (!source[0]) return "not_found" as const;
358
+ const target = await tx<{ id: string }[]>`
359
+ SELECT id FROM ai.files WHERE conversation_id = ${input.conversationId} AND path = ${input.to}
360
+ `;
361
+ if (target[0]) return "conflict" as const;
362
+ await tx`UPDATE ai.files SET path = ${input.to}, updated_at = now(), version = version + 1 WHERE id = ${source[0].id}::uuid`;
363
+ return "renamed" as const;
364
+ });
365
+ },
366
+
367
+ /** Copy every file into another conversation (fork). */
368
+ async copyToConversation(input: { sourceConversationId: string; targetConversationId: string }): Promise<number> {
369
+ const rows = await sql<{ id: string }[]>`
370
+ INSERT INTO ai.files (conversation_id, path, bytes, media_type, size, origin)
371
+ SELECT ${input.targetConversationId}, path, bytes, media_type, size, origin
372
+ FROM ai.files
373
+ WHERE conversation_id = ${input.sourceConversationId}
374
+ ON CONFLICT (conversation_id, path) DO NOTHING
375
+ RETURNING id
376
+ `;
377
+ return rows.length;
378
+ },
379
+
380
+ async totalBytes(conversationId: string): Promise<number> {
381
+ const rows = await sql<{ total: number | string }[]>`
382
+ SELECT COALESCE(SUM(size), 0) AS total FROM ai.files WHERE conversation_id = ${conversationId}
383
+ `;
384
+ return Number(rows[0]?.total ?? 0);
385
+ },
386
+ };
387
+
388
+ /** Authorized services may expose this read after resolving the conversation owner. */
389
+ export const listAiConversationFiles = (conversationId: string, prefix?: string): Promise<AiFileStat[]> =>
390
+ aiFileStore.list({ conversationId, prefix });
@@ -0,0 +1,305 @@
1
+ import { isIP } from "node:net";
2
+ import { truncateMiddle } from "@k2b/nessi";
3
+ import { z } from "zod";
4
+ import { coreSettings } from "../services";
5
+ import { defineAiTool } from "./tools";
6
+
7
+ export const AI_FIRECRAWL_API_KEY_SETTING_KEY = "ai.firecrawl_api_key";
8
+
9
+ const FIRECRAWL_BASE_URL = "https://api.firecrawl.dev";
10
+ const FIRECRAWL_SEARCH_LIMIT = 5;
11
+ const FIRECRAWL_TIMEOUT_MS = 30_000;
12
+ const FIRECRAWL_CACHE_MAX_AGE_MS = 172_800_000;
13
+ const WEB_EXTRACT_MAX_CONTENT_CHARS = 1_000_000;
14
+ const WEB_SEARCH_HISTORY_SNIPPET_CHARS = 120;
15
+ const WEB_EXTRACT_HISTORY_CONTENT_CHARS = 4_000;
16
+
17
+ type FirecrawlFetch = typeof fetch;
18
+
19
+ type FirecrawlToolConfig = {
20
+ apiKey?: string | null;
21
+ fetch?: FirecrawlFetch;
22
+ };
23
+
24
+ export const CloudAiWebSearchInputSchema = z
25
+ .object({
26
+ query: z.string().trim().min(1).max(500),
27
+ })
28
+ .strict();
29
+
30
+ /** Flat result list — no billing metadata, the model only needs the sources. */
31
+ export const CloudAiWebSearchOutputSchema = z.array(
32
+ z.object({
33
+ title: z.string(),
34
+ url: z.string(),
35
+ snippet: z.string(),
36
+ position: z.number().int(),
37
+ }),
38
+ );
39
+
40
+ export const CloudAiWebExtractInputSchema = z
41
+ .object({
42
+ url: z.string().trim().url().max(2_000),
43
+ })
44
+ .strict();
45
+
46
+ export const CloudAiWebExtractOutputSchema = z.object({
47
+ url: z.string(),
48
+ title: z.string().optional(),
49
+ description: z.string().optional(),
50
+ content: z.string(),
51
+ truncated: z.boolean(),
52
+ });
53
+
54
+ const readFirecrawlApiKey = async (): Promise<string> =>
55
+ String((await coreSettings.get<string>(AI_FIRECRAWL_API_KEY_SETTING_KEY)) ?? "").trim();
56
+
57
+ export const isCloudAiFirecrawlConfigured = async () => (await readFirecrawlApiKey()).length > 0;
58
+
59
+ const resolveApiKey = async (apiKey: string | null | undefined) => {
60
+ const resolved = (apiKey ?? (await readFirecrawlApiKey())).trim();
61
+ if (!resolved) throw new Error("Firecrawl API key is not configured.");
62
+ return resolved;
63
+ };
64
+
65
+ const fetchWithTimeout = async (fetcher: FirecrawlFetch, url: string, init: RequestInit, signal?: AbortSignal): Promise<Response> => {
66
+ const controller = new AbortController();
67
+ const timeout = setTimeout(() => controller.abort(), FIRECRAWL_TIMEOUT_MS);
68
+ const abort = () => controller.abort();
69
+ if (signal?.aborted) controller.abort();
70
+ else signal?.addEventListener("abort", abort, { once: true });
71
+
72
+ try {
73
+ return await fetcher(url, { ...init, signal: controller.signal });
74
+ } finally {
75
+ clearTimeout(timeout);
76
+ signal?.removeEventListener("abort", abort);
77
+ }
78
+ };
79
+
80
+ const readJson = async (response: Response): Promise<unknown> => {
81
+ try {
82
+ return await response.json();
83
+ } catch {
84
+ return null;
85
+ }
86
+ };
87
+
88
+ const extractErrorMessage = (body: unknown): string | null => {
89
+ if (!body || typeof body !== "object") return null;
90
+ const record = body as Record<string, unknown>;
91
+ if (typeof record.error === "string") return record.error;
92
+ if (typeof record.message === "string") return record.message;
93
+ return null;
94
+ };
95
+
96
+ const firecrawlPost = async (input: {
97
+ apiKey?: string | null;
98
+ fetch?: FirecrawlFetch;
99
+ path: "/v2/search" | "/v2/scrape";
100
+ body: Record<string, unknown>;
101
+ signal?: AbortSignal;
102
+ }): Promise<unknown> => {
103
+ const apiKey = await resolveApiKey(input.apiKey);
104
+ const fetcher = input.fetch ?? fetch;
105
+ const response = await fetchWithTimeout(
106
+ fetcher,
107
+ `${FIRECRAWL_BASE_URL}${input.path}`,
108
+ {
109
+ method: "POST",
110
+ headers: {
111
+ Authorization: `Bearer ${apiKey}`,
112
+ "Content-Type": "application/json",
113
+ },
114
+ body: JSON.stringify(input.body),
115
+ },
116
+ input.signal,
117
+ );
118
+ const body = await readJson(response);
119
+ if (!response.ok) {
120
+ const message = extractErrorMessage(body) ?? response.statusText;
121
+ throw new Error(`Firecrawl request failed (HTTP ${response.status}): ${message}`);
122
+ }
123
+ return body;
124
+ };
125
+
126
+ const isRecord = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === "object" && !Array.isArray(value);
127
+ const asString = (value: unknown): string => (typeof value === "string" ? value.trim() : "");
128
+
129
+ const truncateSnippet = (text: string): string => {
130
+ if (text.length <= WEB_SEARCH_HISTORY_SNIPPET_CHARS) return text;
131
+ return `${text.slice(0, WEB_SEARCH_HISTORY_SNIPPET_CHARS - 3).trimEnd()}...`;
132
+ };
133
+
134
+ const truncate = (text: string, maxChars: number): { text: string; truncated: boolean } => {
135
+ if (text.length <= maxChars) return { text, truncated: false };
136
+ return { text: text.slice(0, Math.max(0, maxChars)).trimEnd(), truncated: true };
137
+ };
138
+
139
+ const isPrivateIpv4 = (hostname: string): boolean => {
140
+ const parts = hostname.split(".").map((part) => Number(part));
141
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true;
142
+ const [a, b] = parts as [number, number, number, number];
143
+ if (a === 0 || a === 10 || a === 127) return true;
144
+ if (a === 100 && b >= 64 && b <= 127) return true;
145
+ if (a === 169 && b === 254) return true;
146
+ if (a === 172 && b >= 16 && b <= 31) return true;
147
+ if (a === 192 && (b === 0 || b === 168)) return true;
148
+ if (a === 198 && (b === 18 || b === 19)) return true;
149
+ return a >= 224;
150
+ };
151
+
152
+ const isPrivateIpv6 = (hostname: string): boolean => {
153
+ const normalized = hostname.replace(/^\[|\]$/g, "").toLowerCase();
154
+ return (
155
+ normalized === "::" ||
156
+ normalized === "::1" ||
157
+ normalized.startsWith("fc") ||
158
+ normalized.startsWith("fd") ||
159
+ normalized.startsWith("fe80:") ||
160
+ normalized.startsWith("2001:db8:")
161
+ );
162
+ };
163
+
164
+ export const assertPublicHttpUrl = (rawUrl: string): string => {
165
+ let parsed: URL;
166
+ try {
167
+ parsed = new URL(rawUrl);
168
+ } catch {
169
+ throw new Error("URL must be valid.");
170
+ }
171
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Error("URL must use http or https.");
172
+
173
+ const hostname = parsed.hostname.toLowerCase();
174
+ if (!hostname || hostname === "localhost" || hostname.endsWith(".localhost")) {
175
+ throw new Error("URL host is not allowed.");
176
+ }
177
+
178
+ const ipKind = isIP(hostname.replace(/^\[|\]$/g, ""));
179
+ if (ipKind === 4 && isPrivateIpv4(hostname)) throw new Error("URL must not point to a private IP address.");
180
+ if (ipKind === 6 && isPrivateIpv6(hostname)) throw new Error("URL must not point to a private IP address.");
181
+ return parsed.toString();
182
+ };
183
+
184
+ export const runCloudAiWebSearch = async (
185
+ input: z.infer<typeof CloudAiWebSearchInputSchema>,
186
+ config: FirecrawlToolConfig & { signal?: AbortSignal } = {},
187
+ ): Promise<z.infer<typeof CloudAiWebSearchOutputSchema>> => {
188
+ const body = await firecrawlPost({
189
+ apiKey: config.apiKey,
190
+ fetch: config.fetch,
191
+ path: "/v2/search",
192
+ body: {
193
+ query: input.query,
194
+ limit: FIRECRAWL_SEARCH_LIMIT,
195
+ sources: ["web"],
196
+ timeout: FIRECRAWL_TIMEOUT_MS,
197
+ ignoreInvalidURLs: true,
198
+ },
199
+ signal: config.signal,
200
+ });
201
+ if (!isRecord(body) || body.success !== true) {
202
+ throw new Error(extractErrorMessage(body) ?? "Firecrawl search returned an invalid response.");
203
+ }
204
+
205
+ const data = isRecord(body.data) ? body.data : {};
206
+ const web = Array.isArray(data.web) ? data.web : [];
207
+ const results = web
208
+ .filter(isRecord)
209
+ .slice(0, FIRECRAWL_SEARCH_LIMIT)
210
+ .map((item, index) => ({
211
+ title: asString(item.title) || asString(isRecord(item.metadata) ? item.metadata.title : undefined) || "Untitled",
212
+ url: asString(item.url) || asString(isRecord(item.metadata) ? (item.metadata.url ?? item.metadata.sourceURL) : undefined),
213
+ snippet: asString(item.description) || asString(item.snippet),
214
+ position: index + 1,
215
+ }))
216
+ .filter((item) => item.url.length > 0);
217
+
218
+ return CloudAiWebSearchOutputSchema.parse(results);
219
+ };
220
+
221
+ export const runCloudAiWebExtract = async (
222
+ input: z.infer<typeof CloudAiWebExtractInputSchema>,
223
+ config: FirecrawlToolConfig & { signal?: AbortSignal; maxChars?: number } = {},
224
+ ): Promise<z.infer<typeof CloudAiWebExtractOutputSchema>> => {
225
+ const url = assertPublicHttpUrl(input.url);
226
+ const body = await firecrawlPost({
227
+ apiKey: config.apiKey,
228
+ fetch: config.fetch,
229
+ path: "/v2/scrape",
230
+ body: {
231
+ url,
232
+ formats: ["markdown"],
233
+ onlyMainContent: true,
234
+ maxAge: FIRECRAWL_CACHE_MAX_AGE_MS,
235
+ timeout: FIRECRAWL_TIMEOUT_MS,
236
+ removeBase64Images: true,
237
+ blockAds: true,
238
+ },
239
+ signal: config.signal,
240
+ });
241
+ if (!isRecord(body) || body.success !== true || !isRecord(body.data)) {
242
+ throw new Error(extractErrorMessage(body) ?? "Firecrawl extract returned an invalid response.");
243
+ }
244
+
245
+ const maxChars = config.maxChars ?? WEB_EXTRACT_MAX_CONTENT_CHARS;
246
+ const data = body.data;
247
+ const metadata = isRecord(data.metadata) ? data.metadata : {};
248
+ const normalizedUrl = asString(data.url) || asString(metadata.url) || asString(metadata.sourceURL) || url;
249
+ const content = asString(data.markdown);
250
+ const truncated = truncate(content, maxChars);
251
+
252
+ return CloudAiWebExtractOutputSchema.parse({
253
+ url: normalizedUrl,
254
+ title: asString(metadata.title) || asString(data.title) || undefined,
255
+ description: asString(metadata.description) || asString(data.description) || undefined,
256
+ content: truncated.text,
257
+ truncated: truncated.truncated,
258
+ });
259
+ };
260
+
261
+ export const createCloudAiWebSearchTool = (config: FirecrawlToolConfig = {}) =>
262
+ defineAiTool({
263
+ name: "web_search",
264
+ description:
265
+ "Search the web for current sources. Input only a plain natural-language query. Search again with a focused query when the first results are incomplete, outdated, or conflicting.",
266
+ inputSchema: CloudAiWebSearchInputSchema,
267
+ outputSchema: CloudAiWebSearchOutputSchema,
268
+ approval: "never",
269
+ timeoutMs: 90_000,
270
+ promptHint:
271
+ "search the web for current facts and sources; use additional focused searches when the evidence is incomplete or conflicting.",
272
+ toHistoricalResult: ({ output }) =>
273
+ output.map((result) => ({
274
+ ...result,
275
+ snippet: truncateSnippet(result.snippet),
276
+ })),
277
+ }).server(async (input, ctx) => runCloudAiWebSearch(input, { ...config, signal: ctx.signal }));
278
+
279
+ export const createCloudAiWebExtractTool = (config: FirecrawlToolConfig = {}) =>
280
+ defineAiTool({
281
+ name: "web_extract",
282
+ description:
283
+ "Read one web page by URL and return clean Markdown. Inspect the relevant pages needed to support the answer; for research or comparison, prefer primary sources and read more than one useful source when warranted.",
284
+ inputSchema: CloudAiWebExtractInputSchema,
285
+ outputSchema: CloudAiWebExtractOutputSchema,
286
+ approval: "never",
287
+ timeoutMs: 90_000,
288
+ promptHint:
289
+ "read relevant pages found by web_search; for research or comparison, inspect multiple useful sources and prefer primary sources.",
290
+ toHistoricalResult: ({ output }) => {
291
+ const content = truncateMiddle(output.content, WEB_EXTRACT_HISTORY_CONTENT_CHARS);
292
+ return {
293
+ url: output.url,
294
+ title: output.title,
295
+ description: output.description,
296
+ content,
297
+ truncated: output.truncated || content !== output.content,
298
+ };
299
+ },
300
+ }).server(async (input, ctx) => runCloudAiWebExtract(input, { ...config, signal: ctx.signal }));
301
+
302
+ export type CloudAiWebSearchInput = z.infer<typeof CloudAiWebSearchInputSchema>;
303
+ export type CloudAiWebSearchOutput = z.infer<typeof CloudAiWebSearchOutputSchema>;
304
+ export type CloudAiWebExtractInput = z.infer<typeof CloudAiWebExtractInputSchema>;
305
+ export type CloudAiWebExtractOutput = z.infer<typeof CloudAiWebExtractOutputSchema>;