@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,934 @@
1
+ import { type SQL, type SQLQuery, sql } from "bun";
2
+ import type { AccessSubject } from "../server";
3
+ import {
4
+ buildAccessPrincipalCondition,
5
+ createAccess,
6
+ deleteAccess,
7
+ hasPermission,
8
+ type PermissionLevel,
9
+ type Principal,
10
+ } from "../server/services/access";
11
+ import { toPgUuidArray } from "../services/postgres";
12
+ import { mountAiSkillFilePath } from "./file-mount";
13
+ import { AI_SHORT_ID_PATTERN, withAiShortIdForDb } from "./short-id";
14
+ import {
15
+ type AiSkillExtraFrontmatter,
16
+ type AiSkillReferenceInput,
17
+ serializeAiSkillMarkdown,
18
+ validateAiSkillDescription,
19
+ validateAiSkillExtraFrontmatter,
20
+ validateAiSkillInstructions,
21
+ validateAiSkillName,
22
+ validateAiSkillReferences,
23
+ } from "./skill-format";
24
+ import type { AiSkillFileToolContent, AiSkillFileToolStat } from "./types";
25
+
26
+ export type AiSkillPermission = Exclude<PermissionLevel, "none">;
27
+
28
+ export type AiSkillReference = AiSkillReferenceInput;
29
+
30
+ export type AiSkillSummary = {
31
+ id: string;
32
+ shortId: string;
33
+ name: string;
34
+ description: string;
35
+ permission: AiSkillPermission;
36
+ enabled: boolean;
37
+ revision: number;
38
+ referenceCount: number;
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ };
42
+
43
+ export type AiSkill = AiSkillSummary & {
44
+ instructions: string;
45
+ extraFrontmatter: AiSkillExtraFrontmatter;
46
+ references: AiSkillReference[];
47
+ };
48
+
49
+ export type AiSkillAccess = {
50
+ id: string;
51
+ shortId: string;
52
+ principal: Principal;
53
+ permission: AiSkillPermission;
54
+ displayName?: string;
55
+ createdAt: string;
56
+ };
57
+
58
+ export type AiSkillAdminListItem = {
59
+ id: string;
60
+ shortId: string;
61
+ name: string;
62
+ description: string;
63
+ referenceCount: number;
64
+ accessCount: number;
65
+ adminCount: number;
66
+ createdAt: string;
67
+ updatedAt: string;
68
+ };
69
+
70
+ export type AiSkillAdminSummary = {
71
+ total: number;
72
+ unmanaged: number;
73
+ totalAccess: number;
74
+ };
75
+
76
+ export type AiLoadedSkillSnapshot = {
77
+ name: string;
78
+ description: string;
79
+ revision: number;
80
+ instructions: string;
81
+ files: { path: string; content: string }[];
82
+ loadedAt: string;
83
+ };
84
+
85
+ type SkillRow = {
86
+ id: string;
87
+ short_id: string;
88
+ managed_key: string | null;
89
+ name: string;
90
+ description: string;
91
+ instructions: string;
92
+ extra_frontmatter: AiSkillExtraFrontmatter | string;
93
+ revision: number;
94
+ created_at: Date | string;
95
+ updated_at: Date | string;
96
+ };
97
+
98
+ type SkillAccessRow = {
99
+ short_id: string;
100
+ user_id: string | null;
101
+ group_id: string | null;
102
+ service_account_id: string | null;
103
+ authenticated_only: boolean;
104
+ permission: AiSkillPermission;
105
+ created_at: Date | string;
106
+ display_name: string | null;
107
+ };
108
+
109
+ type SkillSummaryRow = SkillRow & {
110
+ permission: AiSkillPermission;
111
+ reference_count: number;
112
+ enabled: boolean;
113
+ };
114
+
115
+ type AdminSkillRow = SkillRow & {
116
+ reference_count: number;
117
+ access_count: number;
118
+ admin_count: number;
119
+ };
120
+
121
+ type SnapshotRow = {
122
+ skill_name: string;
123
+ description: string;
124
+ revision: number;
125
+ instructions: string;
126
+ files: { path: string; content: string }[] | string;
127
+ loaded_at: Date | string;
128
+ };
129
+
130
+ export class AiSkillLastAdminError extends Error {
131
+ constructor() {
132
+ super("A skill must keep at least one admin access entry.");
133
+ this.name = "AiSkillLastAdminError";
134
+ }
135
+ }
136
+
137
+ export class AiSkillRevisionConflictError extends Error {
138
+ constructor() {
139
+ super("This skill changed after it was opened. Reload it before saving.");
140
+ this.name = "AiSkillRevisionConflictError";
141
+ }
142
+ }
143
+
144
+ export class AiSkillInputError extends Error {
145
+ constructor(message: string) {
146
+ super(message);
147
+ this.name = "AiSkillInputError";
148
+ }
149
+ }
150
+
151
+ const iso = (value: Date | string): string => (value instanceof Date ? value.toISOString() : new Date(value).toISOString());
152
+
153
+ const jsonObject = (value: AiSkillExtraFrontmatter | string): AiSkillExtraFrontmatter =>
154
+ typeof value === "string" ? (JSON.parse(value) as AiSkillExtraFrontmatter) : value;
155
+
156
+ const snapshotFiles = (value: SnapshotRow["files"]): { path: string; content: string }[] =>
157
+ typeof value === "string" ? (JSON.parse(value) as { path: string; content: string }[]) : value;
158
+
159
+ const principalForSubject = (subject: AccessSubject): Principal =>
160
+ subject.type === "user"
161
+ ? { type: "user", userId: subject.userId }
162
+ : { type: "service_account", serviceAccountId: subject.serviceAccountId };
163
+
164
+ const accessMatch = (subject: AccessSubject | null): SQLQuery =>
165
+ buildAccessPrincipalCondition({
166
+ subject,
167
+ columns: {
168
+ userId: sql`access.user_id`,
169
+ groupId: sql`access.group_id`,
170
+ serviceAccountId: sql`access.service_account_id`,
171
+ authenticatedOnly: sql`access.authenticated_only`,
172
+ },
173
+ });
174
+
175
+ const getRow = async (skillId: string, db: SQL = sql): Promise<SkillRow | null> =>
176
+ (await db<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid`)[0] ?? null;
177
+
178
+ const getRowByShortId = async (shortId: string, db: SQL = sql): Promise<SkillRow | null> =>
179
+ (await db<SkillRow[]>`SELECT * FROM ai.skills WHERE short_id = ${shortId}`)[0] ?? null;
180
+
181
+ const getRowByName = async (name: string, db: SQL = sql): Promise<SkillRow | null> =>
182
+ (await db<SkillRow[]>`SELECT * FROM ai.skills WHERE name = ${name}`)[0] ?? null;
183
+
184
+ const permissionFor = async (skillId: string, subject: AccessSubject | null, db: SQL = sql): Promise<AiSkillPermission | "none"> => {
185
+ const match = accessMatch(subject);
186
+ const rows = await db<{ permission: AiSkillPermission }[]>`
187
+ SELECT access.permission
188
+ FROM ai.skill_access skill_access
189
+ JOIN auth.access access ON access.id = skill_access.access_id
190
+ WHERE skill_access.skill_id = ${skillId}::uuid AND ${match}
191
+ ORDER BY CASE access.permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC
192
+ LIMIT 1
193
+ `;
194
+ return rows[0]?.permission ?? "none";
195
+ };
196
+
197
+ const enabledFor = async (skillId: string, subject: AccessSubject | null, db: SQL = sql): Promise<boolean> => {
198
+ if (subject?.type !== "user") return true;
199
+ const [disabled] = await db<{ disabled: boolean }[]>`
200
+ SELECT true AS disabled
201
+ FROM ai.skill_user_disabled
202
+ WHERE skill_id = ${skillId}::uuid AND user_id = ${subject.userId}::uuid
203
+ `;
204
+ return !disabled;
205
+ };
206
+
207
+ const listReferences = async (skillId: string, db: SQL = sql): Promise<AiSkillReference[]> =>
208
+ db<AiSkillReference[]>`
209
+ SELECT path, content FROM ai.skill_references
210
+ WHERE skill_id = ${skillId}::uuid
211
+ ORDER BY path
212
+ `;
213
+
214
+ const toSkill = async (row: SkillRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiSkill | null> => {
215
+ const permission = await permissionFor(row.id, subject, db);
216
+ if (permission === "none") return null;
217
+ const references = await listReferences(row.id, db);
218
+ return {
219
+ id: row.id,
220
+ shortId: row.short_id,
221
+ name: row.name,
222
+ description: row.description,
223
+ instructions: row.instructions,
224
+ extraFrontmatter: jsonObject(row.extra_frontmatter),
225
+ references,
226
+ referenceCount: references.length,
227
+ permission,
228
+ enabled: await enabledFor(row.id, subject, db),
229
+ revision: Number(row.revision),
230
+ createdAt: iso(row.created_at),
231
+ updatedAt: iso(row.updated_at),
232
+ };
233
+ };
234
+
235
+ const requireSkill = async (
236
+ row: SkillRow | null,
237
+ subject: AccessSubject | null,
238
+ required: AiSkillPermission = "read",
239
+ db: SQL = sql,
240
+ ): Promise<AiSkill | null> => {
241
+ if (!row) return null;
242
+ const skill = await toSkill(row, subject, db);
243
+ return skill && hasPermission(skill.permission, required) ? skill : null;
244
+ };
245
+
246
+ const listSkillAccess = async (skillId: string, db: SQL = sql): Promise<AiSkillAccess[]> => {
247
+ const rows = await db<SkillAccessRow[]>`
248
+ SELECT skill_access.short_id, access.user_id, access.group_id, access.service_account_id, access.authenticated_only,
249
+ access.permission, access.created_at,
250
+ COALESCE(users.display_name, groups.name, service_accounts.name,
251
+ CASE WHEN access.authenticated_only THEN 'All authenticated users' ELSE 'Public' END) AS display_name
252
+ FROM ai.skill_access skill_access
253
+ JOIN auth.access access ON access.id = skill_access.access_id
254
+ LEFT JOIN auth.users users ON users.id = access.user_id
255
+ LEFT JOIN auth.groups groups ON groups.id = access.group_id
256
+ LEFT JOIN auth.service_accounts service_accounts ON service_accounts.id = access.service_account_id
257
+ WHERE skill_access.skill_id = ${skillId}::uuid
258
+ ORDER BY access.created_at, access.id
259
+ `;
260
+ return rows.map((row) => ({
261
+ id: row.short_id,
262
+ shortId: row.short_id,
263
+ principal: row.user_id
264
+ ? { type: "user", userId: row.user_id }
265
+ : row.group_id
266
+ ? { type: "group", groupId: row.group_id }
267
+ : row.service_account_id
268
+ ? { type: "service_account", serviceAccountId: row.service_account_id }
269
+ : row.authenticated_only
270
+ ? { type: "authenticated" }
271
+ : { type: "public" },
272
+ permission: row.permission,
273
+ displayName: row.display_name ?? undefined,
274
+ createdAt: iso(row.created_at),
275
+ }));
276
+ };
277
+
278
+ const createSkillAccess = async (
279
+ skillId: string,
280
+ input: { principal: Principal; permission: AiSkillPermission },
281
+ db: SQL,
282
+ ): Promise<AiSkillAccess> => {
283
+ const created = await createAccess(input, db);
284
+ if (!created.ok) throw new Error(created.error.message);
285
+ let shortId = "";
286
+ try {
287
+ await withAiShortIdForDb(db, "idx_ai_skill_access_short_id", (attempt, candidate) => {
288
+ shortId = candidate;
289
+ return attempt`
290
+ INSERT INTO ai.skill_access (skill_id, access_id, short_id)
291
+ VALUES (${skillId}::uuid, ${created.data.id}::uuid, ${candidate})
292
+ `;
293
+ });
294
+ } catch (error) {
295
+ await deleteAccess({ id: created.data.id }, db);
296
+ throw error;
297
+ }
298
+ const entry = (await listSkillAccess(skillId, db)).find((item) => item.shortId === shortId);
299
+ if (!entry) throw new Error("Failed to read the created skill access entry.");
300
+ return entry;
301
+ };
302
+
303
+ const updateSkillAccess = async (skillId: string, accessId: string, permission: AiSkillPermission, db: SQL): Promise<boolean> => {
304
+ const [bound] = await db<{ id: string; permission: AiSkillPermission }[]>`
305
+ SELECT access.id, access.permission
306
+ FROM ai.skill_access skill_access
307
+ JOIN auth.access access ON access.id = skill_access.access_id
308
+ WHERE skill_access.skill_id = ${skillId}::uuid AND skill_access.short_id = ${accessId}
309
+ `;
310
+ if (!bound) return false;
311
+ if (bound.permission === "admin" && permission !== "admin") {
312
+ const [admins] = await db<{ count: number }[]>`
313
+ SELECT count(*)::int AS count FROM ai.skill_access skill_access
314
+ JOIN auth.access access ON access.id = skill_access.access_id
315
+ WHERE skill_access.skill_id = ${skillId}::uuid AND access.permission = 'admin'
316
+ `;
317
+ if ((admins?.count ?? 0) <= 1) throw new AiSkillLastAdminError();
318
+ }
319
+ return (await db`UPDATE auth.access SET permission = ${permission}::auth.permission_level WHERE id = ${bound.id}::uuid`).count === 1;
320
+ };
321
+
322
+ const revokeSkillAccess = async (skillId: string, accessId: string, db: SQL): Promise<boolean> => {
323
+ const [bound] = await db<{ access_id: string; permission: AiSkillPermission }[]>`
324
+ SELECT skill_access.access_id, access.permission
325
+ FROM ai.skill_access skill_access
326
+ JOIN auth.access access ON access.id = skill_access.access_id
327
+ WHERE skill_access.skill_id = ${skillId}::uuid AND skill_access.short_id = ${accessId}
328
+ `;
329
+ if (!bound) return false;
330
+ if (bound.permission === "admin") {
331
+ const [admins] = await db<{ count: number }[]>`
332
+ SELECT count(*)::int AS count FROM ai.skill_access skill_access
333
+ JOIN auth.access access ON access.id = skill_access.access_id
334
+ WHERE skill_access.skill_id = ${skillId}::uuid AND access.permission = 'admin'
335
+ `;
336
+ if ((admins?.count ?? 0) <= 1) throw new AiSkillLastAdminError();
337
+ }
338
+ const deleted = await deleteAccess({ id: bound.access_id }, db);
339
+ if (!deleted.ok) throw new Error(deleted.error.message);
340
+ return true;
341
+ };
342
+
343
+ const validatedFields = (input: {
344
+ name: string;
345
+ description: string;
346
+ instructions: string;
347
+ extraFrontmatter?: AiSkillExtraFrontmatter;
348
+ references?: readonly AiSkillReferenceInput[];
349
+ }) => {
350
+ try {
351
+ return {
352
+ name: validateAiSkillName(input.name),
353
+ description: validateAiSkillDescription(input.description),
354
+ instructions: validateAiSkillInstructions(input.instructions),
355
+ extraFrontmatter: validateAiSkillExtraFrontmatter(input.extraFrontmatter),
356
+ references: validateAiSkillReferences(input.references ?? []),
357
+ };
358
+ } catch (error) {
359
+ if (error instanceof Error) throw new AiSkillInputError(error.message);
360
+ throw error;
361
+ }
362
+ };
363
+
364
+ const sameReferences = (left: readonly AiSkillReference[], right: readonly AiSkillReference[]): boolean =>
365
+ left.length === right.length &&
366
+ left.every((reference, index) => reference.path === right[index]?.path && reference.content === right[index]?.content);
367
+
368
+ const skillSearchPattern = (search?: string): string | null => {
369
+ const value = search?.trim().toLowerCase();
370
+ return value ? `%${value.replace(/[\\%_]/g, (char) => `\\${char}`)}%` : null;
371
+ };
372
+
373
+ const toAdminSkill = (row: AdminSkillRow): AiSkillAdminListItem => ({
374
+ id: row.id,
375
+ shortId: row.short_id,
376
+ name: row.name,
377
+ description: row.description,
378
+ referenceCount: Number(row.reference_count),
379
+ accessCount: Number(row.access_count),
380
+ adminCount: Number(row.admin_count),
381
+ createdAt: iso(row.created_at),
382
+ updatedAt: iso(row.updated_at),
383
+ });
384
+
385
+ const skillSnapshotFiles = (skill: AiSkill): { path: string; content: string }[] => [
386
+ {
387
+ path: `${skill.name}/SKILL.md`,
388
+ content: serializeAiSkillMarkdown(skill),
389
+ },
390
+ ...skill.references.map((reference) => ({ path: `${skill.name}/${reference.path}`, content: reference.content })),
391
+ ];
392
+
393
+ export const aiSkills = {
394
+ async seedOnce(input: {
395
+ key: string;
396
+ name: string;
397
+ description: string;
398
+ instructions: string;
399
+ extraFrontmatter?: AiSkillExtraFrontmatter;
400
+ references?: readonly AiSkillReferenceInput[];
401
+ }): Promise<void> {
402
+ const key = input.key.trim();
403
+ if (!key) throw new AiSkillInputError("Skill seed key is required.");
404
+ const fields = validatedFields(input);
405
+ await sql.begin(async (tx) => {
406
+ const [claimed] = await tx<{ key: string }[]>`
407
+ INSERT INTO ai.skill_seeds (key) VALUES (${key})
408
+ ON CONFLICT (key) DO NOTHING
409
+ RETURNING key
410
+ `;
411
+ if (!claimed) return;
412
+
413
+ let [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE managed_key = ${key} FOR UPDATE`;
414
+ if (!row) {
415
+ const [conflict] = await tx<{ id: string }[]>`SELECT id FROM ai.skills WHERE name = ${fields.name}`;
416
+ if (conflict) return;
417
+ const rows = await withAiShortIdForDb(
418
+ tx,
419
+ "idx_ai_skills_short_id",
420
+ (attempt, shortId) => attempt<SkillRow[]>`
421
+ INSERT INTO ai.skills (short_id, name, description, instructions, extra_frontmatter)
422
+ VALUES (
423
+ ${shortId}, ${fields.name}, ${fields.description}, ${fields.instructions},
424
+ (${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb
425
+ )
426
+ RETURNING *
427
+ `,
428
+ );
429
+ row = rows[0]!;
430
+ } else {
431
+ const references = await listReferences(row.id, tx);
432
+ const changed =
433
+ row.name !== fields.name ||
434
+ row.description !== fields.description ||
435
+ row.instructions !== fields.instructions ||
436
+ JSON.stringify(jsonObject(row.extra_frontmatter)) !== JSON.stringify(fields.extraFrontmatter) ||
437
+ !sameReferences(references, fields.references);
438
+ if (changed) {
439
+ [row] = await tx<SkillRow[]>`
440
+ UPDATE ai.skills
441
+ SET name = ${fields.name}, description = ${fields.description}, instructions = ${fields.instructions},
442
+ extra_frontmatter = (${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb,
443
+ managed_key = NULL, revision = revision + 1, updated_at = now()
444
+ WHERE id = ${row.id}::uuid
445
+ RETURNING *
446
+ `;
447
+ } else {
448
+ [row] = await tx<SkillRow[]>`
449
+ UPDATE ai.skills SET managed_key = NULL WHERE id = ${row.id}::uuid RETURNING *
450
+ `;
451
+ }
452
+ }
453
+
454
+ const currentReferences = await listReferences(row!.id, tx);
455
+ if (!sameReferences(currentReferences, fields.references)) {
456
+ await tx`DELETE FROM ai.skill_references WHERE skill_id = ${row!.id}::uuid`;
457
+ for (const reference of fields.references) {
458
+ await tx`
459
+ INSERT INTO ai.skill_references (skill_id, path, content)
460
+ VALUES (${row!.id}::uuid, ${reference.path}, ${reference.content})
461
+ `;
462
+ }
463
+ }
464
+
465
+ const [grant] = await tx<{ id: string; permission: AiSkillPermission }[]>`
466
+ SELECT access.id, access.permission
467
+ FROM ai.skill_access skill_access
468
+ JOIN auth.access access ON access.id = skill_access.access_id
469
+ WHERE skill_access.skill_id = ${row!.id}::uuid AND access.authenticated_only = true
470
+ LIMIT 1
471
+ `;
472
+ if (!grant) {
473
+ await createSkillAccess(row!.id, { principal: { type: "authenticated" }, permission: "read" }, tx);
474
+ } else if (grant.permission !== "read") {
475
+ await tx`UPDATE auth.access SET permission = 'read' WHERE id = ${grant.id}::uuid`;
476
+ }
477
+ });
478
+ },
479
+
480
+ async create(input: {
481
+ subject: AccessSubject;
482
+ name: string;
483
+ description: string;
484
+ instructions: string;
485
+ extraFrontmatter?: AiSkillExtraFrontmatter;
486
+ references?: readonly AiSkillReferenceInput[];
487
+ }): Promise<AiSkill> {
488
+ const fields = validatedFields(input);
489
+ return sql.begin(async (tx) => {
490
+ const rows = await withAiShortIdForDb(
491
+ tx,
492
+ "idx_ai_skills_short_id",
493
+ (attempt, shortId) => attempt<SkillRow[]>`
494
+ INSERT INTO ai.skills (short_id, name, description, instructions, extra_frontmatter)
495
+ VALUES (
496
+ ${shortId}, ${fields.name}, ${fields.description}, ${fields.instructions},
497
+ (${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb
498
+ )
499
+ RETURNING *
500
+ `,
501
+ );
502
+ for (const reference of fields.references) {
503
+ await tx`
504
+ INSERT INTO ai.skill_references (skill_id, path, content)
505
+ VALUES (${rows[0]!.id}::uuid, ${reference.path}, ${reference.content})
506
+ `;
507
+ }
508
+ await createSkillAccess(rows[0]!.id, { principal: principalForSubject(input.subject), permission: "admin" }, tx);
509
+ return (await toSkill(rows[0]!, input.subject, tx))!;
510
+ });
511
+ },
512
+
513
+ async list(subject: AccessSubject | null): Promise<AiSkillSummary[]> {
514
+ const match = accessMatch(subject);
515
+ const userId = subject?.type === "user" ? subject.userId : null;
516
+ const rows = await sql<SkillSummaryRow[]>`
517
+ SELECT skill.*,
518
+ (array_agg(access.permission ORDER BY
519
+ CASE access.permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC
520
+ ))[1] AS permission,
521
+ count(DISTINCT reference.path)::int AS reference_count,
522
+ (disabled.user_id IS NULL) AS enabled
523
+ FROM ai.skills skill
524
+ JOIN ai.skill_access skill_access ON skill_access.skill_id = skill.id
525
+ JOIN auth.access access ON access.id = skill_access.access_id
526
+ LEFT JOIN ai.skill_references reference ON reference.skill_id = skill.id
527
+ LEFT JOIN ai.skill_user_disabled disabled
528
+ ON disabled.skill_id = skill.id AND disabled.user_id = ${userId}::uuid
529
+ WHERE access.permission <> 'none' AND ${match}
530
+ GROUP BY skill.id, disabled.user_id
531
+ ORDER BY skill.name, skill.id
532
+ LIMIT 200
533
+ `;
534
+ return rows.map((row) => ({
535
+ id: row.id,
536
+ shortId: row.short_id,
537
+ name: row.name,
538
+ description: row.description,
539
+ permission: row.permission,
540
+ revision: Number(row.revision),
541
+ referenceCount: Number(row.reference_count),
542
+ enabled: row.enabled,
543
+ createdAt: iso(row.created_at),
544
+ updatedAt: iso(row.updated_at),
545
+ }));
546
+ },
547
+
548
+ async get(skillId: string, subject: AccessSubject | null, required: AiSkillPermission = "read"): Promise<AiSkill | null> {
549
+ return requireSkill(await getRow(skillId), subject, required);
550
+ },
551
+
552
+ async getByShortId(shortId: string, subject: AccessSubject | null, required: AiSkillPermission = "read"): Promise<AiSkill | null> {
553
+ return requireSkill(await getRowByShortId(shortId), subject, required);
554
+ },
555
+
556
+ async getByName(name: string, subject: AccessSubject | null, required: AiSkillPermission = "read"): Promise<AiSkill | null> {
557
+ return requireSkill(await getRowByName(name), subject, required);
558
+ },
559
+
560
+ async update(
561
+ skillId: string,
562
+ subject: AccessSubject | null,
563
+ input: {
564
+ expectedRevision: number;
565
+ name: string;
566
+ description: string;
567
+ instructions: string;
568
+ extraFrontmatter?: AiSkillExtraFrontmatter;
569
+ references?: readonly AiSkillReferenceInput[];
570
+ },
571
+ ): Promise<AiSkill | null> {
572
+ const fields = validatedFields(input);
573
+ return sql.begin(async (tx) => {
574
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
575
+ if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "write")) return null;
576
+ if (Number(row.revision) !== input.expectedRevision) throw new AiSkillRevisionConflictError();
577
+ const [updated] = await tx<SkillRow[]>`
578
+ UPDATE ai.skills
579
+ SET name = ${fields.name}, description = ${fields.description}, instructions = ${fields.instructions},
580
+ extra_frontmatter = (${JSON.stringify(fields.extraFrontmatter)}::text)::jsonb,
581
+ revision = revision + 1, updated_at = now()
582
+ WHERE id = ${skillId}::uuid
583
+ RETURNING *
584
+ `;
585
+ await tx`DELETE FROM ai.skill_references WHERE skill_id = ${skillId}::uuid`;
586
+ for (const reference of fields.references) {
587
+ await tx`
588
+ INSERT INTO ai.skill_references (skill_id, path, content)
589
+ VALUES (${skillId}::uuid, ${reference.path}, ${reference.content})
590
+ `;
591
+ }
592
+ return toSkill(updated!, subject, tx);
593
+ });
594
+ },
595
+
596
+ async delete(skillId: string, subject: AccessSubject | null): Promise<boolean> {
597
+ return sql.begin(async (tx) => {
598
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
599
+ if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return false;
600
+ const accessIds = (await tx<{ access_id: string }[]>`SELECT access_id FROM ai.skill_access WHERE skill_id = ${skillId}::uuid`).map(
601
+ (entry) => entry.access_id,
602
+ );
603
+ await tx`DELETE FROM ai.skills WHERE id = ${skillId}::uuid`;
604
+ if (accessIds.length) await tx`DELETE FROM auth.access WHERE id = ANY(${toPgUuidArray(accessIds)}::uuid[])`;
605
+ return true;
606
+ });
607
+ },
608
+
609
+ async listAccess(skillId: string, subject: AccessSubject | null): Promise<AiSkillAccess[] | null> {
610
+ if (!(await requireSkill(await getRow(skillId), subject, "admin"))) return null;
611
+ return listSkillAccess(skillId);
612
+ },
613
+
614
+ async grantAccess(
615
+ skillId: string,
616
+ subject: AccessSubject | null,
617
+ input: { principal: Principal; permission: AiSkillPermission },
618
+ ): Promise<AiSkillAccess | null> {
619
+ return sql.begin(async (tx) => {
620
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
621
+ if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "admin")) return null;
622
+ return createSkillAccess(skillId, input, tx);
623
+ });
624
+ },
625
+
626
+ async updateAccess(skillId: string, accessId: string, subject: AccessSubject | null, permission: AiSkillPermission): Promise<boolean> {
627
+ return sql.begin(async (tx) => {
628
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
629
+ return row && hasPermission(await permissionFor(row.id, subject, tx), "admin")
630
+ ? updateSkillAccess(skillId, accessId, permission, tx)
631
+ : false;
632
+ });
633
+ },
634
+
635
+ async revokeAccess(skillId: string, accessId: string, subject: AccessSubject | null): Promise<boolean> {
636
+ return sql.begin(async (tx) => {
637
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
638
+ return row && hasPermission(await permissionFor(row.id, subject, tx), "admin") ? revokeSkillAccess(skillId, accessId, tx) : false;
639
+ });
640
+ },
641
+
642
+ admin: {
643
+ async list(params: {
644
+ search?: string;
645
+ page?: number;
646
+ perPage?: number;
647
+ }): Promise<{ items: AiSkillAdminListItem[]; total: number; page: number; perPage: number }> {
648
+ const perPage = Math.min(Math.max(params.perPage ?? 100, 1), 500);
649
+ const page = Math.max(params.page ?? 1, 1);
650
+ const offset = (page - 1) * perPage;
651
+ const pattern = skillSearchPattern(params.search);
652
+ const [countRow] = await sql<{ total: number }[]>`
653
+ SELECT count(*)::int AS total
654
+ FROM ai.skills skill
655
+ WHERE ${pattern}::text IS NULL
656
+ OR lower(skill.name) LIKE ${pattern} ESCAPE '\\'
657
+ OR lower(skill.description) LIKE ${pattern} ESCAPE '\\'
658
+ OR lower(skill.short_id) LIKE ${pattern} ESCAPE '\\'
659
+ `;
660
+ const rows = await sql<AdminSkillRow[]>`
661
+ SELECT skill.*,
662
+ count(DISTINCT reference.path)::int AS reference_count,
663
+ count(DISTINCT skill_access.access_id)::int AS access_count,
664
+ count(DISTINCT skill_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
665
+ FROM ai.skills skill
666
+ LEFT JOIN ai.skill_references reference ON reference.skill_id = skill.id
667
+ LEFT JOIN ai.skill_access skill_access ON skill_access.skill_id = skill.id
668
+ LEFT JOIN auth.access access ON access.id = skill_access.access_id
669
+ WHERE ${pattern}::text IS NULL
670
+ OR lower(skill.name) LIKE ${pattern} ESCAPE '\\'
671
+ OR lower(skill.description) LIKE ${pattern} ESCAPE '\\'
672
+ OR lower(skill.short_id) LIKE ${pattern} ESCAPE '\\'
673
+ GROUP BY skill.id
674
+ ORDER BY skill.updated_at DESC, skill.id
675
+ LIMIT ${perPage} OFFSET ${offset}
676
+ `;
677
+ return { items: rows.map(toAdminSkill), total: countRow?.total ?? 0, page, perPage };
678
+ },
679
+
680
+ async summary(params: { search?: string } = {}): Promise<AiSkillAdminSummary> {
681
+ const pattern = skillSearchPattern(params.search);
682
+ const [row] = await sql<{ total: number; unmanaged: number; total_access: number }[]>`
683
+ WITH filtered AS (
684
+ SELECT skill.id,
685
+ count(DISTINCT skill_access.access_id)::int AS access_count,
686
+ count(DISTINCT skill_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
687
+ FROM ai.skills skill
688
+ LEFT JOIN ai.skill_access skill_access ON skill_access.skill_id = skill.id
689
+ LEFT JOIN auth.access access ON access.id = skill_access.access_id
690
+ WHERE ${pattern}::text IS NULL
691
+ OR lower(skill.name) LIKE ${pattern} ESCAPE '\\'
692
+ OR lower(skill.description) LIKE ${pattern} ESCAPE '\\'
693
+ OR lower(skill.short_id) LIKE ${pattern} ESCAPE '\\'
694
+ GROUP BY skill.id
695
+ )
696
+ SELECT count(*)::int AS total,
697
+ count(*) FILTER (WHERE admin_count = 0)::int AS unmanaged,
698
+ coalesce(sum(access_count), 0)::int AS total_access
699
+ FROM filtered
700
+ `;
701
+ return { total: row?.total ?? 0, unmanaged: row?.unmanaged ?? 0, totalAccess: row?.total_access ?? 0 };
702
+ },
703
+
704
+ async getByShortId(shortId: string): Promise<AiSkillAdminListItem | null> {
705
+ const rows = await sql<AdminSkillRow[]>`
706
+ SELECT skill.*,
707
+ count(DISTINCT reference.path)::int AS reference_count,
708
+ count(DISTINCT skill_access.access_id)::int AS access_count,
709
+ count(DISTINCT skill_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
710
+ FROM ai.skills skill
711
+ LEFT JOIN ai.skill_references reference ON reference.skill_id = skill.id
712
+ LEFT JOIN ai.skill_access skill_access ON skill_access.skill_id = skill.id
713
+ LEFT JOIN auth.access access ON access.id = skill_access.access_id
714
+ WHERE skill.short_id = ${shortId}
715
+ GROUP BY skill.id
716
+ `;
717
+ return rows[0] ? toAdminSkill(rows[0]) : null;
718
+ },
719
+
720
+ listAccess: listSkillAccess,
721
+
722
+ async grantAccess(skillId: string, input: { principal: Principal; permission: AiSkillPermission }): Promise<AiSkillAccess | null> {
723
+ return sql.begin(async (tx) => {
724
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
725
+ if (!row) return null;
726
+ return createSkillAccess(skillId, input, tx);
727
+ });
728
+ },
729
+
730
+ async updateAccess(skillId: string, accessId: string, permission: AiSkillPermission): Promise<boolean> {
731
+ return sql.begin(async (tx) => {
732
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
733
+ if (!row) return false;
734
+ return updateSkillAccess(skillId, accessId, permission, tx);
735
+ });
736
+ },
737
+
738
+ async revokeAccess(skillId: string, accessId: string): Promise<boolean> {
739
+ return sql.begin(async (tx) => {
740
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
741
+ if (!row) return false;
742
+ return revokeSkillAccess(skillId, accessId, tx);
743
+ });
744
+ },
745
+
746
+ async delete(skillId: string): Promise<boolean> {
747
+ return sql.begin(async (tx) => {
748
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
749
+ if (!row) return false;
750
+ const accessIds = (await tx<{ access_id: string }[]>`SELECT access_id FROM ai.skill_access WHERE skill_id = ${skillId}::uuid`).map(
751
+ (entry) => entry.access_id,
752
+ );
753
+ await tx`DELETE FROM ai.skills WHERE id = ${skillId}::uuid`;
754
+ if (accessIds.length) await tx`DELETE FROM auth.access WHERE id = ANY(${toPgUuidArray(accessIds)}::uuid[])`;
755
+ return true;
756
+ });
757
+ },
758
+ },
759
+
760
+ async setEnabled(skillId: string, subject: AccessSubject | null, enabled: boolean): Promise<boolean | null> {
761
+ if (subject?.type !== "user") return null;
762
+ return sql.begin(async (tx) => {
763
+ if (!(await requireSkill(await getRow(skillId, tx), subject, "read", tx))) return null;
764
+ if (enabled) {
765
+ await tx`
766
+ DELETE FROM ai.skill_user_disabled
767
+ WHERE skill_id = ${skillId}::uuid AND user_id = ${subject.userId}::uuid
768
+ `;
769
+ } else {
770
+ await tx`
771
+ INSERT INTO ai.skill_user_disabled (skill_id, user_id)
772
+ VALUES (${skillId}::uuid, ${subject.userId}::uuid)
773
+ ON CONFLICT (skill_id, user_id) DO NOTHING
774
+ `;
775
+ }
776
+ return enabled;
777
+ });
778
+ },
779
+
780
+ async setReference(
781
+ skillId: string,
782
+ subject: AccessSubject | null,
783
+ input: { expectedRevision: number; path: string; content: string },
784
+ ): Promise<AiSkill | null> {
785
+ return this.setReferences(skillId, subject, {
786
+ expectedRevision: input.expectedRevision,
787
+ references: [{ path: input.path, content: input.content }],
788
+ });
789
+ },
790
+
791
+ async setReferences(
792
+ skillId: string,
793
+ subject: AccessSubject | null,
794
+ input: { expectedRevision: number; references: readonly AiSkillReferenceInput[] },
795
+ ): Promise<AiSkill | null> {
796
+ if (input.references.length === 0) throw new AiSkillInputError("Set at least one Skill reference.");
797
+ const references = validateAiSkillReferences(input.references);
798
+ return sql.begin(async (tx) => {
799
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
800
+ if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "write")) return null;
801
+ if (Number(row.revision) !== input.expectedRevision) throw new AiSkillRevisionConflictError();
802
+ const existing = await tx<AiSkillReferenceInput[]>`
803
+ SELECT path, content FROM ai.skill_references WHERE skill_id = ${skillId}::uuid
804
+ `;
805
+ const merged = new Map(existing.map((reference) => [reference.path, reference]));
806
+ for (const reference of references) merged.set(reference.path, reference);
807
+ validateAiSkillReferences([...merged.values()]);
808
+ for (const reference of references) {
809
+ await tx`
810
+ INSERT INTO ai.skill_references (skill_id, path, content)
811
+ VALUES (${skillId}::uuid, ${reference.path}, ${reference.content})
812
+ ON CONFLICT (skill_id, path) DO UPDATE SET content = EXCLUDED.content
813
+ `;
814
+ }
815
+ const [updated] = await tx<SkillRow[]>`
816
+ UPDATE ai.skills SET revision = revision + 1, updated_at = now()
817
+ WHERE id = ${skillId}::uuid RETURNING *
818
+ `;
819
+ return toSkill(updated!, subject, tx);
820
+ });
821
+ },
822
+
823
+ async removeReference(
824
+ skillId: string,
825
+ subject: AccessSubject | null,
826
+ input: { expectedRevision: number; path: string },
827
+ ): Promise<AiSkill | null> {
828
+ const [reference] = validateAiSkillReferences([{ path: input.path, content: "" }]);
829
+ return sql.begin(async (tx) => {
830
+ const [row] = await tx<SkillRow[]>`SELECT * FROM ai.skills WHERE id = ${skillId}::uuid FOR UPDATE`;
831
+ if (!row || !hasPermission(await permissionFor(row.id, subject, tx), "write")) return null;
832
+ if (Number(row.revision) !== input.expectedRevision) throw new AiSkillRevisionConflictError();
833
+ const deleted = await tx`
834
+ DELETE FROM ai.skill_references
835
+ WHERE skill_id = ${skillId}::uuid AND path = ${reference!.path}
836
+ `;
837
+ if (deleted.count === 0) return null;
838
+ const [updated] = await tx<SkillRow[]>`
839
+ UPDATE ai.skills SET revision = revision + 1, updated_at = now()
840
+ WHERE id = ${skillId}::uuid RETURNING *
841
+ `;
842
+ return toSkill(updated!, subject, tx);
843
+ });
844
+ },
845
+
846
+ async loadForTurn(
847
+ turnId: string,
848
+ selector: string | { id: string },
849
+ subject: AccessSubject | null,
850
+ ): Promise<AiLoadedSkillSnapshot | null> {
851
+ const name = typeof selector === "string" ? validateAiSkillName(selector) : null;
852
+ const shortId = typeof selector === "string" ? null : selector.id;
853
+ if (shortId !== null && !AI_SHORT_ID_PATTERN.test(shortId)) throw new AiSkillInputError("Invalid Skill ID.");
854
+ return sql.begin(async (tx) => {
855
+ const [row] = await tx<SkillRow[]>`
856
+ SELECT * FROM ai.skills WHERE name = ${name} OR short_id = ${shortId} FOR SHARE
857
+ `;
858
+ const skill = await requireSkill(row ?? null, subject, "read", tx);
859
+ if (!skill?.enabled) return null;
860
+ const files = skillSnapshotFiles(skill);
861
+ await tx`
862
+ INSERT INTO ai.turn_skill_snapshots (turn_id, skill_id, skill_name, description, revision, instructions, files)
863
+ VALUES (
864
+ ${turnId}::uuid, ${skill.id}::uuid, ${skill.name}, ${skill.description}, ${skill.revision}, ${skill.instructions},
865
+ (${JSON.stringify(files)}::text)::jsonb
866
+ )
867
+ ON CONFLICT DO NOTHING
868
+ `;
869
+ const [snapshot] = await tx<SnapshotRow[]>`
870
+ SELECT skill_name, description, revision, instructions, files, loaded_at
871
+ FROM ai.turn_skill_snapshots
872
+ WHERE turn_id = ${turnId}::uuid AND skill_id = ${skill.id}::uuid
873
+ `;
874
+ return snapshot
875
+ ? {
876
+ name: snapshot.skill_name,
877
+ description: snapshot.description,
878
+ revision: Number(snapshot.revision),
879
+ instructions: snapshot.instructions,
880
+ files: snapshotFiles(snapshot.files).map((file) => ({ ...file, path: mountAiSkillFilePath(file.path) })),
881
+ loadedAt: iso(snapshot.loaded_at),
882
+ }
883
+ : null;
884
+ });
885
+ },
886
+
887
+ async listTurnFiles(turnId: string, subject: AccessSubject | null): Promise<AiSkillFileToolStat[]> {
888
+ const match = accessMatch(subject);
889
+ const userId = subject?.type === "user" ? subject.userId : null;
890
+ const rows = await sql<SnapshotRow[]>`
891
+ SELECT DISTINCT snapshot.skill_name, snapshot.description, snapshot.revision, snapshot.instructions,
892
+ snapshot.files, snapshot.loaded_at
893
+ FROM ai.turn_skill_snapshots snapshot
894
+ JOIN ai.skill_access skill_access ON skill_access.skill_id = snapshot.skill_id
895
+ JOIN auth.access access ON access.id = skill_access.access_id
896
+ LEFT JOIN ai.skill_user_disabled disabled
897
+ ON disabled.skill_id = snapshot.skill_id AND disabled.user_id = ${userId}::uuid
898
+ WHERE snapshot.turn_id = ${turnId}::uuid AND access.permission <> 'none' AND ${match}
899
+ AND disabled.user_id IS NULL
900
+ ORDER BY snapshot.skill_name
901
+ `;
902
+ return rows.flatMap((row) =>
903
+ snapshotFiles(row.files).map((file) => ({
904
+ path: file.path,
905
+ mediaType: "text/markdown",
906
+ size: new TextEncoder().encode(file.content).byteLength,
907
+ updatedAt: iso(row.loaded_at),
908
+ })),
909
+ );
910
+ },
911
+
912
+ async readTurnFile(turnId: string, path: string, subject: AccessSubject | null): Promise<AiSkillFileToolContent | null> {
913
+ const files = await this.listTurnFiles(turnId, subject);
914
+ const stat = files.find((file) => file.path === path);
915
+ if (!stat) return null;
916
+ const [skillName] = path.split("/", 1);
917
+ const match = accessMatch(subject);
918
+ const userId = subject?.type === "user" ? subject.userId : null;
919
+ const [row] = await sql<SnapshotRow[]>`
920
+ SELECT DISTINCT snapshot.skill_name, snapshot.description, snapshot.revision, snapshot.instructions,
921
+ snapshot.files, snapshot.loaded_at
922
+ FROM ai.turn_skill_snapshots snapshot
923
+ JOIN ai.skill_access skill_access ON skill_access.skill_id = snapshot.skill_id
924
+ JOIN auth.access access ON access.id = skill_access.access_id
925
+ LEFT JOIN ai.skill_user_disabled disabled
926
+ ON disabled.skill_id = snapshot.skill_id AND disabled.user_id = ${userId}::uuid
927
+ WHERE snapshot.turn_id = ${turnId}::uuid AND snapshot.skill_name = ${skillName}
928
+ AND access.permission <> 'none' AND ${match} AND disabled.user_id IS NULL
929
+ LIMIT 1
930
+ `;
931
+ const file = row ? snapshotFiles(row.files).find((entry) => entry.path === path) : null;
932
+ return file ? { ...stat, bytes: new TextEncoder().encode(file.content) } : null;
933
+ },
934
+ };