@aglyn/plugins-ai 1.0.0-beta.231 → 1.0.0-beta.232

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 (220) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +8 -4
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-follow.component.js +1 -0
  5. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  6. package/src/lib/components/ai-job-links.js +13 -0
  7. package/src/lib/components/ai-job-links.js.map +1 -1
  8. package/src/lib/components/ai-jobs-list-page.component.js +1 -0
  9. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  10. package/src/lib/components/ai-media-create.component.d.ts +20 -7
  11. package/src/lib/components/ai-media-create.component.js +107 -55
  12. package/src/lib/components/ai-media-create.component.js.map +1 -1
  13. package/src/lib/components/ai-seo-fields-card.component.js +1 -0
  14. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  15. package/src/lib/components/ai-site-memory-card.component.d.ts +32 -0
  16. package/src/lib/components/ai-site-memory-card.component.js +187 -0
  17. package/src/lib/components/ai-site-memory-card.component.js.map +1 -0
  18. package/src/lib/components/ai-site-start-card.component.js +80 -77
  19. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  20. package/src/lib/components/ai-theme-proposal-card.component.js +4 -0
  21. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  22. package/src/lib/components/ai-usage-strip.component.js +1 -0
  23. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  24. package/src/lib/components/assist-panel.component.d.ts +10 -0
  25. package/src/lib/components/assist-panel.component.js +119 -9
  26. package/src/lib/components/assist-panel.component.js.map +1 -1
  27. package/src/lib/components/assist-signals-page.component.js +2 -0
  28. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  29. package/src/lib/components/billing-ai-allotments.component.js +3 -0
  30. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  31. package/src/lib/components/billing-ai-top-users.component.js +1 -0
  32. package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
  33. package/src/lib/components/billing-assist-overage-card.component.js +2 -0
  34. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  35. package/src/lib/components/member-ai-usage-card.component.js +10 -0
  36. package/src/lib/components/member-ai-usage-card.component.js.map +1 -1
  37. package/src/lib/components/staff-org-ai-card.component.js +1 -0
  38. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  39. package/src/lib/components/staff-org-ai-conversations.component.d.ts +23 -0
  40. package/src/lib/components/staff-org-ai-conversations.component.js +346 -0
  41. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -0
  42. package/src/lib/jobs/ai-build-capabilities.d.ts +13 -1
  43. package/src/lib/jobs/ai-build-capabilities.js +101 -7
  44. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  45. package/src/lib/jobs/ai-business-profile-prefill.d.ts +29 -0
  46. package/src/lib/jobs/ai-business-profile-prefill.js +85 -0
  47. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -0
  48. package/src/lib/jobs/ai-job-build-step.js +3 -1
  49. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  50. package/src/lib/jobs/ai-job-edit-step.d.ts +225 -0
  51. package/src/lib/jobs/ai-job-edit-step.js +631 -0
  52. package/src/lib/jobs/ai-job-edit-step.js.map +1 -0
  53. package/src/lib/jobs/ai-job-insight-step.js +21 -4
  54. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  55. package/src/lib/jobs/ai-job-layout-language.d.ts +8 -3
  56. package/src/lib/jobs/ai-job-layout-language.js +8 -2
  57. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  58. package/src/lib/jobs/ai-job-layout-step.js +7 -6
  59. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  60. package/src/lib/jobs/ai-job-logic-step.d.ts +20 -0
  61. package/src/lib/jobs/ai-job-logic-step.js +87 -6
  62. package/src/lib/jobs/ai-job-logic-step.js.map +1 -1
  63. package/src/lib/jobs/ai-job-page-language.d.ts +52 -7
  64. package/src/lib/jobs/ai-job-page-language.js +72 -9
  65. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  66. package/src/lib/jobs/ai-job-page-step.js +12 -2
  67. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  68. package/src/lib/jobs/ai-job-plan-step.d.ts +19 -5
  69. package/src/lib/jobs/ai-job-plan-step.js +54 -20
  70. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  71. package/src/lib/jobs/ai-job-site-content.d.ts +163 -0
  72. package/src/lib/jobs/ai-job-site-content.js +443 -0
  73. package/src/lib/jobs/ai-job-site-content.js.map +1 -0
  74. package/src/lib/jobs/ai-job-site-look.d.ts +112 -0
  75. package/src/lib/jobs/ai-job-site-look.js +322 -0
  76. package/src/lib/jobs/ai-job-site-look.js.map +1 -0
  77. package/src/lib/jobs/ai-job-site-step.d.ts +26 -5
  78. package/src/lib/jobs/ai-job-site-step.js +163 -24
  79. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  80. package/src/lib/jobs/ai-jobs-notify.d.ts +6 -1
  81. package/src/lib/jobs/ai-jobs-notify.js +15 -3
  82. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  83. package/src/lib/layout-language/ai-layout-compiler.d.ts +9 -0
  84. package/src/lib/layout-language/ai-layout-compiler.js +120 -33
  85. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  86. package/src/lib/layout-language/ai-layout-frame.js +5 -4
  87. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  88. package/src/lib/layout-language/ai-layout-language.d.ts +12 -5
  89. package/src/lib/layout-language/ai-layout-language.js +92 -9
  90. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  91. package/src/lib/layout-language/ai-layout-pictures.d.ts +144 -0
  92. package/src/lib/layout-language/ai-layout-pictures.js +214 -0
  93. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -0
  94. package/src/lib/layout-language/ai-layout-store.d.ts +2 -0
  95. package/src/lib/layout-language/ai-layout-store.js +7 -1
  96. package/src/lib/layout-language/ai-layout-store.js.map +1 -1
  97. package/src/lib/model/ai-build-progress.js +4 -1
  98. package/src/lib/model/ai-build-progress.js.map +1 -1
  99. package/src/lib/model/ai-edit-job.d.ts +109 -0
  100. package/src/lib/model/ai-edit-job.js +157 -0
  101. package/src/lib/model/ai-edit-job.js.map +1 -0
  102. package/src/lib/model/ai-job-activity.js +1 -0
  103. package/src/lib/model/ai-job-activity.js.map +1 -1
  104. package/src/lib/model/ai-jobs.types.d.ts +3 -1
  105. package/src/lib/model/ai-jobs.types.js.map +1 -1
  106. package/src/lib/model/ai-logic-job.d.ts +18 -1
  107. package/src/lib/model/ai-logic-job.js +24 -3
  108. package/src/lib/model/ai-logic-job.js.map +1 -1
  109. package/src/lib/model/ai-media-credits.d.ts +8 -4
  110. package/src/lib/model/ai-media-credits.js +11 -4
  111. package/src/lib/model/ai-media-credits.js.map +1 -1
  112. package/src/lib/model/ai-media-kinds.d.ts +68 -0
  113. package/src/lib/model/ai-media-kinds.js +98 -0
  114. package/src/lib/model/ai-media-kinds.js.map +1 -0
  115. package/src/lib/model/ai-site-build-progress.d.ts +4 -0
  116. package/src/lib/model/ai-site-build-progress.js +28 -11
  117. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  118. package/src/lib/model/ai-site-context.d.ts +111 -0
  119. package/src/lib/model/ai-site-context.js +222 -0
  120. package/src/lib/model/ai-site-context.js.map +1 -0
  121. package/src/lib/model/ai-site-job.d.ts +10 -2
  122. package/src/lib/model/ai-site-job.js +12 -5
  123. package/src/lib/model/ai-site-job.js.map +1 -1
  124. package/src/lib/model/ai-site-kinds.d.ts +164 -0
  125. package/src/lib/model/ai-site-kinds.js +1942 -0
  126. package/src/lib/model/ai-site-kinds.js.map +1 -0
  127. package/src/lib/model/ai-site-look.d.ts +150 -0
  128. package/src/lib/model/ai-site-look.js +1236 -0
  129. package/src/lib/model/ai-site-look.js.map +1 -0
  130. package/src/lib/model/ai-site-memory.d.ts +80 -0
  131. package/src/lib/model/ai-site-memory.js +171 -0
  132. package/src/lib/model/ai-site-memory.js.map +1 -0
  133. package/src/lib/model/ai-site-start.d.ts +10 -1
  134. package/src/lib/model/ai-site-start.js +16 -2
  135. package/src/lib/model/ai-site-start.js.map +1 -1
  136. package/src/lib/model/assist-follow-up.d.ts +92 -0
  137. package/src/lib/model/assist-follow-up.js +128 -0
  138. package/src/lib/model/assist-follow-up.js.map +1 -0
  139. package/src/lib/plugin-config.js +2 -0
  140. package/src/lib/plugin-config.js.map +1 -1
  141. package/src/lib/plugin.js +20 -1
  142. package/src/lib/plugin.js.map +1 -1
  143. package/src/lib/providers/anthropic.d.ts +9 -1
  144. package/src/lib/providers/anthropic.js +143 -9
  145. package/src/lib/providers/anthropic.js.map +1 -1
  146. package/src/lib/providers/catalog.d.ts +37 -7
  147. package/src/lib/providers/catalog.js +103 -11
  148. package/src/lib/providers/catalog.js.map +1 -1
  149. package/src/lib/providers/contract.d.ts +29 -0
  150. package/src/lib/providers/contract.js.map +1 -1
  151. package/src/lib/providers/image-contract.d.ts +22 -1
  152. package/src/lib/providers/image-contract.js +40 -1
  153. package/src/lib/providers/image-contract.js.map +1 -1
  154. package/src/lib/providers/image-endpoints.d.ts +24 -0
  155. package/src/lib/providers/image-endpoints.js +28 -0
  156. package/src/lib/providers/image-endpoints.js.map +1 -0
  157. package/src/lib/providers/model-choice.js +23 -2
  158. package/src/lib/providers/model-choice.js.map +1 -1
  159. package/src/lib/providers/routing.js +18 -0
  160. package/src/lib/providers/routing.js.map +1 -1
  161. package/src/lib/providers/vertex-image-endpoint.d.ts +48 -0
  162. package/src/lib/providers/vertex-image-endpoint.js +80 -0
  163. package/src/lib/providers/vertex-image-endpoint.js.map +1 -0
  164. package/src/lib/providers/vertex-image.d.ts +13 -27
  165. package/src/lib/providers/vertex-image.js +15 -42
  166. package/src/lib/providers/vertex-image.js.map +1 -1
  167. package/src/lib/runtime/ai-blog-post-generation.d.ts +62 -0
  168. package/src/lib/runtime/ai-blog-post-generation.js +89 -0
  169. package/src/lib/runtime/ai-blog-post-generation.js.map +1 -0
  170. package/src/lib/runtime/ai-dev-env.d.ts +53 -0
  171. package/src/lib/runtime/ai-dev-env.js +72 -0
  172. package/src/lib/runtime/ai-dev-env.js.map +1 -0
  173. package/src/lib/runtime/ai-dev-replay.d.ts +116 -0
  174. package/src/lib/runtime/ai-dev-replay.js +313 -0
  175. package/src/lib/runtime/ai-dev-replay.js.map +1 -0
  176. package/src/lib/runtime/ai-doctrine.js +5 -1
  177. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  178. package/src/lib/runtime/ai-eval-live.js +4 -0
  179. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  180. package/src/lib/runtime/ai-eval.d.ts +13 -1
  181. package/src/lib/runtime/ai-eval.js +84 -1
  182. package/src/lib/runtime/ai-eval.js.map +1 -1
  183. package/src/lib/runtime/ai-live-batch.d.ts +45 -0
  184. package/src/lib/runtime/ai-live-batch.js +89 -0
  185. package/src/lib/runtime/ai-live-batch.js.map +1 -0
  186. package/src/lib/runtime/ai-runtime.js +46 -2
  187. package/src/lib/runtime/ai-runtime.js.map +1 -1
  188. package/src/lib/runtime/site-context.d.ts +58 -0
  189. package/src/lib/runtime/site-context.js +127 -0
  190. package/src/lib/runtime/site-context.js.map +1 -0
  191. package/src/lib/server/ai-admin-conversations.d.ts +22 -0
  192. package/src/lib/server/ai-admin-conversations.js +197 -0
  193. package/src/lib/server/ai-admin-conversations.js.map +1 -0
  194. package/src/lib/server/ai-media-image.d.ts +5 -5
  195. package/src/lib/server/ai-media-image.js +25 -11
  196. package/src/lib/server/ai-media-image.js.map +1 -1
  197. package/src/lib/server/ai-media-raster-prompt.d.ts +34 -0
  198. package/src/lib/server/ai-media-raster-prompt.js +53 -0
  199. package/src/lib/server/ai-media-raster-prompt.js.map +1 -0
  200. package/src/lib/server/assist-chat.d.ts +11 -0
  201. package/src/lib/server/assist-chat.js +57 -18
  202. package/src/lib/server/assist-chat.js.map +1 -1
  203. package/src/lib/server/assist-edit-applied.js +16 -3
  204. package/src/lib/server/assist-edit-applied.js.map +1 -1
  205. package/src/lib/server/assist-view-context.d.ts +19 -1
  206. package/src/lib/server/assist-view-context.js +38 -1
  207. package/src/lib/server/assist-view-context.js.map +1 -1
  208. package/src/lib/server.js +10 -0
  209. package/src/lib/server.js.map +1 -1
  210. package/src/lib/subprocessors.js +17 -1
  211. package/src/lib/subprocessors.js.map +1 -1
  212. package/src/lib/tools/ai-blog-post-tool.d.ts +60 -0
  213. package/src/lib/tools/ai-blog-post-tool.js +175 -0
  214. package/src/lib/tools/ai-blog-post-tool.js.map +1 -0
  215. package/src/lib/usage/assist-usage.d.ts +3 -0
  216. package/src/lib/usage/assist-usage.js +4 -2
  217. package/src/lib/usage/assist-usage.js.map +1 -1
  218. package/src/lib/usage/staff-org-ai-conversations.d.ts +103 -0
  219. package/src/lib/usage/staff-org-ai-conversations.js +152 -0
  220. package/src/lib/usage/staff-org-ai-conversations.js.map +1 -0
@@ -18,6 +18,8 @@ import { permissionRefusal, checkRateLimit, emailUnverifiedResponse, firebaseAdm
18
18
  import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
19
19
  import { logAiEditApplied } from "../activity/ai-activity.js";
20
20
  import { ASSIST_EDIT_DOCUMENT_KINDS, ASSIST_EDIT_OP_COUNT_WORDS, assistEditDocumentOf } from "../model/assist-edit.js";
21
+ import { aiPreferencesFromEdit } from "../model/ai-site-memory.js";
22
+ import { rememberAiSitePreferences } from "../runtime/site-context.js";
21
23
  import { sanitiseId } from "./assist-view-context.js";
22
24
  /**
23
25
  * The applied-edit record (AGL-2906, AGL-2929): the panel reports that the
@@ -25,8 +27,9 @@ import { sanitiseId } from "./assist-view-context.js";
25
27
  * `ai.edit.applied` row to the site's activity log, attributed to them.
26
28
  *
27
29
  * The edits were applied in the author's own editor, as unsaved changes;
28
- * nothing here can see them or repeat them, and nothing here writes a
29
- * document. What this door checks is that the report stands on a proposal
30
+ * nothing here can see them or repeat them, and nothing here writes the
31
+ * edited document. (It does remember what the edit showed about the owner's
32
+ * preferences, AGL-3661 — see `model/ai-site-memory.ts`.) What this door checks is that the report stands on a proposal
30
33
  * the server issued: the exchange exists under the named org, the same
31
34
  * member asked it about the same site, and its signal records edit
32
35
  * operations proposed from the besigner route of the same document. A
@@ -205,7 +208,7 @@ async function handler(request) {
205
208
  status: 200
206
209
  });
207
210
  case 'recorded':
208
- var _decoded_email;
211
+ var _decoded_email, _asked_question;
209
212
  await logAiEditApplied(report.hostId, {
210
213
  uid: decoded.uid,
211
214
  email: (_decoded_email = decoded.email) != null ? _decoded_email : null
@@ -215,6 +218,16 @@ async function handler(request) {
215
218
  versionId: report.versionId,
216
219
  opCounts: report.opCounts
217
220
  });
221
+ // What the applied edit showed about the owner's preferences
222
+ // (AGL-3661), remembered for every later AI job on the site. Rules,
223
+ // no model call; a failure never fails the record it follows.
224
+ await rememberAiSitePreferences(firestore, report.hostId, aiPreferencesFromEdit({
225
+ question: String((_asked_question = asked['question']) != null ? _asked_question : ''),
226
+ opCounts: report.opCounts
227
+ })).catch((error)=>console.warn('ai site memory unwritten', {
228
+ hostId: report.hostId,
229
+ error
230
+ }));
218
231
  return Response.json({
219
232
  ok: true
220
233
  }, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-edit-applied.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n permissionRefusal,\n checkRateLimit,\n emailUnverifiedResponse,\n firebaseAdmin,\n getOrgForUser,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n memberHasPermissionOnHost,\n rateLimitHeaders,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { logAiEditApplied } from '../activity/ai-activity'\nimport {\n ASSIST_EDIT_DOCUMENT_KINDS,\n ASSIST_EDIT_OP_COUNT_WORDS,\n assistEditDocumentOf,\n type AssistEditAppliedReport,\n type AssistEditDocumentKind,\n} from '../model/assist-edit'\nimport { sanitiseId } from './assist-view-context'\n\n/**\n * The applied-edit record (AGL-2906, AGL-2929): the panel reports that the\n * author applied a proposal in their editor, and this door writes the\n * `ai.edit.applied` row to the site's activity log, attributed to them.\n *\n * The edits were applied in the author's own editor, as unsaved changes;\n * nothing here can see them or repeat them, and nothing here writes a\n * document. What this door checks is that the report stands on a proposal\n * the server issued: the exchange exists under the named org, the same\n * member asked it about the same site, and its signal records edit\n * operations proposed from the besigner route of the same document. A\n * report that fails any of those writes nothing. The counts are held to the\n * operation words and to what that turn proposed, and a second report for\n * one exchange writes no second row.\n *\n * The ladder: 405 → 401 → 400 → 403 email unverified → 403 not a member →\n * 403 the role lacks `ai.generate` (staff pass) → 404 `release_ai_generative`\n * off (staff preview) → 423 lockdown → 429 rate → 404 no such exchange of\n * the caller's → 409 the exchange proposed no such edit. No reservation and\n * no provider: a record of an act costs no credits.\n */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** The report, held to its own alphabets; null when anything is missing or malformed. */\nexport function parseEditAppliedReport(payload: unknown): AssistEditAppliedReport | null {\n const body = isRecord(payload) ? payload : {}\n // Ids are document path segments here, so a slash is refused rather than\n // followed into a subcollection.\n const orgId = sanitiseId(String(body['orgId'] ?? ''))\n const hostId = sanitiseId(String(body['hostId'] ?? ''))\n const exchangeId = sanitiseId(String(body['exchangeId'] ?? ''))\n const documentId = sanitiseId(String(body['documentId'] ?? ''))\n const versionId = sanitiseId(String(body['versionId'] ?? ''))\n const documentKind = body['documentKind'] as AssistEditDocumentKind\n if (!orgId || !hostId || !exchangeId || !documentId || !versionId) return null\n if (!ASSIST_EDIT_DOCUMENT_KINDS.includes(documentKind)) return null\n const opCounts: Record<string, number> = {}\n for (const [word, count] of Object.entries(isRecord(body['opCounts']) ? body['opCounts'] : {})) {\n if (!ASSIST_EDIT_OP_COUNT_WORDS.includes(word)) continue\n const parsed = Number(count)\n if (Number.isInteger(parsed) && parsed > 0) opCounts[word] = parsed\n }\n if (!Object.keys(opCounts).length) return null\n return { orgId, hostId, exchangeId, documentKind, documentId, versionId, opCounts }\n}\n\ntype RecordVerdict = 'recorded' | 'duplicate' | 'unknown' | 'no-proposal' | 'too-many'\n\nasync function handler(request: Request): Promise<Response> {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = request.headers.get('authorization') ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n let payload: unknown\n try {\n payload = await request.json()\n } catch {\n payload = null\n }\n const report = parseEditAppliedReport(payload)\n if (!report) {\n return Response.json({ error: 'Missing or malformed report' }, { status: 400 })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const resolved = await getOrgForUser(decoded.uid, report.orgId)\n if (!resolved || resolved.orgId !== report.orgId) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n if (\n !staff &&\n !(await memberHasPermissionOnHost(report.orgId, report.hostId, resolved.member, 'ai.generate'))\n ) {\n return permissionRefusal('ai.generate')\n }\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', report.orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: (resolved.org ?? {}) as Record<string, unknown>,\n })\n if (locked) return locked\n const rate = checkRateLimit(`assist-edit-applied:${decoded.uid}`, {\n limit: 30,\n windowMs: 60_000,\n })\n if (!rate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(rate) },\n )\n }\n\n const firestore = app.firestore()\n const orgRef = firestore.collection('orgs').doc(report.orgId)\n const exchange = await orgRef.collection('assistExchanges').doc(report.exchangeId).get()\n const asked = exchange.exists ? (exchange.data() as Record<string, unknown> | undefined) : undefined\n if (!asked || asked['uid'] !== decoded.uid || (asked['hostId'] ?? null) !== report.hostId) {\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n }\n\n const signalRef = orgRef.collection('assistSignals').doc(report.exchangeId)\n const verdict = await firestore.runTransaction(async (tx): Promise<RecordVerdict> => {\n const signal = await tx.get(signalRef)\n const data = signal.exists ? (signal.data() as Record<string, unknown> | undefined) : undefined\n if (!data) return 'unknown'\n const proposed = Number(data['editOps'] ?? 0)\n const document = assistEditDocumentOf(String(data['route'] ?? ''))\n if (\n !(proposed > 0) ||\n !document ||\n document.kind !== report.documentKind ||\n document.documentId !== report.documentId\n ) {\n return 'no-proposal'\n }\n const applied = Object.values(report.opCounts).reduce((sum, count) => sum + count, 0)\n if (applied > proposed) return 'too-many'\n if (data['editAppliedAt']) return 'duplicate'\n tx.update(signalRef, { editAppliedAt: FieldValue.serverTimestamp() })\n return 'recorded'\n })\n\n switch (verdict) {\n case 'unknown':\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n case 'no-proposal':\n return Response.json(\n { error: 'That exchange proposed no edit to this document' },\n { status: 409 },\n )\n case 'too-many':\n return Response.json(\n { error: 'More edits reported than that exchange proposed' },\n { status: 409 },\n )\n case 'duplicate':\n return Response.json({ ok: true, duplicate: true }, { status: 200 })\n case 'recorded':\n await logAiEditApplied(\n report.hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n {\n type: report.documentKind,\n id: report.documentId,\n versionId: report.versionId,\n opCounts: report.opCounts,\n },\n )\n return Response.json({ ok: true }, { status: 200 })\n }\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json({ error: 'The edit could not be recorded' }, { status: 500 })\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["FieldValue","permissionRefusal","checkRateLimit","emailUnverifiedResponse","firebaseAdmin","getOrgForUser","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","memberHasPermissionOnHost","rateLimitHeaders","invalidIdTokenResponse","logAiEditApplied","ASSIST_EDIT_DOCUMENT_KINDS","ASSIST_EDIT_OP_COUNT_WORDS","assistEditDocumentOf","sanitiseId","isRecord","value","Array","isArray","parseEditAppliedReport","payload","body","orgId","String","hostId","exchangeId","documentId","versionId","documentKind","includes","opCounts","word","count","Object","entries","parsed","Number","isInteger","keys","length","handler","request","method","Response","json","error","status","authorization","headers","get","idToken","startsWith","slice","undefined","report","resolved","asked","app","decoded","auth","verifyIdToken","email_verified","staff","uid","member","locked","org","rate","limit","windowMs","allowed","reason","firestore","orgRef","collection","doc","exchange","exists","data","signalRef","verdict","runTransaction","tx","signal","proposed","document","kind","applied","values","reduce","sum","update","editAppliedAt","serverTimestamp","ok","duplicate","email","type","id","unauthenticated","console","dynamic","POST"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-edit-applied.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n permissionRefusal,\n checkRateLimit,\n emailUnverifiedResponse,\n firebaseAdmin,\n getOrgForUser,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n memberHasPermissionOnHost,\n rateLimitHeaders,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { logAiEditApplied } from '../activity/ai-activity'\nimport {\n ASSIST_EDIT_DOCUMENT_KINDS,\n ASSIST_EDIT_OP_COUNT_WORDS,\n assistEditDocumentOf,\n type AssistEditAppliedReport,\n type AssistEditDocumentKind,\n} from '../model/assist-edit'\nimport { aiPreferencesFromEdit } from '../model/ai-site-memory'\nimport { rememberAiSitePreferences } from '../runtime/site-context'\nimport { sanitiseId } from './assist-view-context'\n\n/**\n * The applied-edit record (AGL-2906, AGL-2929): the panel reports that the\n * author applied a proposal in their editor, and this door writes the\n * `ai.edit.applied` row to the site's activity log, attributed to them.\n *\n * The edits were applied in the author's own editor, as unsaved changes;\n * nothing here can see them or repeat them, and nothing here writes the\n * edited document. (It does remember what the edit showed about the owner's\n * preferences, AGL-3661 — see `model/ai-site-memory.ts`.) What this door checks is that the report stands on a proposal\n * the server issued: the exchange exists under the named org, the same\n * member asked it about the same site, and its signal records edit\n * operations proposed from the besigner route of the same document. A\n * report that fails any of those writes nothing. The counts are held to the\n * operation words and to what that turn proposed, and a second report for\n * one exchange writes no second row.\n *\n * The ladder: 405 → 401 → 400 → 403 email unverified → 403 not a member →\n * 403 the role lacks `ai.generate` (staff pass) → 404 `release_ai_generative`\n * off (staff preview) → 423 lockdown → 429 rate → 404 no such exchange of\n * the caller's → 409 the exchange proposed no such edit. No reservation and\n * no provider: a record of an act costs no credits.\n */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** The report, held to its own alphabets; null when anything is missing or malformed. */\nexport function parseEditAppliedReport(payload: unknown): AssistEditAppliedReport | null {\n const body = isRecord(payload) ? payload : {}\n // Ids are document path segments here, so a slash is refused rather than\n // followed into a subcollection.\n const orgId = sanitiseId(String(body['orgId'] ?? ''))\n const hostId = sanitiseId(String(body['hostId'] ?? ''))\n const exchangeId = sanitiseId(String(body['exchangeId'] ?? ''))\n const documentId = sanitiseId(String(body['documentId'] ?? ''))\n const versionId = sanitiseId(String(body['versionId'] ?? ''))\n const documentKind = body['documentKind'] as AssistEditDocumentKind\n if (!orgId || !hostId || !exchangeId || !documentId || !versionId) return null\n if (!ASSIST_EDIT_DOCUMENT_KINDS.includes(documentKind)) return null\n const opCounts: Record<string, number> = {}\n for (const [word, count] of Object.entries(isRecord(body['opCounts']) ? body['opCounts'] : {})) {\n if (!ASSIST_EDIT_OP_COUNT_WORDS.includes(word)) continue\n const parsed = Number(count)\n if (Number.isInteger(parsed) && parsed > 0) opCounts[word] = parsed\n }\n if (!Object.keys(opCounts).length) return null\n return { orgId, hostId, exchangeId, documentKind, documentId, versionId, opCounts }\n}\n\ntype RecordVerdict = 'recorded' | 'duplicate' | 'unknown' | 'no-proposal' | 'too-many'\n\nasync function handler(request: Request): Promise<Response> {\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const authorization = request.headers.get('authorization') ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n let payload: unknown\n try {\n payload = await request.json()\n } catch {\n payload = null\n }\n const report = parseEditAppliedReport(payload)\n if (!report) {\n return Response.json({ error: 'Missing or malformed report' }, { status: 400 })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const resolved = await getOrgForUser(decoded.uid, report.orgId)\n if (!resolved || resolved.orgId !== report.orgId) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n if (\n !staff &&\n !(await memberHasPermissionOnHost(report.orgId, report.hostId, resolved.member, 'ai.generate'))\n ) {\n return permissionRefusal('ai.generate')\n }\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', report.orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: (resolved.org ?? {}) as Record<string, unknown>,\n })\n if (locked) return locked\n const rate = checkRateLimit(`assist-edit-applied:${decoded.uid}`, {\n limit: 30,\n windowMs: 60_000,\n })\n if (!rate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(rate) },\n )\n }\n\n const firestore = app.firestore()\n const orgRef = firestore.collection('orgs').doc(report.orgId)\n const exchange = await orgRef.collection('assistExchanges').doc(report.exchangeId).get()\n const asked = exchange.exists ? (exchange.data() as Record<string, unknown> | undefined) : undefined\n if (!asked || asked['uid'] !== decoded.uid || (asked['hostId'] ?? null) !== report.hostId) {\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n }\n\n const signalRef = orgRef.collection('assistSignals').doc(report.exchangeId)\n const verdict = await firestore.runTransaction(async (tx): Promise<RecordVerdict> => {\n const signal = await tx.get(signalRef)\n const data = signal.exists ? (signal.data() as Record<string, unknown> | undefined) : undefined\n if (!data) return 'unknown'\n const proposed = Number(data['editOps'] ?? 0)\n const document = assistEditDocumentOf(String(data['route'] ?? ''))\n if (\n !(proposed > 0) ||\n !document ||\n document.kind !== report.documentKind ||\n document.documentId !== report.documentId\n ) {\n return 'no-proposal'\n }\n const applied = Object.values(report.opCounts).reduce((sum, count) => sum + count, 0)\n if (applied > proposed) return 'too-many'\n if (data['editAppliedAt']) return 'duplicate'\n tx.update(signalRef, { editAppliedAt: FieldValue.serverTimestamp() })\n return 'recorded'\n })\n\n switch (verdict) {\n case 'unknown':\n return Response.json({ error: 'Unknown exchange' }, { status: 404 })\n case 'no-proposal':\n return Response.json(\n { error: 'That exchange proposed no edit to this document' },\n { status: 409 },\n )\n case 'too-many':\n return Response.json(\n { error: 'More edits reported than that exchange proposed' },\n { status: 409 },\n )\n case 'duplicate':\n return Response.json({ ok: true, duplicate: true }, { status: 200 })\n case 'recorded':\n await logAiEditApplied(\n report.hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n {\n type: report.documentKind,\n id: report.documentId,\n versionId: report.versionId,\n opCounts: report.opCounts,\n },\n )\n // What the applied edit showed about the owner's preferences\n // (AGL-3661), remembered for every later AI job on the site. Rules,\n // no model call; a failure never fails the record it follows.\n await rememberAiSitePreferences(\n firestore,\n report.hostId,\n aiPreferencesFromEdit({ question: String(asked['question'] ?? ''), opCounts: report.opCounts }),\n ).catch((error) => console.warn('ai site memory unwritten', { hostId: report.hostId, error }))\n return Response.json({ ok: true }, { status: 200 })\n }\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json({ error: 'The edit could not be recorded' }, { status: 500 })\n }\n}\n\nexport const dynamic = 'force-dynamic'\nexport { handler as POST }\n"],"names":["FieldValue","permissionRefusal","checkRateLimit","emailUnverifiedResponse","firebaseAdmin","getOrgForUser","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","memberHasPermissionOnHost","rateLimitHeaders","invalidIdTokenResponse","logAiEditApplied","ASSIST_EDIT_DOCUMENT_KINDS","ASSIST_EDIT_OP_COUNT_WORDS","assistEditDocumentOf","aiPreferencesFromEdit","rememberAiSitePreferences","sanitiseId","isRecord","value","Array","isArray","parseEditAppliedReport","payload","body","orgId","String","hostId","exchangeId","documentId","versionId","documentKind","includes","opCounts","word","count","Object","entries","parsed","Number","isInteger","keys","length","handler","request","method","Response","json","error","status","authorization","headers","get","idToken","startsWith","slice","undefined","report","resolved","asked","app","decoded","auth","verifyIdToken","email_verified","staff","uid","member","locked","org","rate","limit","windowMs","allowed","reason","firestore","orgRef","collection","doc","exchange","exists","data","signalRef","verdict","runTransaction","tx","signal","proposed","document","kind","applied","values","reduce","sum","update","editAppliedAt","serverTimestamp","ok","duplicate","email","type","id","question","catch","console","warn","unauthenticated","dynamic","POST"],"mappings":"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"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { type AssistEditDocumentKind } from '../model/assist-edit';
18
+ import { type AssistBuildDraft } from '../model/assist-follow-up';
18
19
  /**
19
20
  * Aglyn Assist level 2 — the GUIDE capability (AGL-1988, under AGL-1860).
20
21
  *
@@ -293,6 +294,17 @@ export declare function visibleAssistText(raw: string): string;
293
294
  export declare function finalAssistText(raw: string): string;
294
295
  /** The raw JSON body between the fences, if the model emitted one. */
295
296
  export declare function extractAssistAction(raw: string): string | null;
297
+ /**
298
+ * The navigation to a draft a build in this chat made (AGL-3616), offered
299
+ * beside the view's own when the request listed any. Its destination is the
300
+ * site's Pages list, where every draft waits: the panel, which holds the
301
+ * draft's own address, sends the person to that draft's Besigner instead and
302
+ * asks the request again there. The server never builds an address from a
303
+ * draft, so a ref cannot steer anyone anywhere the registry does not point.
304
+ */
305
+ export declare const ASSIST_OPEN_DRAFT_ACTION: AssistNavigateAction;
306
+ /** The view with the open-draft navigation added, where the request listed drafts. */
307
+ export declare function withAssistOpenDraftAction(view: AssistView | null, drafts: readonly AssistBuildDraft[]): AssistView | null;
296
308
  /** A proposal that survived validation — inert, and safe to show. */
297
309
  export interface AssistProposal {
298
310
  id: string;
@@ -307,6 +319,12 @@ export interface AssistProposal {
307
319
  }>;
308
320
  /** Whether `href` carries the values, or the card must ask the user to type them. */
309
321
  prefill: boolean;
322
+ /**
323
+ * For the open-draft navigation (AGL-3616): the ref of the draft to open,
324
+ * one the request listed. The panel resolves it to the draft's Besigner
325
+ * and asks the request again there.
326
+ */
327
+ draft?: AssistBuildDraft;
310
328
  }
311
329
  /**
312
330
  * Turn a model-emitted action block into a proposal, or nothing.
@@ -326,7 +344,7 @@ export interface AssistProposal {
326
344
  export declare function resolveAssistProposal(rawBlock: string | null, view: AssistView | null, scope: {
327
345
  orgSlug: string;
328
346
  hostId: string;
329
- }): AssistProposal | null;
347
+ }, drafts?: readonly AssistBuildDraft[]): AssistProposal | null;
330
348
  /**
331
349
  * The write boundary, asserted over the live table rather than described in
332
350
  * a comment. Called by the spec; exported so the check travels with the data
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -15,6 +16,7 @@
15
16
  * limitations under the License.
16
17
  */ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
17
18
  import { ASSIST_EDIT_ACTION_ID, ASSIST_EDIT_DOCUMENT_KINDS, ASSIST_EDIT_TOOL_NAME } from "../model/assist-edit.js";
19
+ import { ASSIST_OPEN_DRAFT_ACTION_ID, ASSIST_OPEN_DRAFT_PARAM } from "../model/assist-follow-up.js";
18
20
  /**
19
21
  * Destinations verified to read `assist_*` search params. EMPTY on purpose:
20
22
  * `assertInertActions()` fails any action with `prefill: true` whose route is
@@ -773,6 +775,25 @@ const isNavigateAction = (action)=>action.kind !== 'edit';
773
775
  const body = (end < 0 ? raw.slice(bodyStart) : raw.slice(bodyStart, end)).trim();
774
776
  return body || null;
775
777
  }
778
+ /**
779
+ * The navigation to a draft a build in this chat made (AGL-3616), offered
780
+ * beside the view's own when the request listed any. Its destination is the
781
+ * site's Pages list, where every draft waits: the panel, which holds the
782
+ * draft's own address, sends the person to that draft's Besigner instead and
783
+ * asks the request again there. The server never builds an address from a
784
+ * draft, so a ref cannot steer anyone anywhere the registry does not point.
785
+ */ export const ASSIST_OPEN_DRAFT_ACTION = orgAction(ASSIST_OPEN_DRAFT_ACTION_ID, 'Open the draft in the Besigner', 'a draft this chat built, in the Besigner, where your request is asked again', '/[orgSlug]/hosts/[host]/screens', [
786
+ ASSIST_OPEN_DRAFT_PARAM
787
+ ]);
788
+ /** The view with the open-draft navigation added, where the request listed drafts. */ export function withAssistOpenDraftAction(view, drafts) {
789
+ if (!view || !drafts.length) return view;
790
+ return _extends({}, view, {
791
+ actions: [
792
+ ...view.actions,
793
+ ASSIST_OPEN_DRAFT_ACTION
794
+ ]
795
+ });
796
+ }
776
797
  const MAX_PARAM_VALUE_CHARS = 200;
777
798
  /**
778
799
  * Turn a model-emitted action block into a proposal, or nothing.
@@ -788,7 +809,7 @@ const MAX_PARAM_VALUE_CHARS = 200;
788
809
  *
789
810
  * Anything unexpected returns null. A dropped proposal costs the user a
790
811
  * button; an honoured bad one costs them trust.
791
- */ export function resolveAssistProposal(rawBlock, view, scope) {
812
+ */ export function resolveAssistProposal(rawBlock, view, scope, drafts = []) {
792
813
  var _parsed_id, _parsed_params;
793
814
  if (!rawBlock || !view) return null;
794
815
  const navigations = view.actions.filter(isNavigateAction);
@@ -817,6 +838,22 @@ const MAX_PARAM_VALUE_CHARS = 200;
817
838
  }
818
839
  const href = buildActionHref(action, scope, values);
819
840
  if (!href) return null;
841
+ // The open-draft navigation names a draft the request listed, or nothing.
842
+ if (action.id === ASSIST_OPEN_DRAFT_ACTION_ID) {
843
+ var _values_find;
844
+ const ref = (_values_find = values.find((value)=>value.name === ASSIST_OPEN_DRAFT_PARAM)) == null ? void 0 : _values_find.value;
845
+ const draft = drafts.find((one)=>one.ref === ref);
846
+ if (!draft) return null;
847
+ return {
848
+ id: action.id,
849
+ label: `Open “${draft.label}” in the Besigner`,
850
+ outcome: `the ${draft.noun} “${draft.label}” in the Besigner, where your request is asked again`,
851
+ href,
852
+ values: [],
853
+ prefill: false,
854
+ draft
855
+ };
856
+ }
820
857
  return {
821
858
  id: action.id,
822
859
  label: action.label,