@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,529 @@
1
+ import { z } from "zod";
2
+ import { coreSettings } from "../services";
3
+ import { getAiCredential, listAiCredentialProfileIds } from "./credentials";
4
+ import { AI_FIRECRAWL_API_KEY_SETTING_KEY } from "./firecrawl-tools";
5
+ import { createAiProvider } from "./provider";
6
+ import {
7
+ AI_DATA_BOUNDARIES,
8
+ AI_MODEL_CAPABILITIES,
9
+ type AiDataBoundary,
10
+ type AiModelCapability,
11
+ type AiModelPolicy,
12
+ type AiModelProfile,
13
+ type AiProviderId,
14
+ type AiPublicModelProfile,
15
+ type AiResolvedModel,
16
+ type AiSettingsError,
17
+ type AiSettingsState,
18
+ } from "./types";
19
+
20
+ const PROVIDERS = ["openai", "openrouter", "anthropic", "mistral", "gemini", "ollama", "vllm", "openai-compatible"] as const;
21
+ const LEGACY_DATA_BOUNDARIES = ["local", "internal"] as const;
22
+ const DATA_BOUNDARY_INPUTS = [...AI_DATA_BOUNDARIES, ...LEGACY_DATA_BOUNDARIES] as const;
23
+ const HttpUrlSchema = z
24
+ .string()
25
+ .trim()
26
+ .url()
27
+ .refine((value) => {
28
+ const protocol = new URL(value).protocol;
29
+ return protocol === "http:" || protocol === "https:";
30
+ }, "Provider endpoints must use HTTP or HTTPS.");
31
+
32
+ export const providerRequiresCredential = (provider: AiProviderId): boolean =>
33
+ provider === "openai" || provider === "openrouter" || provider === "anthropic" || provider === "mistral" || provider === "gemini";
34
+
35
+ export const providerSupportsCredential = (provider: AiProviderId): boolean =>
36
+ providerRequiresCredential(provider) || provider === "vllm" || provider === "openai-compatible";
37
+
38
+ const defaultDataBoundary = (provider: AiProviderId): AiDataBoundary =>
39
+ provider === "ollama" || provider === "vllm" || provider === "openai-compatible" ? "private" : "hosted";
40
+
41
+ const normalizeDataBoundary = (boundary: (typeof DATA_BOUNDARY_INPUTS)[number] | undefined, provider: AiProviderId): AiDataBoundary => {
42
+ if (boundary === "hosted") return "hosted";
43
+ if (boundary === "private" || boundary === "local" || boundary === "internal") return "private";
44
+ return defaultDataBoundary(provider);
45
+ };
46
+
47
+ const isModelCapability = (value: string): value is AiModelCapability => AI_MODEL_CAPABILITIES.some((capability) => capability === value);
48
+
49
+ // Keep in sync with the ai.max_tool_result_chars default in core-settings.ts.
50
+ // This is an operator ceiling. The runtime derives the actual per-result
51
+ // budget from the selected model's context window.
52
+ const DEFAULT_MAX_TOOL_RESULT_CHARS = 2_000_000;
53
+
54
+ const normalizeMaxToolResultChars = (value: unknown): number => {
55
+ const numeric = Number(value);
56
+ if (!Number.isFinite(numeric) || numeric <= 0) return DEFAULT_MAX_TOOL_RESULT_CHARS;
57
+ return Math.floor(numeric);
58
+ };
59
+
60
+ const normalizeCapabilities = (values: string[] | undefined): AiModelCapability[] => {
61
+ if (!values) return ["streaming"];
62
+ const capabilities = values.filter(isModelCapability);
63
+ return [...new Set(capabilities)];
64
+ };
65
+
66
+ const ModelProfileSchema = z
67
+ .object({
68
+ id: z
69
+ .string()
70
+ .trim()
71
+ .min(1)
72
+ .regex(/^[a-z0-9][a-z0-9._-]*$/),
73
+ label: z.string().trim().min(1),
74
+ provider: z.enum(PROVIDERS),
75
+ model: z.string().trim().min(1),
76
+ enabled: z.boolean().default(true),
77
+ // Small logo shown in the admin card and the composer model picker. Hard cap:
78
+ // it ships to every user via the public model list (64x64 webp ≈ a few KB).
79
+ image: z
80
+ .string()
81
+ .trim()
82
+ .regex(/^data:image\//)
83
+ .max(28_000)
84
+ .optional(),
85
+ // Legacy/advanced metadata is tolerated but no longer used for model selection.
86
+ tags: z.array(z.string()).optional(),
87
+ capabilities: z.array(z.string()).optional(),
88
+ dataBoundary: z.enum(DATA_BOUNDARY_INPUTS).optional(),
89
+ // Legacy name accepted for stored profiles created before dataBoundary.
90
+ dataPolicy: z.enum(DATA_BOUNDARY_INPUTS).optional(),
91
+ baseURL: HttpUrlSchema.optional(),
92
+ contextWindow: z.number().int().positive().optional(),
93
+ temperature: z.number().min(0).max(2).optional(),
94
+ maxOutputTokens: z.number().int().positive().optional(),
95
+ maxLoadedTools: z.number().int().optional(),
96
+ maxToolRounds: z.number().int().optional(),
97
+ creditsPerInputToken: z.number().nonnegative().optional(),
98
+ creditsPerOutputToken: z.number().nonnegative().optional(),
99
+ })
100
+ .superRefine((profile, ctx) => {
101
+ if (profile.provider === "openai-compatible" && !profile.baseURL) {
102
+ ctx.addIssue({ code: "custom", path: ["baseURL"], message: "OpenAI-compatible profiles require a base URL." });
103
+ }
104
+ });
105
+
106
+ const profileToPublic = (profile: AiModelProfile): AiPublicModelProfile => ({
107
+ id: profile.id,
108
+ label: profile.label,
109
+ provider: profile.provider,
110
+ model: profile.model,
111
+ image: profile.image,
112
+ capabilities: profile.capabilities,
113
+ dataBoundary: profile.dataBoundary,
114
+ contextWindow: profile.contextWindow,
115
+ });
116
+
117
+ const normalizeProfile = (raw: z.infer<typeof ModelProfileSchema>): AiModelProfile => {
118
+ const { capabilities, dataBoundary, dataPolicy: legacyDataPolicy, tags: _legacyTags, ...profile } = raw;
119
+ return {
120
+ ...profile,
121
+ capabilities: normalizeCapabilities(capabilities),
122
+ dataBoundary: normalizeDataBoundary(dataBoundary ?? legacyDataPolicy, raw.provider),
123
+ };
124
+ };
125
+
126
+ export const parseAiModelProfiles = (rawJson: string): { profiles: AiModelProfile[]; error?: AiSettingsError } => {
127
+ let raw: unknown;
128
+ try {
129
+ raw = rawJson.trim() ? JSON.parse(rawJson) : [];
130
+ } catch (error) {
131
+ return {
132
+ profiles: [],
133
+ error: {
134
+ code: "invalid_model_profiles",
135
+ message: "AI model profiles must be valid JSON.",
136
+ fields: { "ai.model_profiles_json": error instanceof Error ? error.message : "Invalid JSON" },
137
+ },
138
+ };
139
+ }
140
+
141
+ const parsed = z.array(ModelProfileSchema).safeParse(raw);
142
+ if (!parsed.success) {
143
+ return {
144
+ profiles: [],
145
+ error: {
146
+ code: "invalid_model_profiles",
147
+ message: "AI model profiles do not match the expected shape.",
148
+ fields: { "ai.model_profiles_json": z.prettifyError(parsed.error) },
149
+ },
150
+ };
151
+ }
152
+
153
+ const seen = new Set<string>();
154
+ const duplicate = parsed.data.find((profile) => {
155
+ if (seen.has(profile.id)) return true;
156
+ seen.add(profile.id);
157
+ return false;
158
+ });
159
+
160
+ if (duplicate) {
161
+ return {
162
+ profiles: [],
163
+ error: {
164
+ code: "invalid_model_profiles",
165
+ message: `Duplicate AI model profile id "${duplicate.id}".`,
166
+ fields: { "ai.model_profiles_json": `Duplicate model profile id "${duplicate.id}".` },
167
+ },
168
+ };
169
+ }
170
+
171
+ const profiles = parsed.data.map(normalizeProfile);
172
+ return { profiles };
173
+ };
174
+
175
+ export const validateAiSettingsConfiguration = (input: {
176
+ enabled: boolean;
177
+ defaultModelId: string;
178
+ backgroundModelId: string;
179
+ visionModelId?: string;
180
+ workflowModelId: string;
181
+ profiles: readonly AiModelProfile[];
182
+ credentialProfileIds: readonly string[];
183
+ }): Record<string, string> => {
184
+ if (!input.enabled) return {};
185
+
186
+ const errors: Record<string, string> = {};
187
+ const enabledProfiles = input.profiles.filter((profile) => profile.enabled);
188
+ const defaultProfile = input.profiles.find((profile) => profile.id === input.defaultModelId);
189
+ if (!input.defaultModelId || !defaultProfile?.enabled) {
190
+ errors["ai.default_model_id"] = "Choose an enabled model profile.";
191
+ }
192
+
193
+ if (input.backgroundModelId) {
194
+ const backgroundProfile = input.profiles.find((profile) => profile.id === input.backgroundModelId);
195
+ if (!backgroundProfile?.enabled) errors["ai.background_model_id"] = "Choose an enabled model profile or use the platform default.";
196
+ }
197
+
198
+ if (input.visionModelId) {
199
+ const visionProfile = input.profiles.find((profile) => profile.id === input.visionModelId);
200
+ if (!visionProfile?.enabled || !visionProfile.capabilities.includes("vision")) {
201
+ errors["ai.vision_model_id"] = "Choose an enabled model profile with Vision support or disable the fallback.";
202
+ }
203
+ }
204
+
205
+ if (input.workflowModelId) {
206
+ const workflowProfile = input.profiles.find((profile) => profile.id === input.workflowModelId);
207
+ if (!workflowProfile?.enabled) errors["ai.workflow_model_id"] = "Choose an enabled model profile or use the background model.";
208
+ }
209
+
210
+ const configured = new Set(input.credentialProfileIds);
211
+ const missingCredentials = enabledProfiles.filter(
212
+ (profile) => providerRequiresCredential(profile.provider) && !configured.has(profile.id),
213
+ );
214
+ if (missingCredentials.length > 0) {
215
+ errors["ai.model_profiles_json"] = `Enter a provider API key for: ${missingCredentials.map((profile) => profile.label).join(", ")}.`;
216
+ }
217
+
218
+ return errors;
219
+ };
220
+
221
+ export const planAiProfileCredentials = (input: {
222
+ currentProfiles: readonly AiModelProfile[];
223
+ nextProfiles: readonly AiModelProfile[];
224
+ existingCredentialProfileIds: readonly string[];
225
+ submittedCredentialProfileIds: readonly string[];
226
+ }): { keepCredentialProfileIds: string[]; unsupportedCredentialProfileId?: string } => {
227
+ const nextProfilesById = new Map(input.nextProfiles.map((profile) => [profile.id, profile]));
228
+ const unsupportedCredentialProfileId = input.submittedCredentialProfileIds.find((profileId) => {
229
+ const profile = nextProfilesById.get(profileId);
230
+ return !profile || !providerSupportsCredential(profile.provider);
231
+ });
232
+ if (unsupportedCredentialProfileId) return { keepCredentialProfileIds: [], unsupportedCredentialProfileId };
233
+
234
+ const existingCredentials = new Set(input.existingCredentialProfileIds);
235
+ const submittedCredentials = new Set(input.submittedCredentialProfileIds);
236
+ const currentProfilesById = new Map(input.currentProfiles.map((profile) => [profile.id, profile]));
237
+ const keepCredentialProfileIds = input.nextProfiles
238
+ .filter((profile) => {
239
+ if (!providerSupportsCredential(profile.provider)) return false;
240
+ if (submittedCredentials.has(profile.id)) return true;
241
+ const current = currentProfilesById.get(profile.id);
242
+ return current?.provider === profile.provider && existingCredentials.has(profile.id);
243
+ })
244
+ .map((profile) => profile.id);
245
+
246
+ return { keepCredentialProfileIds };
247
+ };
248
+
249
+ export const resolveAiSettingsStateFromRaw = async (input: {
250
+ enabled: boolean;
251
+ defaultModelId: string;
252
+ visionModelId?: string;
253
+ profilesJson: string;
254
+ globalInstructions?: string;
255
+ compactionInstructions?: string;
256
+ maxToolResultChars?: unknown;
257
+ firecrawlApiKey?: string;
258
+ credentialProfileIds?: readonly string[];
259
+ readCredential?: (profileId: string) => Promise<string | null | undefined>;
260
+ }): Promise<AiSettingsState> => {
261
+ const parsed = parseAiModelProfiles(input.profilesJson ?? "[]");
262
+ const baseState = {
263
+ enabled: Boolean(input.enabled),
264
+ defaultModelId: input.defaultModelId ?? "",
265
+ visionModelId: input.visionModelId ?? "",
266
+ globalInstructions: input.globalInstructions ?? "",
267
+ compactionInstructions: input.compactionInstructions ?? "",
268
+ maxToolResultChars: normalizeMaxToolResultChars(input.maxToolResultChars),
269
+ firecrawlConfigured: Boolean(input.firecrawlApiKey?.trim()),
270
+ };
271
+ if (parsed.error) {
272
+ return {
273
+ ok: false,
274
+ ...baseState,
275
+ profiles: parsed.profiles,
276
+ error: parsed.error,
277
+ };
278
+ }
279
+
280
+ if (!input.enabled) {
281
+ return {
282
+ ok: true,
283
+ ...baseState,
284
+ enabled: false,
285
+ profiles: parsed.profiles,
286
+ };
287
+ }
288
+
289
+ const defaultProfile = parsed.profiles.find((profile) => profile.id === input.defaultModelId);
290
+ if (!input.defaultModelId || !defaultProfile) {
291
+ return {
292
+ ok: false,
293
+ ...baseState,
294
+ enabled: true,
295
+ profiles: parsed.profiles,
296
+ error: {
297
+ code: "missing_default_model",
298
+ message: "AI is enabled but no valid default model profile is configured.",
299
+ fields: { "ai.default_model_id": "Choose an enabled model profile id." },
300
+ },
301
+ };
302
+ }
303
+
304
+ if (!defaultProfile.enabled) {
305
+ return {
306
+ ok: false,
307
+ ...baseState,
308
+ enabled: true,
309
+ profiles: parsed.profiles,
310
+ error: {
311
+ code: "default_model_disabled",
312
+ message: `Default AI model "${input.defaultModelId}" is disabled.`,
313
+ fields: { "ai.default_model_id": "Choose an enabled model profile id." },
314
+ },
315
+ };
316
+ }
317
+
318
+ if (providerRequiresCredential(defaultProfile.provider)) {
319
+ let credentialConfigured = input.credentialProfileIds?.includes(defaultProfile.id) ?? false;
320
+ if (!input.credentialProfileIds && input.readCredential) {
321
+ try {
322
+ credentialConfigured = Boolean((await input.readCredential(defaultProfile.id))?.trim());
323
+ } catch {
324
+ credentialConfigured = false;
325
+ }
326
+ }
327
+ if (!credentialConfigured) {
328
+ return {
329
+ ok: false,
330
+ ...baseState,
331
+ enabled: true,
332
+ profiles: parsed.profiles,
333
+ error: {
334
+ code: "missing_provider_credential",
335
+ message: `Default AI model "${input.defaultModelId}" is missing provider credentials.`,
336
+ fields: { "ai.model_profiles_json": "Enter the provider API key on the default model profile." },
337
+ },
338
+ };
339
+ }
340
+ }
341
+
342
+ return {
343
+ ok: true,
344
+ ...baseState,
345
+ enabled: true,
346
+ profiles: parsed.profiles,
347
+ };
348
+ };
349
+
350
+ const readAiSettingsSnapshot = async (): Promise<{ state: AiSettingsState; credentialProfileIds: string[] }> => {
351
+ const [
352
+ enabled,
353
+ defaultModelId,
354
+ visionModelId,
355
+ profilesJson,
356
+ globalInstructions,
357
+ compactionInstructions,
358
+ maxToolResultChars,
359
+ firecrawlApiKey,
360
+ ] = await Promise.all([
361
+ coreSettings.get<boolean>("ai.enabled"),
362
+ coreSettings.get<string>("ai.default_model_id"),
363
+ coreSettings.get<string>("ai.vision_model_id"),
364
+ coreSettings.get<string>("ai.model_profiles_json"),
365
+ coreSettings.get<string>("ai.global_instructions"),
366
+ coreSettings.get<string>("ai.compaction_instructions"),
367
+ coreSettings.get<number>("ai.max_tool_result_chars"),
368
+ coreSettings.get<string>(AI_FIRECRAWL_API_KEY_SETTING_KEY),
369
+ ]);
370
+
371
+ const parsed = parseAiModelProfiles(profilesJson ?? "[]");
372
+ const defaultProfile = parsed.profiles.find((profile) => profile.id === defaultModelId);
373
+ const needsCredentials =
374
+ Boolean(enabled) &&
375
+ !parsed.error &&
376
+ Boolean(defaultProfile?.enabled) &&
377
+ parsed.profiles.some((profile) => profile.enabled && providerRequiresCredential(profile.provider));
378
+ const credentialProfileIds = needsCredentials ? await listAiCredentialProfileIds() : [];
379
+
380
+ const state = await resolveAiSettingsStateFromRaw({
381
+ enabled: Boolean(enabled),
382
+ defaultModelId: defaultModelId ?? "",
383
+ visionModelId: visionModelId ?? "",
384
+ profilesJson: profilesJson ?? "[]",
385
+ globalInstructions: globalInstructions ?? "",
386
+ compactionInstructions: compactionInstructions ?? "",
387
+ maxToolResultChars,
388
+ firecrawlApiKey: firecrawlApiKey ?? "",
389
+ credentialProfileIds,
390
+ });
391
+ return { state, credentialProfileIds };
392
+ };
393
+
394
+ export const readAiSettingsState = async (): Promise<AiSettingsState> => (await readAiSettingsSnapshot()).state;
395
+
396
+ const hasAll = <T extends string>(values: readonly T[], required: readonly T[] | undefined): boolean =>
397
+ !required || required.every((requiredValue) => values.includes(requiredValue));
398
+
399
+ const matchesPolicy = (profile: AiModelProfile, policy: AiModelPolicy): boolean => {
400
+ if (!profile.enabled) return false;
401
+ if ("allowedModelIds" in policy && policy.allowedModelIds && !policy.allowedModelIds.includes(profile.id)) return false;
402
+ if (policy.allowedDataBoundaries && !policy.allowedDataBoundaries.includes(profile.dataBoundary)) return false;
403
+ return hasAll(profile.capabilities, policy.requiredCapabilities);
404
+ };
405
+
406
+ const resolvePolicyModelId = (state: Extract<AiSettingsState, { ok: true }>, policy: AiModelPolicy, requestedModelId?: string): string => {
407
+ if (policy.kind === "locked") return policy.modelId;
408
+ if (policy.kind === "selectable") return requestedModelId ?? policy.defaultModelId ?? state.defaultModelId;
409
+ return state.defaultModelId;
410
+ };
411
+
412
+ export const selectAiModelProfile = (
413
+ state: Extract<AiSettingsState, { ok: true }>,
414
+ policy: AiModelPolicy,
415
+ requestedModelId?: string,
416
+ ): AiModelProfile => {
417
+ const modelId = resolvePolicyModelId(state, policy, requestedModelId);
418
+ const profile = state.profiles.find((candidate) => candidate.id === modelId);
419
+ if (!profile || !matchesPolicy(profile, policy)) {
420
+ throw Object.assign(new Error(`AI model "${modelId}" is not allowed for this chat.`), {
421
+ aiError: {
422
+ code: "model_policy_mismatch",
423
+ message: `AI model "${modelId}" is not allowed for this chat.`,
424
+ fields: { modelProfileId: "Choose an allowed enabled model profile." },
425
+ } satisfies AiSettingsError,
426
+ });
427
+ }
428
+ return profile;
429
+ };
430
+
431
+ export const resolveAiModel = async (
432
+ policy: AiModelPolicy = { kind: "platform-default" },
433
+ requestedModelId?: string,
434
+ ): Promise<AiResolvedModel> => {
435
+ const state = await readAiSettingsState();
436
+ return resolveAiModelFromState(state, policy, requestedModelId);
437
+ };
438
+
439
+ const visionModelPolicy = (modelId: string, allowedDataBoundaries?: AiDataBoundary[]): AiModelPolicy => ({
440
+ kind: "locked",
441
+ modelId,
442
+ requiredCapabilities: ["vision"],
443
+ allowedDataBoundaries,
444
+ });
445
+
446
+ export const resolveAiVisionModel = async (allowedDataBoundaries?: AiDataBoundary[]): Promise<AiResolvedModel> => {
447
+ const state = await readAiSettingsState();
448
+ if (!state.ok) throw Object.assign(new Error(state.error.message), { aiError: state.error });
449
+ const modelId = (state.visionModelId ?? "").trim();
450
+ if (!modelId) throw new Error("No vision model is configured.");
451
+ return resolveAiModelFromState(state, visionModelPolicy(modelId, allowedDataBoundaries));
452
+ };
453
+
454
+ export const isAiVisionModelConfigured = async (allowedDataBoundaries?: AiDataBoundary[]): Promise<boolean> => {
455
+ const { state, credentialProfileIds } = await readAiSettingsSnapshot();
456
+ if (!state.ok || !state.enabled || !state.visionModelId?.trim()) return false;
457
+ try {
458
+ const profile = selectAiModelProfile(state, visionModelPolicy(state.visionModelId, allowedDataBoundaries));
459
+ return isUsableProfile(profile, new Set(credentialProfileIds));
460
+ } catch {
461
+ return false;
462
+ }
463
+ };
464
+
465
+ export const resolveAiModelFromState = async (
466
+ state: AiSettingsState,
467
+ policy: AiModelPolicy = { kind: "platform-default" },
468
+ requestedModelId?: string,
469
+ ): Promise<AiResolvedModel> => {
470
+ if (!state.ok) throw Object.assign(new Error(state.error.message), { aiError: state.error });
471
+ if (!state.enabled) {
472
+ throw Object.assign(new Error("AI is disabled."), {
473
+ aiError: { code: "ai_disabled", message: "AI is disabled." } satisfies AiSettingsError,
474
+ });
475
+ }
476
+
477
+ const profile = selectAiModelProfile(state, policy, requestedModelId);
478
+
479
+ const credential = providerSupportsCredential(profile.provider) ? await getAiCredential(profile.id) : null;
480
+ if (providerRequiresCredential(profile.provider) && !credential?.trim()) {
481
+ throw Object.assign(new Error(`AI model "${profile.id}" is missing provider credentials.`), {
482
+ aiError: {
483
+ code: "missing_provider_credential",
484
+ message: `AI model "${profile.id}" is missing provider credentials.`,
485
+ fields: { "ai.model_profiles_json": "Enter the provider API key on this model profile." },
486
+ } satisfies AiSettingsError,
487
+ });
488
+ }
489
+
490
+ return { profile, provider: createAiProvider(profile, credential?.trim() || undefined) };
491
+ };
492
+
493
+ const isUsableProfile = (profile: AiModelProfile, credentialProfileIds: ReadonlySet<string>): boolean =>
494
+ !providerRequiresCredential(profile.provider) || credentialProfileIds.has(profile.id);
495
+
496
+ export const listAiModels = async (policy: AiModelPolicy = { kind: "selectable" }): Promise<AiPublicModelProfile[]> => {
497
+ const { state, credentialProfileIds } = await readAiSettingsSnapshot();
498
+ if (!state.ok || !state.enabled) return [];
499
+ const configured = new Set(credentialProfileIds);
500
+ return state.profiles.filter((profile) => matchesPolicy(profile, policy) && isUsableProfile(profile, configured)).map(profileToPublic);
501
+ };
502
+
503
+ export const toPublicAiSettingsState = async (allowedDataBoundaries?: AiDataBoundary[]) => {
504
+ const { state, credentialProfileIds } = await readAiSettingsSnapshot();
505
+ const configured = new Set(credentialProfileIds);
506
+ let visionModelConfigured = false;
507
+ if (state.ok && state.enabled && state.visionModelId?.trim()) {
508
+ try {
509
+ const profile = selectAiModelProfile(state, visionModelPolicy(state.visionModelId, allowedDataBoundaries));
510
+ visionModelConfigured = isUsableProfile(profile, configured);
511
+ } catch {
512
+ // The configured auxiliary model is outside this application's policy.
513
+ }
514
+ }
515
+ return {
516
+ ok: state.ok,
517
+ enabled: state.enabled,
518
+ defaultModelId: state.defaultModelId,
519
+ visionModelConfigured,
520
+ error: state.ok ? null : state.error,
521
+ firecrawlConfigured: state.firecrawlConfigured,
522
+ models:
523
+ state.ok && state.enabled
524
+ ? state.profiles.filter((profile) => profile.enabled && isUsableProfile(profile, configured)).map(profileToPublic)
525
+ : [],
526
+ };
527
+ };
528
+
529
+ export type { AiDataBoundary, AiModelCapability, AiModelPolicy, AiModelProfile };
@@ -0,0 +1,45 @@
1
+ import { crypto } from "@k2b/stdlib";
2
+ import type { SQL } from "bun";
3
+
4
+ export const AI_SHORT_ID_PATTERN = /^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz]{6}$/;
5
+
6
+ export const createAiShortId = (): string => crypto.common.readableId(6);
7
+
8
+ export const withAiShortId = async <T>(
9
+ constraint: string,
10
+ insert: (shortId: string) => Promise<T>,
11
+ allocate: () => string = createAiShortId,
12
+ ): Promise<T> => {
13
+ for (let attempt = 0; attempt < 5; attempt++) {
14
+ try {
15
+ return await insert(allocate());
16
+ } catch (error) {
17
+ const collision =
18
+ typeof error === "object" &&
19
+ error !== null &&
20
+ "code" in error &&
21
+ error.code === "23505" &&
22
+ "constraint" in error &&
23
+ error.constraint === constraint;
24
+ if (!collision) throw error;
25
+ }
26
+ }
27
+ throw new Error("Failed to allocate a unique AI short ID");
28
+ };
29
+
30
+ type TransactionSql = SQL & { savepoint: <T>(fn: (db: SQL) => Promise<T>) => Promise<T> };
31
+
32
+ const isTransaction = (db: SQL): db is TransactionSql => typeof (db as Partial<TransactionSql>).savepoint === "function";
33
+
34
+ /** Keep a short-ID collision from aborting the caller's surrounding transaction. */
35
+ export const withAiShortIdForDb = <T>(
36
+ db: SQL,
37
+ constraint: string,
38
+ insert: (db: SQL, shortId: string) => Promise<T>,
39
+ allocate: () => string = createAiShortId,
40
+ ): Promise<T> =>
41
+ withAiShortId(
42
+ constraint,
43
+ (shortId) => (isTransaction(db) ? db.savepoint((attempt) => insert(attempt, shortId)) : insert(db, shortId)),
44
+ allocate,
45
+ );
@@ -0,0 +1,73 @@
1
+ import { AI_SKILL_DESCRIPTION_MAX_CHARS, AI_SKILL_NAME_MAX_CHARS } from "./skill-format";
2
+ import type { AiSkillSummary } from "./skills";
3
+
4
+ export const AI_SKILL_CATALOG_MAX_CHARS = 8_000;
5
+ const AI_SKILL_CATALOG_MIN_CHARS = AI_SKILL_NAME_MAX_CHARS + AI_SKILL_DESCRIPTION_MAX_CHARS + 8;
6
+
7
+ export type AiSkillCatalogEntry = Pick<AiSkillSummary, "name" | "description">;
8
+
9
+ const catalogLine = (skill: AiSkillCatalogEntry): string => `- ${skill.name}: ${skill.description.replace(/\s+/g, " ").trim()}`;
10
+
11
+ const terms = (value: string): string[] => [
12
+ ...new Set(
13
+ value
14
+ .toLocaleLowerCase()
15
+ .split(/[^\p{L}\p{N}]+/u)
16
+ .filter((term) => term.length >= 2),
17
+ ),
18
+ ];
19
+
20
+ const relevance = (skill: AiSkillCatalogEntry, query: string): number => {
21
+ const normalizedQuery = query.toLocaleLowerCase().trim();
22
+ if (!normalizedQuery) return 0;
23
+ const name = skill.name.toLocaleLowerCase();
24
+ const description = skill.description.toLocaleLowerCase();
25
+ let score = name.includes(normalizedQuery) ? 100 : 0;
26
+ for (const term of terms(normalizedQuery)) {
27
+ if (name.includes(term)) score += 20;
28
+ if (description.includes(term)) score += 3;
29
+ }
30
+ return score;
31
+ };
32
+
33
+ const ranked = (skills: readonly AiSkillCatalogEntry[], query: string): AiSkillCatalogEntry[] =>
34
+ skills
35
+ .map((skill, index) => ({ skill, index, score: relevance(skill, query) }))
36
+ .sort((left, right) => right.score - left.score || left.index - right.index)
37
+ .map(({ skill }) => skill);
38
+
39
+ export const selectAiSkillCatalog = (
40
+ skills: readonly AiSkillCatalogEntry[],
41
+ contextWindow: number,
42
+ query: string,
43
+ ): { skills: AiSkillCatalogEntry[]; omitted: number } => {
44
+ if (!skills.length) return { skills: [], omitted: 0 };
45
+ const budget = Math.min(
46
+ AI_SKILL_CATALOG_MAX_CHARS,
47
+ Math.max(AI_SKILL_CATALOG_MIN_CHARS, contextWindow > 0 ? Math.floor(contextWindow * 0.02 * 4) : AI_SKILL_CATALOG_MAX_CHARS),
48
+ );
49
+ const normalized = skills.map((skill) => ({ name: skill.name, description: skill.description.replace(/\s+/g, " ").trim() }));
50
+ if (normalized.map(catalogLine).join("\n").length <= budget) return { skills: normalized, omitted: 0 };
51
+
52
+ const selected: AiSkillCatalogEntry[] = [];
53
+ let used = 0;
54
+ for (const skill of ranked(normalized, query)) {
55
+ const size = catalogLine(skill).length + (selected.length ? 1 : 0);
56
+ if (used + size > budget) continue;
57
+ selected.push(skill);
58
+ used += size;
59
+ }
60
+ return { skills: selected, omitted: normalized.length - selected.length };
61
+ };
62
+
63
+ export const searchAiSkillCatalog = (
64
+ skills: readonly AiSkillCatalogEntry[],
65
+ query: string,
66
+ limit: number,
67
+ ): { skills: AiSkillCatalogEntry[]; more: boolean } => {
68
+ const normalized = skills.map((skill) => ({ name: skill.name, description: skill.description }));
69
+ const matches = ranked(normalized, query).filter((skill) => relevance(skill, query) > 0);
70
+ return { skills: matches.slice(0, limit), more: matches.length > limit };
71
+ };
72
+
73
+ export const aiSkillCatalogChars = (skills: readonly AiSkillCatalogEntry[]): number => skills.map(catalogLine).join("\n").length;