@k2b/cloud 0.6.0-rc.0 → 0.7.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 (315) hide show
  1. package/README.md +2 -2
  2. package/package.json +30 -17
  3. package/scripts/browser-performance.ts +14 -0
  4. package/scripts/build-pdf-renderer.ts +39 -0
  5. package/scripts/build.ts +15 -35
  6. package/scripts/preload.ts +9 -12
  7. package/scripts/sync-recovery-smoke.ts +2 -1
  8. package/src/_internal/app-presentation.ts +14 -2
  9. package/src/_internal/capabilities.ts +108 -7
  10. package/src/_internal/capability-streams.ts +81 -0
  11. package/src/_internal/define-app.ts +62 -36
  12. package/src/_internal/heartbeat.ts +4 -0
  13. package/src/_internal/help-catalog.ts +27 -172
  14. package/src/_internal/help.ts +13 -76
  15. package/src/_internal/page-responses.ts +2 -0
  16. package/src/_internal/postgres-application-name.ts +30 -0
  17. package/src/_internal/process-identity.ts +15 -0
  18. package/src/_internal/process-sync.ts +1 -1
  19. package/src/_internal/registry-validation.ts +29 -25
  20. package/src/_internal/registry.ts +2 -53
  21. package/src/_internal/runtime-context.ts +5 -16
  22. package/src/_internal/server-timing.ts +13 -0
  23. package/src/_internal/status-preserving-ssr.ts +18 -6
  24. package/src/_internal/web-vitals-asset.ts +10 -0
  25. package/src/access/PermissionEditor.tsx +14 -153
  26. package/src/access/PrincipalPicker.tsx +104 -0
  27. package/src/access/ui.ts +2 -0
  28. package/src/ai/admin.ts +14 -13
  29. package/src/ai/assistant-models.ts +12 -1
  30. package/src/ai/audio-format.ts +34 -0
  31. package/src/ai/audio-tool.ts +90 -0
  32. package/src/ai/browser-code-contracts.ts +79 -0
  33. package/src/ai/browser.ts +2 -0
  34. package/src/ai/capabilities.ts +55 -36
  35. package/src/ai/capability-execution.ts +47 -12
  36. package/src/ai/chat/blocks.tsx +157 -95
  37. package/src/ai/chat/capability-result.ts +15 -0
  38. package/src/ai/chat/capability-table.tsx +2 -10
  39. package/src/ai/chat/composer-adapter.ts +48 -3
  40. package/src/ai/chat/message-actions.tsx +8 -7
  41. package/src/ai/chat/message-utils.ts +12 -1
  42. package/src/ai/chat/messages.ts +2 -0
  43. package/src/ai/chat/presentation.tsx +19 -48
  44. package/src/ai/chat/primitives.tsx +43 -1
  45. package/src/ai/chat/tool-disclosure.tsx +17 -2
  46. package/src/ai/chat/tool-groups.ts +52 -0
  47. package/src/ai/chat-quotas.ts +25 -0
  48. package/src/ai/chat-task-contracts.ts +9 -1
  49. package/src/ai/chat-tasks.ts +276 -43
  50. package/src/ai/client/controller.ts +110 -33
  51. package/src/ai/client/file-source.ts +12 -6
  52. package/src/ai/client/live-connection.ts +1 -0
  53. package/src/ai/client/projection.ts +22 -11
  54. package/src/ai/client/transport.ts +4 -0
  55. package/src/ai/code-capability-routes.ts +110 -0
  56. package/src/ai/code-capability-transport.ts +38 -0
  57. package/src/ai/code-mode-skill.ts +114 -0
  58. package/src/ai/code-runtime-tools.ts +166 -0
  59. package/src/ai/code-source-contracts.ts +202 -0
  60. package/src/ai/code-source-tools.ts +145 -0
  61. package/src/ai/compaction.ts +27 -6
  62. package/src/ai/data-analysis-skill.ts +10 -0
  63. package/src/ai/default-tools.ts +25 -2
  64. package/src/ai/dictation-runtime.ts +171 -0
  65. package/src/ai/dictations.ts +188 -0
  66. package/src/ai/draft-content.ts +34 -0
  67. package/src/ai/enrich.ts +6 -0
  68. package/src/ai/executor.ts +191 -31
  69. package/src/ai/file-content-version.ts +22 -0
  70. package/src/ai/file-context.ts +2 -1
  71. package/src/ai/file-media-type.ts +12 -0
  72. package/src/ai/file-reference-contracts.ts +15 -0
  73. package/src/ai/file-tools.ts +2 -1
  74. package/src/ai/files-store.ts +174 -71
  75. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  76. package/src/ai/grids-skill.ts +40 -10
  77. package/src/ai/http.ts +15 -8
  78. package/src/ai/index.ts +26 -2
  79. package/src/ai/inference-calls.ts +198 -0
  80. package/src/ai/live-events.ts +5 -1
  81. package/src/ai/memory-learning.ts +60 -28
  82. package/src/ai/message-queue.ts +167 -0
  83. package/src/ai/migrate.ts +187 -11
  84. package/src/ai/model-pricing.ts +43 -0
  85. package/src/ai/pdf-render-worker.ts +74 -0
  86. package/src/ai/pdf-render.fixture.ts +28 -0
  87. package/src/ai/pdf-render.ts +77 -0
  88. package/src/ai/projects-routes.ts +3 -3
  89. package/src/ai/projects.ts +20 -6
  90. package/src/ai/protocol.ts +3 -0
  91. package/src/ai/provider.ts +1 -8
  92. package/src/ai/quota-provider.ts +191 -0
  93. package/src/ai/quota-report.ts +152 -0
  94. package/src/ai/quotas-migrate.ts +45 -0
  95. package/src/ai/quotas.ts +168 -0
  96. package/src/ai/routes.ts +215 -11
  97. package/src/ai/run-timeout.ts +10 -0
  98. package/src/ai/runtime-tools.ts +3 -0
  99. package/src/ai/runtime.ts +55 -16
  100. package/src/ai/settings.ts +36 -8
  101. package/src/ai/short-id.ts +2 -2
  102. package/src/ai/skill-catalog.ts +0 -10
  103. package/src/ai/skill-search.ts +47 -0
  104. package/src/ai/skill-seeds.ts +105 -20
  105. package/src/ai/skill-tool.ts +16 -5
  106. package/src/ai/skills-routes.ts +32 -4
  107. package/src/ai/skills.ts +324 -155
  108. package/src/ai/store.ts +373 -191
  109. package/src/ai/stream.ts +5 -0
  110. package/src/ai/structured-runs.ts +4 -3
  111. package/src/ai/structured.ts +8 -1
  112. package/src/ai/system-prompt.ts +12 -2
  113. package/src/ai/timeline.ts +2 -5
  114. package/src/ai/todo-contracts.ts +20 -0
  115. package/src/ai/todo-tool.ts +16 -0
  116. package/src/ai/tool-audit.ts +30 -40
  117. package/src/ai/tools.ts +6 -0
  118. package/src/ai/transcription.ts +202 -0
  119. package/src/ai/turn-timing.ts +114 -0
  120. package/src/ai/types.ts +58 -10
  121. package/src/ai/ui.tsx +2 -0
  122. package/src/ai/usage.ts +118 -118
  123. package/src/ai/vision-tool.ts +39 -10
  124. package/src/api/admin-ai-projects.ts +2 -2
  125. package/src/api/admin-ai-quotas.ts +74 -0
  126. package/src/api/admin-ai-skills.ts +48 -5
  127. package/src/api/admin-ai-usage.ts +2 -2
  128. package/src/api/admin-core-settings.ts +25 -0
  129. package/src/api/admin-identity.ts +91 -51
  130. package/src/api/admin-rail.ts +58 -0
  131. package/src/api/announcements.ts +14 -0
  132. package/src/api/capabilities.ts +292 -43
  133. package/src/api/capability-streams.ts +262 -0
  134. package/src/api/help.ts +8 -44
  135. package/src/api/index.ts +6 -0
  136. package/src/api/mcp.ts +44 -26
  137. package/src/api/me-web-vitals.ts +35 -0
  138. package/src/api/me.ts +2 -0
  139. package/src/api/search/schemas.ts +26 -15
  140. package/src/api/search.bench.ts +1 -1
  141. package/src/api/search.ts +54 -17
  142. package/src/browser/CloudResourceSearch.tsx +640 -702
  143. package/src/browser/app-approval-vault.ts +19 -7
  144. package/src/browser/command-bridge.ts +109 -0
  145. package/src/browser/command-shortcuts.ts +76 -0
  146. package/src/browser/commands.ts +66 -0
  147. package/src/browser/navigation-search.ts +48 -0
  148. package/src/browser/notifications.ts +1 -1
  149. package/src/browser/resource-picker.tsx +5 -18
  150. package/src/browser/resource-search-dialog.ts +9 -0
  151. package/src/browser/resource-search-input.ts +46 -0
  152. package/src/browser/resource-search-messages.ts +76 -40
  153. package/src/browser/resource-search.ts +7 -0
  154. package/src/browser/search-bridge.ts +102 -0
  155. package/src/browser/search-commands.ts +59 -0
  156. package/src/browser/search.ts +13 -0
  157. package/src/browser/spotlight-position.ts +208 -0
  158. package/src/browser/testing.ts +5 -0
  159. package/src/browser/web-vitals.ts +25 -0
  160. package/src/capabilities/claims.ts +179 -0
  161. package/src/capabilities/client.ts +34 -0
  162. package/src/capabilities/command-link.ts +16 -0
  163. package/src/capabilities/executions.ts +298 -0
  164. package/src/capabilities/migrate.ts +81 -0
  165. package/src/capabilities/server.ts +114 -34
  166. package/src/capabilities/stream-body.ts +16 -0
  167. package/src/capabilities/streams.ts +28 -0
  168. package/src/cli/admin/ai-quotas.ts +161 -0
  169. package/src/cli/admin/ai-skills.ts +64 -0
  170. package/src/cli/admin/ai-usage.ts +53 -30
  171. package/src/cli/admin/app-credentials.ts +79 -0
  172. package/src/cli/admin/data.ts +17 -0
  173. package/src/cli/admin/index.ts +9 -0
  174. package/src/cli/capabilities.ts +29 -0
  175. package/src/cli/capability-streams.ts +25 -0
  176. package/src/cli/index.ts +3 -2
  177. package/src/config/env.ts +11 -8
  178. package/src/contracts/app.ts +23 -3
  179. package/src/contracts/capabilities.ts +68 -5
  180. package/src/contracts/capability-streams.ts +46 -0
  181. package/src/contracts/commands.ts +49 -0
  182. package/src/contracts/index.ts +6 -1
  183. package/src/contracts/rail-admin.ts +38 -0
  184. package/src/contracts/rail-preferences.ts +16 -0
  185. package/src/contracts/registry.ts +3 -27
  186. package/src/contracts/settings-types.ts +1 -1
  187. package/src/contracts/shared.ts +9 -5
  188. package/src/contracts/web-vitals.ts +27 -0
  189. package/src/desktop/solid.tsx +3 -3
  190. package/src/index.ts +0 -4
  191. package/src/server/index.ts +3 -0
  192. package/src/server/middleware/auth.ts +3 -2
  193. package/src/server/middleware/route-template.ts +9 -3
  194. package/src/server/middleware/runtime.ts +2 -1
  195. package/src/server/middleware/settings.ts +17 -11
  196. package/src/server/services/access-revision.ts +10 -0
  197. package/src/server/services/access.ts +1 -1
  198. package/src/server/services/index.ts +1 -0
  199. package/src/services/account-category-policy.ts +9 -0
  200. package/src/services/account-lifecycle/audit.ts +1 -1
  201. package/src/services/accounts/app.ts +12 -2
  202. package/src/services/accounts/email-write.ts +23 -0
  203. package/src/services/accounts/groups.ts +15 -4
  204. package/src/services/accounts/identities.ts +249 -0
  205. package/src/services/accounts/identity-reconciliation.ts +87 -0
  206. package/src/services/accounts/ipa-data.ts +2 -2
  207. package/src/services/accounts/local-groups.ts +2 -2
  208. package/src/services/accounts/posix.ts +45 -29
  209. package/src/services/accounts/users.ts +2 -2
  210. package/src/services/announcements/index.ts +69 -8
  211. package/src/services/audit/index.ts +1 -1
  212. package/src/services/cache-fill.ts +35 -0
  213. package/src/services/help/index.ts +109 -0
  214. package/src/services/help/maintenance.ts +25 -0
  215. package/src/services/help/store.ts +180 -0
  216. package/src/services/help/types.ts +46 -0
  217. package/src/services/identity/invocation-actor.ts +2 -2
  218. package/src/services/identity/key-config.ts +2 -1
  219. package/src/services/identity/key-ring.ts +1 -1
  220. package/src/services/index.ts +15 -0
  221. package/src/services/ipa/users.ts +268 -221
  222. package/src/services/legal-consent.ts +1 -1
  223. package/src/services/logging/index.ts +15 -13
  224. package/src/services/logging/trace.ts +2 -1
  225. package/src/services/logging/web-vitals.ts +81 -0
  226. package/src/services/mandates/index.ts +18 -4
  227. package/src/services/mandates/policy.ts +65 -2
  228. package/src/services/notifications/batches.ts +124 -57
  229. package/src/services/notifications/browser.ts +12 -0
  230. package/src/services/notifications/index.ts +0 -2
  231. package/src/services/notifications/platform.ts +0 -4
  232. package/src/services/oauth-tokens.ts +3 -3
  233. package/src/services/pdf/gotenberg.ts +107 -84
  234. package/src/services/pdf/index.ts +3 -0
  235. package/src/services/providers/local/users.ts +25 -16
  236. package/src/services/public-http.ts +101 -0
  237. package/src/services/rail-shortcuts.ts +95 -0
  238. package/src/services/rail-snapshot.ts +53 -0
  239. package/src/services/request-cache-redis.ts +28 -0
  240. package/src/services/session/index.ts +15 -2
  241. package/src/services/session/user.ts +62 -37
  242. package/src/services/settings/core-settings.ts +21 -28
  243. package/src/services/settings/defaults.ts +5 -0
  244. package/src/services/settings/store.ts +58 -84
  245. package/src/shared/ai-costs.ts +42 -0
  246. package/src/shared/ai-platform-prompt.ts +1 -1
  247. package/src/shared/ai-quotas.ts +136 -0
  248. package/src/shared/ai-usage.ts +6 -16
  249. package/src/shared/app-presentation.ts +21 -15
  250. package/src/shared/capability-messages.ts +14 -0
  251. package/src/shared/help.ts +6 -42
  252. package/src/shared/index.ts +6 -0
  253. package/src/ssr/AdminLayout.tsx +7 -5
  254. package/src/ssr/AdminSidebar.tsx +29 -15
  255. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  256. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  257. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  258. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  259. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  260. package/src/ssr/Layout.tsx +17 -6
  261. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  262. package/src/ssr/LayoutFooter.tsx +1 -1
  263. package/src/ssr/LayoutHeader.tsx +46 -14
  264. package/src/ssr/LayoutHelp.tsx +23 -22
  265. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  266. package/src/ssr/LayoutRail.tsx +17 -9
  267. package/src/ssr/MobileNavigation.tsx +71 -0
  268. package/src/ssr/MobileProfileActions.tsx +49 -0
  269. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  270. package/src/ssr/RailApps.island.tsx +2 -2
  271. package/src/ssr/RailEditor.tsx +17 -2
  272. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  273. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  274. package/src/ssr/admin-navigation.ts +7 -1
  275. package/src/ssr/help.ts +29 -0
  276. package/src/ssr/islands/SearchBar.tsx +67 -0
  277. package/src/ssr/islands/index.ts +3 -1
  278. package/src/ssr/layout-context.ts +2 -2
  279. package/src/ssr/layout-help-search.ts +15 -0
  280. package/src/ssr/mobile-menu-history.ts +70 -0
  281. package/src/ssr/platform-messages.ts +16 -8
  282. package/src/ssr/profile-actions.ts +42 -0
  283. package/src/ssr/rail-context.ts +8 -4
  284. package/src/ssr/rail-messages.ts +2 -0
  285. package/src/ssr/rail-navigation.ts +3 -3
  286. package/src/ssr/workspace-navigation.ts +75 -0
  287. package/src/styles/effects.css +6 -6
  288. package/src/styles/global.css +54 -0
  289. package/src/styles/input.css +43 -41
  290. package/src/styles/resource-search.css +505 -0
  291. package/src/styles/utilities-feedback.css +13 -0
  292. package/src/styles/utilities-layout.css +1 -5
  293. package/src/styles/utilities-navigation.css +3 -39
  294. package/src/types/ambient.d.ts +5 -0
  295. package/src/workflows/ai/runtime.ts +5 -1
  296. package/src/workflows/ai/store.ts +2 -0
  297. package/src/workflows/ai-actions.ts +16 -13
  298. package/src/workflows/definition.ts +10 -5
  299. package/src/workflows/store/actions.ts +44 -20
  300. package/src/workflows/store/index.ts +2 -0
  301. package/src/workflows/store/runs.ts +36 -0
  302. package/src/workflows/store/worker-pool.ts +55 -0
  303. package/src/workflows/store/worker-runtime.ts +90 -0
  304. package/scripts/app-favicon.test.ts +0 -49
  305. package/src/ai/chat/blocks.render.test.tsx +0 -1108
  306. package/src/ai/kit-skill.ts +0 -22
  307. package/src/ai/usage-migrate.ts +0 -81
  308. package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
  309. package/src/browser/notification-target.ts +0 -27
  310. package/src/contracts/notification-live.ts +0 -66
  311. package/src/services/notifications/live.ts +0 -50
  312. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  313. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
  314. package/src/ssr/Layout.render.test.tsx +0 -148
  315. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -1,3 +1,4 @@
1
+ import { AiModelPricingSchema } from "../shared/ai-costs";
1
2
  import { z } from "zod";
2
3
  import { coreSettings } from "../services";
3
4
  import { getAiCredential, listAiCredentialProfileIds } from "./credentials";
@@ -94,10 +95,22 @@ const ModelProfileSchema = z
94
95
  maxOutputTokens: z.number().int().positive().optional(),
95
96
  maxLoadedTools: z.number().int().optional(),
96
97
  maxToolRounds: z.number().int().optional(),
97
- creditsPerInputToken: z.number().nonnegative().optional(),
98
- creditsPerOutputToken: z.number().nonnegative().optional(),
98
+ pricing: AiModelPricingSchema.optional(),
99
99
  })
100
100
  .superRefine((profile, ctx) => {
101
+ if (profile.capabilities?.includes("transcription")) {
102
+ if (profile.pricing) ctx.addIssue({ code: "custom", path: ["pricing"], message: "Audio pricing is not supported." });
103
+ if (profile.capabilities.some((capability) => capability !== "transcription")) {
104
+ ctx.addIssue({ code: "custom", path: ["capabilities"], message: "Audio transcription cannot be combined with chat capabilities." });
105
+ }
106
+ if (profile.provider !== "openai" && profile.provider !== "openai-compatible") {
107
+ ctx.addIssue({
108
+ code: "custom",
109
+ path: ["provider"],
110
+ message: "Audio transcription requires OpenAI or an OpenAI-compatible endpoint.",
111
+ });
112
+ }
113
+ }
101
114
  if (profile.provider === "openai-compatible" && !profile.baseURL) {
102
115
  ctx.addIssue({ code: "custom", path: ["baseURL"], message: "OpenAI-compatible profiles require a base URL." });
103
116
  }
@@ -177,6 +190,7 @@ export const validateAiSettingsConfiguration = (input: {
177
190
  defaultModelId: string;
178
191
  backgroundModelId: string;
179
192
  visionModelId?: string;
193
+ audioModelId?: string;
180
194
  workflowModelId: string;
181
195
  profiles: readonly AiModelProfile[];
182
196
  credentialProfileIds: readonly string[];
@@ -186,13 +200,14 @@ export const validateAiSettingsConfiguration = (input: {
186
200
  const errors: Record<string, string> = {};
187
201
  const enabledProfiles = input.profiles.filter((profile) => profile.enabled);
188
202
  const defaultProfile = input.profiles.find((profile) => profile.id === input.defaultModelId);
189
- if (!input.defaultModelId || !defaultProfile?.enabled) {
203
+ if (!input.defaultModelId || !defaultProfile?.enabled || defaultProfile.capabilities.includes("transcription")) {
190
204
  errors["ai.default_model_id"] = "Choose an enabled model profile.";
191
205
  }
192
206
 
193
207
  if (input.backgroundModelId) {
194
208
  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.";
209
+ if (!backgroundProfile?.enabled || backgroundProfile.capabilities.includes("transcription"))
210
+ errors["ai.background_model_id"] = "Choose an enabled chat model profile or use the platform default.";
196
211
  }
197
212
 
198
213
  if (input.visionModelId) {
@@ -204,7 +219,15 @@ export const validateAiSettingsConfiguration = (input: {
204
219
 
205
220
  if (input.workflowModelId) {
206
221
  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.";
222
+ if (!workflowProfile?.enabled || workflowProfile.capabilities.includes("transcription"))
223
+ errors["ai.workflow_model_id"] = "Choose an enabled chat model profile or use the background model.";
224
+ }
225
+
226
+ if (input.audioModelId) {
227
+ const audioProfile = input.profiles.find((profile) => profile.id === input.audioModelId);
228
+ if (!audioProfile?.enabled || !audioProfile.capabilities.includes("transcription")) {
229
+ errors["ai.audio_model_id"] = "Choose an enabled audio transcription profile or disable audio transcription.";
230
+ }
208
231
  }
209
232
 
210
233
  const configured = new Set(input.credentialProfileIds);
@@ -301,7 +324,7 @@ export const resolveAiSettingsStateFromRaw = async (input: {
301
324
  };
302
325
  }
303
326
 
304
- if (!defaultProfile.enabled) {
327
+ if (!defaultProfile.enabled || defaultProfile.capabilities.includes("transcription")) {
305
328
  return {
306
329
  ok: false,
307
330
  ...baseState,
@@ -309,7 +332,7 @@ export const resolveAiSettingsStateFromRaw = async (input: {
309
332
  profiles: parsed.profiles,
310
333
  error: {
311
334
  code: "default_model_disabled",
312
- message: `Default AI model "${input.defaultModelId}" is disabled.`,
335
+ message: `Default AI model "${input.defaultModelId}" must be an enabled text model.`,
313
336
  fields: { "ai.default_model_id": "Choose an enabled model profile id." },
314
337
  },
315
338
  };
@@ -398,6 +421,7 @@ const hasAll = <T extends string>(values: readonly T[], required: readonly T[] |
398
421
 
399
422
  const matchesPolicy = (profile: AiModelProfile, policy: AiModelPolicy): boolean => {
400
423
  if (!profile.enabled) return false;
424
+ if (profile.capabilities.includes("transcription") !== Boolean(policy.requiredCapabilities?.includes("transcription"))) return false;
401
425
  if ("allowedModelIds" in policy && policy.allowedModelIds && !policy.allowedModelIds.includes(profile.id)) return false;
402
426
  if (policy.allowedDataBoundaries && !policy.allowedDataBoundaries.includes(profile.dataBoundary)) return false;
403
427
  return hasAll(profile.capabilities, policy.requiredCapabilities);
@@ -521,7 +545,11 @@ export const toPublicAiSettingsState = async (allowedDataBoundaries?: AiDataBoun
521
545
  firecrawlConfigured: state.firecrawlConfigured,
522
546
  models:
523
547
  state.ok && state.enabled
524
- ? state.profiles.filter((profile) => profile.enabled && isUsableProfile(profile, configured)).map(profileToPublic)
548
+ ? state.profiles
549
+ .filter(
550
+ (profile) => matchesPolicy(profile, { kind: "selectable", allowedDataBoundaries }) && isUsableProfile(profile, configured),
551
+ )
552
+ .map(profileToPublic)
525
553
  : [],
526
554
  };
527
555
  };
@@ -14,11 +14,11 @@ export const withAiShortId = async <T>(
14
14
  try {
15
15
  return await insert(allocate());
16
16
  } catch (error) {
17
+ const uniqueViolation = typeof error === "object" && error !== null && (("code" in error && error.code === "23505") || ("errno" in error && error.errno === "23505"));
17
18
  const collision =
18
19
  typeof error === "object" &&
19
20
  error !== null &&
20
- "code" in error &&
21
- error.code === "23505" &&
21
+ uniqueViolation &&
22
22
  "constraint" in error &&
23
23
  error.constraint === constraint;
24
24
  if (!collision) throw error;
@@ -60,14 +60,4 @@ export const selectAiSkillCatalog = (
60
60
  return { skills: selected, omitted: normalized.length - selected.length };
61
61
  };
62
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
63
  export const aiSkillCatalogChars = (skills: readonly AiSkillCatalogEntry[]): number => skills.map(catalogLine).join("\n").length;
@@ -0,0 +1,47 @@
1
+ import { sql } from "bun";
2
+ import { toPgTextArray } from "../services/postgres";
3
+
4
+ const BM25_INDEX = "ai.skills_search_bm25_idx";
5
+ let bm25Available: Promise<boolean> | undefined;
6
+
7
+ /** BM25 only ranks candidates; native matching and access checks always apply. */
8
+ export async function withAiSkillSearch<T>(query: string, run: (bm25: boolean) => Promise<T>): Promise<T> {
9
+ if (!query) return run(false);
10
+ bm25Available ??= sql<{ available: boolean }[]>`
11
+ SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_textsearch')
12
+ AND EXISTS (SELECT 1 FROM pg_class c JOIN pg_am am ON am.oid = c.relam
13
+ WHERE c.oid = to_regclass(${BM25_INDEX}) AND am.amname = 'bm25') AS available
14
+ `.then(rows => rows[0]?.available ?? false);
15
+ const bm25 = await bm25Available;
16
+ try {
17
+ return await run(bm25);
18
+ } catch (error) {
19
+ const capabilityLost = typeof error === "object" && error !== null &&
20
+ ["code", "errno"].some(key => key in error &&
21
+ ["0A000", "42704", "42883", "55000"].includes(String(Reflect.get(error, key))));
22
+ if (!bm25 || !capabilityLost) throw error;
23
+ bm25Available = Promise.resolve(false);
24
+ return run(false);
25
+ }
26
+ }
27
+
28
+ export function aiSkillSearchSql(query: string, bm25: boolean) {
29
+ const terms = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [])];
30
+ // Match every term in either field. Short prefixes stay exact; longer terms
31
+ // tolerate typos without matching unrelated short words throughout the catalog.
32
+ const matches = !query ? sql`TRUE` : !terms.length ? sql`FALSE` : sql`
33
+ NOT EXISTS (SELECT 1 FROM unnest(${toPgTextArray(terms)}::text[]) AS term
34
+ WHERE strpos(lower(skill.name), term) = 0 AND strpos(lower(skill.description), term) = 0
35
+ AND (length(term) < 4 OR greatest(word_similarity(term, skill.name),
36
+ word_similarity(term, skill.description)) < 0.35))
37
+ `;
38
+ const rank = !query ? sql`0::real` : sql`
39
+ CASE WHEN lower(skill.name) = ${query.toLowerCase()} THEN 100 ELSE 0 END
40
+ + CASE WHEN strpos(lower(skill.name), ${query.toLowerCase()}) > 0 THEN 10 ELSE 0 END
41
+ + 2 * word_similarity(${query}, skill.name) + word_similarity(${query}, skill.description)
42
+ `;
43
+ const textRank = !query ? sql`0::real` : bm25
44
+ ? sql`-(skill.search_text <@> to_bm25query(${query}, ${BM25_INDEX}))`
45
+ : sql`ts_rank_cd(to_tsvector('simple', skill.search_text), plainto_tsquery('simple', ${query}))`;
46
+ return { matches, rank, textRank };
47
+ }
@@ -1,6 +1,7 @@
1
- import { CLOUD_KIT_INSTRUCTIONS } from "./kit-skill";
1
+ import { ASSISTANT_DATA_ANALYSIS_SKILL } from "./data-analysis-skill";
2
+ import { ASSISTANT_CODE_MODE_SKILL } from "./code-mode-skill";
2
3
  import { CLOUD_GRIDS_INSTRUCTIONS, CLOUD_GRIDS_QUERY_REFERENCE } from "./grids-skill";
3
- import { aiSkills } from "./skills";
4
+ import type { AiSkillTemplate } from "./skills";
4
5
 
5
6
  const SKILL_CREATOR_INSTRUCTIONS = `# Create and improve Skills
6
7
 
@@ -81,7 +82,15 @@ Pass the exact current revision to mutations and read again after a revision con
81
82
 
82
83
  Creating, changing, and deleting Skills are reviewed mutations. Prepare the concrete content the user requested, then use the Capability review instead of asking for an extra confirmation that duplicates it.
83
84
 
84
- Sharing, access changes, imports, and exports are not available through these capabilities. Say so plainly rather than inventing a tool or bypassing Cloud permissions.`;
85
+ ## Optional App-backed workflows
86
+
87
+ A Skill can reference a reusable Studio App and its published actions instead of duplicating code. Load \`assistant-code-mode\` only when building, changing, or discovering such an App is useful. Apps may be stateless scripts, agent-only services with shared data, dashboards, or a combination. Many Skills need no code and many Apps need no Skill.
88
+
89
+ When referring to an App, include its exact ID, action names, input/output meaning, and a concrete workflow example. Read the action schemas instead of guessing APIs. Skill access and App access are independent: warn when intended recipients can use only one; never grant access implicitly.
90
+
91
+ For requested sharing, discover \`core.entities.search\` to resolve a recipient, then \`core.ai.skill.access.read\` and \`core.ai.skill.access.change\`. Read current grants and pass their exact accessRevision as expectedAccessRevision. Supply either a returned principal and permission (read/write/admin), or an existing accessId and permission (null revokes). Every mutation receives fresh review and preserves the last administrator. After a conflict, re-read and prepare a new review. For App permissions, read the Code Mode access reference; App Use/Manage is separate from Skill Read/Edit/Manage.
92
+
93
+ Imports and exports are not available through these capabilities. Do not invent a tool or bypass Cloud permissions.`;
85
94
 
86
95
  const CLOUD_MAIL_INSTRUCTIONS = `# Work with Cloud Mail
87
96
 
@@ -196,7 +205,7 @@ Use these defaults unless the user asks otherwise or a more specific loaded Skil
196
205
  - Simple subtasks use \`spaces.task.checklist.list\`, \`spaces.task.checklist.create\`, \`spaces.task.checklist.update\` and \`spaces.task.checklist.delete\`. Keep each entry to a label and completion state; do not turn checklist entries into independent assigned tasks.
197
206
  - Use comments for discussion; read before replacing or deleting them. For tag replacement, preserve tags not included in the requested change.
198
207
  - Use \`spaces.item.reference.find\` to find existing items for a source resource. Link on creation through references rather than a redundant second mutation. For an existing item use \`spaces.item.reference.add\`; use \`spaces.item.link-candidate.search\` when a writable target is unknown.
199
- - Use \`spaces.event.create-once\` only in retryable durable workflows with their stable idempotency key. Normal interactive creation uses \`spaces.event.create\`. Never blindly retry an uncertain mutation.
208
+ - Use \`spaces.event.create\` for interactive creation. Never blindly retry an uncertain mutation.
200
209
 
201
210
  ## Calendar mail and cross-app work
202
211
 
@@ -237,7 +246,7 @@ Use these defaults unless the user asks otherwise or a more specific loaded Skil
237
246
 
238
247
  - Conversations: \`core.ai.chats.search\`, \`core.ai.chat.read\`, \`core.ai.chat.search\`, \`core.ai.chat.resources\`, and \`core.ai.chats.resources\`.
239
248
  - Messaging: \`core.ai.chat.message\`.
240
- - Scheduled work: \`core.ai.tasks.list\`, \`core.ai.task.read\`, \`core.ai.task.create\`, \`core.ai.task.update\`, \`core.ai.task.pause\`, \`core.ai.task.resume\`, \`core.ai.task.run\`, and \`core.ai.task.delete\`.
249
+
241
250
 
242
251
  Load only the needed capabilities and reuse returned typed IDs unchanged.
243
252
 
@@ -246,9 +255,75 @@ Load only the needed capabilities and reuse returned typed IDs unchanged.
246
255
  - The runtime Chat ID identifies the current conversation. Use \`core.ai.chat.search\` for earlier content in it. For another conversation, use \`core.ai.chats.search\`, then \`core.ai.chat.read\` or \`core.ai.chat.search\` with the returned ID.
247
256
  - Use \`core.ai.chat.resources\` for resources from one known conversation and \`core.ai.chats.resources\` to search across conversations. Read a returned resource through its owning app rather than guessing its contents.
248
257
  - Before \`core.ai.chat.message\`, identify the exact target and message. Report queued or delivered status accurately and do not claim the target completed the requested work.
249
- - For reminders and recurring work, use \`core.ai.task.create\` with the exact runtime timezone. Ask one focused question when the schedule or intended conversation is materially ambiguous.
250
- - Use \`core.ai.tasks.list\` and \`core.ai.task.read\` before changing a task. Preserve its ID and use the focused update, pause, resume, run, or delete Action.
251
- - A scheduled run continues its conversation with the Project and permissions available at execution time; do not promise access that may later be unavailable.`;
258
+ - For one-time reminders, recurring background work, or changing and repairing a scheduled task, load the \`scheduled-tasks\` Skill.`;
259
+
260
+ const SCHEDULED_TASKS_INSTRUCTIONS = `# Schedule reminders and background work
261
+
262
+ Use this Skill to create or manage a task that runs later, once or repeatedly.
263
+ Use the current conversation unless the user names another owned chat. A simple
264
+ reminder is a valid task: its result is a normal message in that chat.
265
+
266
+ ## Load the tools for the job
267
+
268
+ - Create: \`core.ai.task.create\`.
269
+ - Find and inspect: \`core.ai.tasks.list\`, then \`core.ai.task.read\`.
270
+ - Change instructions, schedule or grants: \`core.ai.task.update\`.
271
+ - Control an existing task: \`core.ai.task.pause\`, \`core.ai.task.resume\`,
272
+ \`core.ai.task.run\`, or \`core.ai.task.delete\`, as requested.
273
+ - Diagnose an occurrence: \`core.ai.task.run.read\`, using the task and occurrence
274
+ IDs returned by task.read.
275
+
276
+ Discover and load only the needed capabilities. Read their current input schemas;
277
+ reuse returned IDs. Read an existing task before changing it. If the request may
278
+ refer to an existing schedule, list tasks before creating a duplicate.
279
+
280
+ ## Prepare the task
281
+
282
+ 1. Resolve the user's timing with the exact runtime timezone (IANA). For a
283
+ one-time reminder or delayed job use kind once and localAt; for recurrence use
284
+ kind cron and cron. Resolve relative wording against the current runtime date
285
+ and time. Ask only if a missing time, timezone or target changes the outcome.
286
+ 2. Write a self-contained prompt with the intended result, verified resource IDs
287
+ and relevant context. Use normal language for results and problems, without a
288
+ fixed return schema. A reminder prompt says what to remind the user about;
289
+ it needs no app capabilities merely to post its result to this chat.
290
+ 3. For work across apps, load the relevant domain Skills and discover every
291
+ capability needed, including searches and reads before writes. Propose grants
292
+ for those capabilities. Choose fixedInput fields from their real schemas:
293
+ constrain a specific note when the task targets it; allow a whole notebook or
294
+ unrestricted inputs when that is the intended scope. Empty fixedInput allows
295
+ any inputs within current user access. Do not invent resource IDs or claim a
296
+ notebook constraint exists on an action that accepts only a note ID.
297
+ 4. Explain the scope through the normal create/update review. Grants authorize
298
+ this task's capability use, not new access to resources. Always-approval
299
+ capabilities cannot be granted. Interactive browser and Code Mode are not
300
+ available in background runs; explain an unsupported requirement rather than
301
+ promising it will work unattended.
302
+ 5. Create the task. Confirm its actual saved timing, timezone and destination only
303
+ after success. Queued or manually started does not mean completed.
304
+
305
+ Examples: “Remind me tomorrow at 09:00 to call Anna” is one once task with no
306
+ capability grants. “Every weekday summarize my mail, compare it with calendar X,
307
+ and update note Y” is a recurring task with discovered read and write grants,
308
+ using verified calendar and note IDs where the respective schemas allow them.
309
+
310
+ ## Changes, errors and missing rights
311
+
312
+ Read the task and relevant failed run before proposing a repair. Distinguish
313
+ missing task grants from lost resource access and ordinary execution errors.
314
+ Update the same task's prompt, future schedule or grants; do not recreate it.
315
+ Omitted grants remain unchanged; supplied grants replace the complete list.
316
+ Expanded authority needs the normal user review and cannot restore lost resource
317
+ access. Check writes already performed before retrying with task.run to avoid
318
+ duplicates; use task.resume when the user wants future scheduled runs restored.
319
+
320
+ Each run works independently from a snapshot of the chat history at its start.
321
+ Available memories, Skills, Project context and chat files remain accessible
322
+ under runtime rules; files are live, not frozen copies. The run returns its result
323
+ or problem to the parent chat, reopening it and updating activity. The user can
324
+ keep chatting meanwhile. Do not load this management Skill on every background
325
+ run unless managing a schedule is itself needed; use the domain Skills for the
326
+ actual work.`;
252
327
 
253
328
  const CLOUD_NOTEBOOKS_REFERENCE = `## Flexible data and automatic page lists
254
329
 
@@ -351,13 +426,11 @@ Mail attachment inputs require base64: encode the exact returned calendar string
351
426
 
352
427
  Do not fabricate organizer/attendee addresses, regenerate the returned calendar text, commit after a failed attachment, or retry an unknown mutation result without reconciliation.`;
353
428
 
354
- const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> = [
355
- {
356
- key: "kit:cloud-kit", name: "cloud-kit",
357
- description: "Use for Cloud Kit: programming or improving browser mini apps, file converters, JavaScript tools, workbench UIs, CSV processing or exact money calculations in an existing Kit app. Read current Kit Help before choosing SDK methods.",
358
- instructions: CLOUD_KIT_INSTRUCTIONS,
359
- },
429
+ const BUILTIN_CLOUD_AI_SKILLS: AiSkillTemplate[] = [
430
+ ASSISTANT_CODE_MODE_SKILL,
431
+ ASSISTANT_DATA_ANALYSIS_SKILL,
360
432
  {
433
+ version: 4,
361
434
  key: "grids:cloud-grids",
362
435
  name: "cloud-grids",
363
436
  description:
@@ -366,6 +439,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
366
439
  references: [{ path: "references/query-tasks.md", content: CLOUD_GRIDS_QUERY_REFERENCE }],
367
440
  },
368
441
  {
442
+ version: 2,
369
443
  key: "assistant:cloud-assistant",
370
444
  name: "cloud-assistant",
371
445
  description:
@@ -373,6 +447,14 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
373
447
  instructions: CLOUD_ASSISTANT_INSTRUCTIONS,
374
448
  },
375
449
  {
450
+ version: 1,
451
+ key: "assistant:scheduled-tasks",
452
+ name: "scheduled-tasks",
453
+ description: "Create and manage one-time reminders, delayed jobs and recurring scheduled background tasks in Cloud Assistant. Use for requests such as remind me tomorrow, check in an hour, every morning, or changing, pausing, resuming and repairing a scheduled task, including missing permissions or failed runs.",
454
+ instructions: SCHEDULED_TASKS_INSTRUCTIONS,
455
+ },
456
+ {
457
+ version: 2,
376
458
  key: "core:skill-creator",
377
459
  name: "skill-creator",
378
460
  description:
@@ -380,6 +462,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
380
462
  instructions: SKILL_CREATOR_INSTRUCTIONS,
381
463
  },
382
464
  {
465
+ version: 1,
383
466
  key: "mail:cloud-mail",
384
467
  name: "cloud-mail",
385
468
  description:
@@ -387,6 +470,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
387
470
  instructions: CLOUD_MAIL_INSTRUCTIONS,
388
471
  },
389
472
  {
473
+ version: 1,
390
474
  key: "notebooks:cloud-notebooks",
391
475
  name: "cloud-notebooks",
392
476
  description:
@@ -395,6 +479,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
395
479
  references: [{ path: "references/structured-pages.md", content: CLOUD_NOTEBOOKS_REFERENCE }],
396
480
  },
397
481
  {
482
+ version: 1,
398
483
  key: "contacts:cloud-contacts",
399
484
  name: "cloud-contacts",
400
485
  description:
@@ -402,6 +487,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
402
487
  instructions: CLOUD_CONTACTS_INSTRUCTIONS,
403
488
  },
404
489
  {
490
+ version: 1,
405
491
  key: "spaces:cloud-spaces",
406
492
  name: "cloud-spaces",
407
493
  description:
@@ -410,6 +496,7 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
410
496
  references: [{ path: "references/calendar-mail.md", content: CLOUD_SPACES_CALENDAR_REFERENCE }],
411
497
  },
412
498
  {
499
+ version: 1,
413
500
  key: "weather:cloud-weather",
414
501
  name: "cloud-weather",
415
502
  description:
@@ -418,13 +505,11 @@ const BUILTIN_CLOUD_AI_SKILLS: Array<Parameters<typeof aiSkills.seedOnce>[0]> =
418
505
  },
419
506
  ];
420
507
 
508
+ export const getBuiltinAiSkillTemplates = (): readonly AiSkillTemplate[] => BUILTIN_CLOUD_AI_SKILLS;
509
+
421
510
  export const getBuiltinAiSkillTemplate = (name: string) => {
422
511
  const seed = BUILTIN_CLOUD_AI_SKILLS.find((entry) => entry.name === name);
423
512
  if (!seed) return undefined;
424
- const { key: _key, ...template } = seed;
425
- return template;
426
- };
427
-
428
- export const seedCloudAiSkills = async (): Promise<void> => {
429
- for (const seed of BUILTIN_CLOUD_AI_SKILLS) await aiSkills.seedOnce(seed);
513
+ const { key, ...template } = seed;
514
+ return { ...template, templateId: key };
430
515
  };
@@ -2,11 +2,22 @@ import { z } from "zod";
2
2
  import type { AccessSubject } from "../server";
3
3
  import { AI_SKILL_FILE_MOUNT } from "./file-mount";
4
4
  import { AI_SHORT_ID_PATTERN } from "./short-id";
5
- import { searchAiSkillCatalog } from "./skill-catalog";
6
5
  import { AI_SKILL_NAME_MAX_CHARS, AI_SKILL_NAME_PATTERN } from "./skill-format";
7
6
  import { aiSkills } from "./skills";
8
7
  import { defineAiTool } from "./tools";
9
8
 
9
+ /** Explicit selections use the same permission check and turn snapshot as load_skill. */
10
+ export async function loadSelectedAiSkills(ids: readonly string[], turnId: string, subject: AccessSubject | null, allowed: boolean) {
11
+ const loaded = [];
12
+ for (const id of new Set(ids)) {
13
+ if (!subject || !allowed) throw new Error("The selected Skill requires a model and tool scope that allow Skills.");
14
+ const skill = await aiSkills.loadForTurn(turnId, { id }, subject);
15
+ if (!skill) throw new Error(`Selected Skill ${id} is unavailable or access was revoked.`);
16
+ loaded.push({ name: skill.name, revision: skill.revision, instructions: skill.instructions, files: skill.files.map(file => file.path) });
17
+ }
18
+ return loaded;
19
+ }
20
+
10
21
  export const CloudAiLoadSkillInputSchema = z
11
22
  .object({
12
23
  name: z
@@ -58,7 +69,7 @@ export const createCloudAiLoadSkillTool = (subject: AccessSubject) =>
58
69
  });
59
70
 
60
71
  export const CloudAiSearchSkillsInputSchema = z.object({
61
- query: z.string().trim().min(1).max(200).describe("Short English terms describing the workflow or domain to find."),
72
+ query: z.string().trim().min(1).max(200).describe("Words from the Skill name or description; minor typos are tolerated."),
62
73
  limit: z.number().int().min(1).max(20).default(10),
63
74
  });
64
75
 
@@ -71,11 +82,11 @@ export const createCloudAiSearchSkillsTool = (subject: AccessSubject) =>
71
82
  defineAiTool({
72
83
  name: "search_skills",
73
84
  description:
74
- "Search enabled Assistant Skills omitted from the bounded system-prompt catalog. Use short English domain or workflow terms, then load_skill with an exact returned name.",
85
+ "Search enabled Assistant Skills omitted from the bounded system-prompt catalog. Search names and descriptions with short domain or workflow terms; minor typos are tolerated. Then load_skill with an exact returned name.",
75
86
  inputSchema: CloudAiSearchSkillsInputSchema,
76
87
  outputSchema: CloudAiSearchSkillsOutputSchema,
77
88
  approval: "never",
78
89
  }).server(async (input) => {
79
- const skills = (await aiSkills.list(subject)).filter((skill) => skill.enabled);
80
- return searchAiSkillCatalog(skills, input.query, input.limit);
90
+ const result = await aiSkills.search(subject, input.query, input.limit);
91
+ return { skills: result.skills.map(({ name, description }) => ({ name, description })), more: result.more };
81
92
  });
@@ -1,6 +1,7 @@
1
+ import { aiProjects } from "./projects";
1
2
  import { Hono, type MiddlewareHandler } from "hono";
2
3
  import { z } from "zod";
3
- import { PrincipalSchema } from "../contracts/shared";
4
+ import { AuthenticatedPrincipalSchema } from "../contracts/shared";
4
5
  import { type AuthContext, auth, err, fail, ok, rateLimit, respond, v } from "../server";
5
6
  import {
6
7
  AI_SKILL_DESCRIPTION_MAX_CHARS,
@@ -38,7 +39,7 @@ const SkillFieldsSchema = z.object({
38
39
  });
39
40
 
40
41
  const UpdateSkillSchema = SkillFieldsSchema.extend({ expectedRevision: z.number().int().positive() });
41
- const SkillAccessSchema = z.object({ principal: PrincipalSchema, permission: z.enum(["read", "write", "admin"]) });
42
+ const SkillAccessSchema = z.object({ principal: AuthenticatedPrincipalSchema, permission: z.enum(["read", "write", "admin"]) });
42
43
  const SkillAccessUpdateSchema = z.object({ permission: z.enum(["read", "write", "admin"]) });
43
44
  const SkillEnabledSchema = z.object({ enabled: z.boolean() });
44
45
 
@@ -56,8 +57,35 @@ type AiSkillsRouteDependencies = {
56
57
  const buildAiSkillsRoutes = (dependencies: AiSkillsRouteDependencies = {}) =>
57
58
  new Hono<AuthContext>()
58
59
  .use(dependencies.limit ?? rateLimit())
59
- .use("*", dependencies.authenticate ?? auth.requireRole("*"))
60
- .get("/", async (c) => respond(c, ok({ skills: (await aiSkills.list(c.get("accessSubject") ?? null)).map(publicSummary) })))
60
+ .use("*", dependencies.authenticate ?? auth.requireRole("authenticated"))
61
+ .get("/", v("query", z.object({ q: z.string().trim().max(200).optional() })), async (c) => {
62
+ const subject = c.get("accessSubject") ?? null;
63
+ const { q } = c.req.valid("query");
64
+ const skills = q === undefined ? await aiSkills.list(subject) : (await aiSkills.search(subject, q)).skills;
65
+ return respond(c, ok({ skills: skills.map(publicSummary) }));
66
+ })
67
+ .get("/project-links/:projectId", v("query", z.object({ q: z.string().max(200).default(""), page: z.coerce.number().int().min(1).max(100000).default(1), available: z.enum(["true","false"]).default("false") })), async c => {
68
+ const subject = c.get("accessSubject") ?? null, query = c.req.valid("query");
69
+ const project = await aiProjects.getByShortId(c.req.param("projectId")!, subject, query.available === "true" ? "admin" : "read");
70
+ const result = project ? await aiSkills.projectSkills(project.id, subject, { query:query.q, page:query.page, available:query.available === "true" }) : null;
71
+ return result ? respond(c, ok({ ...result, items:result.items.map(publicSummary) })) : respond(c, fail(err.notFound("Project")));
72
+ })
73
+ .get("/:skillId/projects", async c => {
74
+ const subject = c.get("accessSubject") ?? null;
75
+ const skill = await aiSkills.getByShortId(c.req.param("skillId")!, subject, "admin");
76
+ const projects = skill ? await aiSkills.linkedProjects(skill.id, subject) : null;
77
+ return projects ? respond(c, ok({ projects:projects.map(project => ({ id:project.shortId, name:project.name })) })) : respond(c, fail(err.notFound("Skill")));
78
+ })
79
+ .put("/:skillId/projects/:projectId", v("json", z.object({ linked:z.boolean() }).strict()), async c => {
80
+ const subject = c.get("accessSubject") ?? null;
81
+ const [skill,project] = await Promise.all([
82
+ aiSkills.getByShortId(c.req.param("skillId")!,subject,"admin"),
83
+ aiProjects.getByShortId(c.req.param("projectId")!,subject,"admin"),
84
+ ]);
85
+ const linked = c.req.valid("json").linked;
86
+ return skill && project && await aiSkills.linkProject(skill.id,project.id,linked,subject)
87
+ ? respond(c,ok({linked})) : respond(c,fail(err.notFound("Skill or Project")));
88
+ })
61
89
  .get("/templates/:name", dependencies.authenticate ?? auth.requireRole("authenticated"), (c) => {
62
90
  if (!c.get("accessSubject")) return respond(c, fail(err.forbidden("Skill templates require an authenticated access subject.")));
63
91
  const template = getBuiltinAiSkillTemplate(c.req.param("name"));