@humaan/payload-plugin-ai-assistant 0.0.0-stage → 0.1.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 (190) hide show
  1. package/README.md +277 -2
  2. package/dist/admin/AiAssistantControl.d.ts +11 -0
  3. package/dist/admin/AiAssistantControl.js +31 -0
  4. package/dist/admin/AiAssistantControl.js.map +1 -0
  5. package/dist/admin/AiAssistantPanel.d.ts +10 -0
  6. package/dist/admin/AiAssistantPanel.js +208 -0
  7. package/dist/admin/AiAssistantPanel.js.map +1 -0
  8. package/dist/admin/AiAssistantPanel.module.scss +689 -0
  9. package/dist/admin/AiSourceSwitch.d.ts +13 -0
  10. package/dist/admin/AiSourceSwitch.js +14 -0
  11. package/dist/admin/AiSourceSwitch.js.map +1 -0
  12. package/dist/admin/BudgetField.d.ts +8 -0
  13. package/dist/admin/BudgetField.js +145 -0
  14. package/dist/admin/BudgetField.js.map +1 -0
  15. package/dist/admin/BudgetField.module.scss +47 -0
  16. package/dist/admin/Conversation.d.ts +12 -0
  17. package/dist/admin/Conversation.js +469 -0
  18. package/dist/admin/Conversation.js.map +1 -0
  19. package/dist/admin/Markdown.d.ts +9 -0
  20. package/dist/admin/Markdown.js +63 -0
  21. package/dist/admin/Markdown.js.map +1 -0
  22. package/dist/admin/PageContextBridge.d.ts +5 -0
  23. package/dist/admin/PageContextBridge.js +72 -0
  24. package/dist/admin/PageContextBridge.js.map +1 -0
  25. package/dist/admin/Pill.d.ts +11 -0
  26. package/dist/admin/Pill.js +27 -0
  27. package/dist/admin/Pill.js.map +1 -0
  28. package/dist/admin/PlanCard.d.ts +14 -0
  29. package/dist/admin/PlanCard.js +465 -0
  30. package/dist/admin/PlanCard.js.map +1 -0
  31. package/dist/admin/SettingsAlerts.d.ts +7 -0
  32. package/dist/admin/SettingsAlerts.js +37 -0
  33. package/dist/admin/SettingsAlerts.js.map +1 -0
  34. package/dist/admin/UsageSummary.d.ts +11 -0
  35. package/dist/admin/UsageSummary.js +129 -0
  36. package/dist/admin/UsageSummary.js.map +1 -0
  37. package/dist/admin/attachmentUpload.d.ts +25 -0
  38. package/dist/admin/attachmentUpload.js +76 -0
  39. package/dist/admin/attachmentUpload.js.map +1 -0
  40. package/dist/admin/openDocument.d.ts +13 -0
  41. package/dist/admin/openDocument.js +34 -0
  42. package/dist/admin/openDocument.js.map +1 -0
  43. package/dist/admin/percent.d.ts +10 -0
  44. package/dist/admin/percent.js +21 -0
  45. package/dist/admin/percent.js.map +1 -0
  46. package/dist/admin/threads.d.ts +21 -0
  47. package/dist/admin/threads.js +92 -0
  48. package/dist/admin/threads.js.map +1 -0
  49. package/dist/admin/toolSteps.d.ts +12 -0
  50. package/dist/admin/toolSteps.js +57 -0
  51. package/dist/admin/toolSteps.js.map +1 -0
  52. package/dist/admin/transport.d.ts +24 -0
  53. package/dist/admin/transport.js +75 -0
  54. package/dist/admin/transport.js.map +1 -0
  55. package/dist/admin/useAttachments.d.ts +32 -0
  56. package/dist/admin/useAttachments.js +91 -0
  57. package/dist/admin/useAttachments.js.map +1 -0
  58. package/dist/admin/useEntityLabel.d.ts +3 -0
  59. package/dist/admin/useEntityLabel.js +23 -0
  60. package/dist/admin/useEntityLabel.js.map +1 -0
  61. package/dist/attachments/collection.d.ts +15 -0
  62. package/dist/attachments/collection.js +90 -0
  63. package/dist/attachments/collection.js.map +1 -0
  64. package/dist/attachments/conversation.d.ts +25 -0
  65. package/dist/attachments/conversation.js +105 -0
  66. package/dist/attachments/conversation.js.map +1 -0
  67. package/dist/attachments/rules.d.ts +9 -0
  68. package/dist/attachments/rules.js +17 -0
  69. package/dist/attachments/rules.js.map +1 -0
  70. package/dist/attachments/store.d.ts +27 -0
  71. package/dist/attachments/store.js +187 -0
  72. package/dist/attachments/store.js.map +1 -0
  73. package/dist/billing.d.ts +60 -0
  74. package/dist/billing.js +116 -0
  75. package/dist/billing.js.map +1 -0
  76. package/dist/chat/history.d.ts +37 -0
  77. package/dist/chat/history.js +165 -0
  78. package/dist/chat/history.js.map +1 -0
  79. package/dist/chat/messages.d.ts +5 -0
  80. package/dist/chat/messages.js +3 -0
  81. package/dist/chat/messages.js.map +1 -0
  82. package/dist/chat/threads.d.ts +33 -0
  83. package/dist/chat/threads.js +77 -0
  84. package/dist/chat/threads.js.map +1 -0
  85. package/dist/chat/tools.d.ts +76 -0
  86. package/dist/chat/tools.js +596 -0
  87. package/dist/chat/tools.js.map +1 -0
  88. package/dist/chat/usage.d.ts +66 -0
  89. package/dist/chat/usage.js +219 -0
  90. package/dist/chat/usage.js.map +1 -0
  91. package/dist/collectionAccess.d.ts +6 -0
  92. package/dist/collectionAccess.js +32 -0
  93. package/dist/collectionAccess.js.map +1 -0
  94. package/dist/constants.d.ts +64 -0
  95. package/dist/constants.js +51 -0
  96. package/dist/constants.js.map +1 -0
  97. package/dist/endpoints/access.d.ts +4 -0
  98. package/dist/endpoints/access.js +28 -0
  99. package/dist/endpoints/access.js.map +1 -0
  100. package/dist/endpoints/allowance.d.ts +22 -0
  101. package/dist/endpoints/allowance.js +37 -0
  102. package/dist/endpoints/allowance.js.map +1 -0
  103. package/dist/endpoints/applyEndpoint.d.ts +12 -0
  104. package/dist/endpoints/applyEndpoint.js +170 -0
  105. package/dist/endpoints/applyEndpoint.js.map +1 -0
  106. package/dist/endpoints/budget.d.ts +30 -0
  107. package/dist/endpoints/budget.js +153 -0
  108. package/dist/endpoints/budget.js.map +1 -0
  109. package/dist/endpoints/chatEndpoint.d.ts +12 -0
  110. package/dist/endpoints/chatEndpoint.js +295 -0
  111. package/dist/endpoints/chatEndpoint.js.map +1 -0
  112. package/dist/endpoints/form.d.ts +23 -0
  113. package/dist/endpoints/form.js +95 -0
  114. package/dist/endpoints/form.js.map +1 -0
  115. package/dist/endpoints/usageEndpoint.d.ts +13 -0
  116. package/dist/endpoints/usageEndpoint.js +70 -0
  117. package/dist/endpoints/usageEndpoint.js.map +1 -0
  118. package/dist/index.d.ts +4 -0
  119. package/dist/index.js +5 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/lexical/editorConfig.d.ts +4 -0
  122. package/dist/lexical/editorConfig.js +19 -0
  123. package/dist/lexical/editorConfig.js.map +1 -0
  124. package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
  125. package/dist/lexical/lexicalToMarkdown.js +10 -0
  126. package/dist/lexical/lexicalToMarkdown.js.map +1 -0
  127. package/dist/lexical/markdownToLexical.d.ts +7 -0
  128. package/dist/lexical/markdownToLexical.js +10 -0
  129. package/dist/lexical/markdownToLexical.js.map +1 -0
  130. package/dist/plan/allowed.d.ts +7 -0
  131. package/dist/plan/allowed.js +19 -0
  132. package/dist/plan/allowed.js.map +1 -0
  133. package/dist/plan/applyPlan.d.ts +16 -0
  134. package/dist/plan/applyPlan.js +353 -0
  135. package/dist/plan/applyPlan.js.map +1 -0
  136. package/dist/plan/catalogue.d.ts +37 -0
  137. package/dist/plan/catalogue.js +157 -0
  138. package/dist/plan/catalogue.js.map +1 -0
  139. package/dist/plan/diff.d.ts +13 -0
  140. package/dist/plan/diff.js +87 -0
  141. package/dist/plan/diff.js.map +1 -0
  142. package/dist/plan/parsePlan.d.ts +27 -0
  143. package/dist/plan/parsePlan.js +322 -0
  144. package/dist/plan/parsePlan.js.map +1 -0
  145. package/dist/plan/prompt.d.ts +1 -0
  146. package/dist/plan/prompt.js +45 -0
  147. package/dist/plan/prompt.js.map +1 -0
  148. package/dist/plan/stored.d.ts +53 -0
  149. package/dist/plan/stored.js +3 -0
  150. package/dist/plan/stored.js.map +1 -0
  151. package/dist/plan/tokens.d.ts +8 -0
  152. package/dist/plan/tokens.js +79 -0
  153. package/dist/plan/tokens.js.map +1 -0
  154. package/dist/plan/view.d.ts +4 -0
  155. package/dist/plan/view.js +43 -0
  156. package/dist/plan/view.js.map +1 -0
  157. package/dist/plugin.d.ts +3 -0
  158. package/dist/plugin.js +464 -0
  159. package/dist/plugin.js.map +1 -0
  160. package/dist/read/outline.d.ts +7 -0
  161. package/dist/read/outline.js +70 -0
  162. package/dist/read/outline.js.map +1 -0
  163. package/dist/read/readTarget.d.ts +4 -0
  164. package/dist/read/readTarget.js +13 -0
  165. package/dist/read/readTarget.js.map +1 -0
  166. package/dist/read/readable.d.ts +8 -0
  167. package/dist/read/readable.js +131 -0
  168. package/dist/read/readable.js.map +1 -0
  169. package/dist/runtime.d.ts +15 -0
  170. package/dist/runtime.js +14 -0
  171. package/dist/runtime.js.map +1 -0
  172. package/dist/schema/fields.d.ts +113 -0
  173. package/dist/schema/fields.js +1036 -0
  174. package/dist/schema/fields.js.map +1 -0
  175. package/dist/schema/listingFields.d.ts +9 -0
  176. package/dist/schema/listingFields.js +95 -0
  177. package/dist/schema/listingFields.js.map +1 -0
  178. package/dist/schema/slugify.d.ts +2 -0
  179. package/dist/schema/slugify.js +5 -0
  180. package/dist/schema/slugify.js.map +1 -0
  181. package/dist/settings.d.ts +10 -0
  182. package/dist/settings.js +54 -0
  183. package/dist/settings.js.map +1 -0
  184. package/dist/types.d.ts +198 -0
  185. package/dist/types.js +6 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/uploads/routeAsset.d.ts +27 -0
  188. package/dist/uploads/routeAsset.js +127 -0
  189. package/dist/uploads/routeAsset.js.map +1 -0
  190. package/package.json +122 -3
@@ -0,0 +1,157 @@
1
+ import { deniedSlugs } from '../constants.js';
2
+ import { pluginRuntime } from '../runtime.js';
3
+ import { assistantNote, blockSlugs, formatFieldGuide } from '../schema/fields.js';
4
+ const denied = new Set(deniedSlugs);
5
+ /** Every collection and global the assistant can read, with the editor's own access. */ export function planTargets(config) {
6
+ const collections = (config.collections ?? []).flatMap((collection)=>{
7
+ if (denied.has(collection.slug) || collection.auth) {
8
+ return [];
9
+ }
10
+ return [
11
+ collectionTarget(collection)
12
+ ];
13
+ });
14
+ const globals = (config.globals ?? []).flatMap((global)=>{
15
+ if (denied.has(global.slug)) {
16
+ return [];
17
+ }
18
+ return [
19
+ globalTarget(global)
20
+ ];
21
+ });
22
+ return [
23
+ ...collections,
24
+ ...globals
25
+ ];
26
+ }
27
+ /**
28
+ * Whether a plan may change a target. Never what the assistant cannot read, nor Payload's own `payload-*`
29
+ * collections, nor anything the plugin options deny.
30
+ */ export function canChangeTarget(config, kind, slug) {
31
+ if (!findTarget(config, kind, slug)) {
32
+ return false;
33
+ }
34
+ const runtime = pluginRuntime(config);
35
+ if (kind === 'global') {
36
+ return !runtime.deniedGlobals.includes(slug);
37
+ }
38
+ return !slug.startsWith('payload-') && !runtime.deniedCollections.includes(slug);
39
+ }
40
+ /** Upload collections a plan may save attachments into. */ export function uploadTargets(config) {
41
+ return (config.collections ?? []).flatMap((collection)=>{
42
+ if (!collection.upload || !canChangeTarget(config, 'collection', collection.slug)) {
43
+ return [];
44
+ }
45
+ return [
46
+ {
47
+ slug: collection.slug,
48
+ mimeTypes: mimeTypesFromUpload(collection.upload)
49
+ }
50
+ ];
51
+ });
52
+ }
53
+ export function findTarget(config, kind, slug) {
54
+ return planTargets(config).find((target)=>target.kind === kind && target.slug === slug);
55
+ }
56
+ export function collectionFields(config, slug) {
57
+ return config.collections?.find((collection)=>collection.slug === slug)?.fields;
58
+ }
59
+ export function globalFields(config, slug) {
60
+ return config.globals?.find((global)=>global.slug === slug)?.fields;
61
+ }
62
+ export function titleFieldOf(config, slug) {
63
+ const collection = config.collections?.find((item)=>item.slug === slug);
64
+ return typeof collection?.admin.useAsTitle === 'string' ? collection.admin.useAsTitle : 'title';
65
+ }
66
+ export function describeTarget(config, kind, slug, rules, blocks = []) {
67
+ const target = findTarget(config, kind, slug);
68
+ if (!target) {
69
+ return {
70
+ error: `Unknown ${kind} "${slug}".`
71
+ };
72
+ }
73
+ const fields = (kind === 'collection' ? collectionFields(config, slug) : globalFields(config, slug)) ?? [];
74
+ const guide = formatFieldGuide(fields, {
75
+ blocks
76
+ });
77
+ const known = blockSlugs(fields);
78
+ const unknown = blocks.filter((block)=>!known.includes(block));
79
+ const listedOnly = known.filter((block)=>!blocks.includes(block));
80
+ const blockNote = [
81
+ listedOnly.length > 0 ? 'Blocks without fields underneath are listed by name only. Call describeTarget again with blocks set to the ones you will write.' : '',
82
+ unknown.length > 0 ? `No block named: ${unknown.join(', ')}.` : ''
83
+ ].filter(Boolean).join(' ');
84
+ const rule = rules?.[slug]?.trim();
85
+ const entity = kind === 'collection' ? config.collections?.find((item)=>item.slug === slug) : config.globals?.find((item)=>item.slug === slug);
86
+ const note = entity ? assistantNote(entity) : undefined;
87
+ return {
88
+ guide: guide || 'This target has no fields the assistant can fill.',
89
+ ...note ? {
90
+ note
91
+ } : {},
92
+ ...blockNote ? {
93
+ blocks: blockNote
94
+ } : {},
95
+ ...rule ? {
96
+ rules: rule
97
+ } : {},
98
+ drafts: target.drafts,
99
+ upload: target.upload
100
+ };
101
+ }
102
+ /** A title search and any field filters, all of which must match. */ export function searchWhere(config, slug, query, filters = []) {
103
+ const conditions = [];
104
+ const trimmed = query.trim();
105
+ if (trimmed) {
106
+ conditions.push({
107
+ [titleFieldOf(config, slug)]: {
108
+ like: trimmed
109
+ }
110
+ });
111
+ }
112
+ for (const filter of filters){
113
+ conditions.push({
114
+ [filter.field]: {
115
+ [filter.operator]: filter.value
116
+ }
117
+ });
118
+ }
119
+ if (conditions.length === 0) {
120
+ return undefined;
121
+ }
122
+ return conditions.length === 1 ? conditions[0] : {
123
+ and: conditions
124
+ };
125
+ }
126
+ function collectionTarget(collection) {
127
+ const labels = collection.labels;
128
+ const label = labels && typeof labels === 'object' && typeof labels.singular === 'string' ? labels.singular : collection.slug;
129
+ return {
130
+ slug: collection.slug,
131
+ kind: 'collection',
132
+ label,
133
+ drafts: hasDrafts(collection.versions),
134
+ upload: Boolean(collection.upload)
135
+ };
136
+ }
137
+ function globalTarget(global) {
138
+ return {
139
+ slug: global.slug,
140
+ kind: 'global',
141
+ label: typeof global.label === 'string' ? global.label : global.slug,
142
+ drafts: hasDrafts(global.versions),
143
+ upload: false
144
+ };
145
+ }
146
+ function hasDrafts(versions) {
147
+ return Boolean(versions && typeof versions === 'object' && versions.drafts);
148
+ }
149
+ function mimeTypesFromUpload(upload) {
150
+ if (!upload || typeof upload !== 'object' || !('mimeTypes' in upload)) {
151
+ return [];
152
+ }
153
+ const types = upload.mimeTypes;
154
+ return Array.isArray(types) ? types.filter((item)=>typeof item === 'string') : [];
155
+ }
156
+
157
+ //# sourceMappingURL=catalogue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/plan/catalogue.ts"],"sourcesContent":["import type {\n CollectionSlug,\n Field,\n SanitizedCollectionConfig,\n SanitizedConfig,\n SanitizedGlobalConfig,\n Where,\n} from 'payload'\n\nimport type { AiAssistantPluginOptions } from '../types.js'\nimport type { PlanTarget } from './stored.js'\n\nimport { deniedSlugs } from '../constants.js'\nimport { pluginRuntime } from '../runtime.js'\nimport { assistantNote, blockSlugs, formatFieldGuide } from '../schema/fields.js'\n\nconst denied = new Set<string>(deniedSlugs)\n\n/** Every collection and global the assistant can read, with the editor's own access. */\nexport function planTargets(config: SanitizedConfig): PlanTarget[] {\n const collections = (config.collections ?? []).flatMap((collection) => {\n if (denied.has(collection.slug) || collection.auth) {\n return []\n }\n return [collectionTarget(collection)]\n })\n const globals = (config.globals ?? []).flatMap((global) => {\n if (denied.has(global.slug)) {\n return []\n }\n return [globalTarget(global)]\n })\n return [...collections, ...globals]\n}\n\n/**\n * Whether a plan may change a target. Never what the assistant cannot read, nor Payload's own `payload-*`\n * collections, nor anything the plugin options deny.\n */\nexport function canChangeTarget(\n config: SanitizedConfig,\n kind: 'collection' | 'global',\n slug: string,\n): boolean {\n if (!findTarget(config, kind, slug)) {\n return false\n }\n const runtime = pluginRuntime(config)\n if (kind === 'global') {\n return !runtime.deniedGlobals.includes(slug)\n }\n return !slug.startsWith('payload-') && !runtime.deniedCollections.includes(slug)\n}\n\n/** Upload collections a plan may save attachments into. */\nexport function uploadTargets(config: SanitizedConfig): { mimeTypes: string[]; slug: string }[] {\n return (config.collections ?? []).flatMap((collection) => {\n if (!collection.upload || !canChangeTarget(config, 'collection', collection.slug)) {\n return []\n }\n return [{ slug: collection.slug, mimeTypes: mimeTypesFromUpload(collection.upload) }]\n })\n}\n\nexport function findTarget(\n config: SanitizedConfig,\n kind: 'collection' | 'global',\n slug: string,\n): PlanTarget | undefined {\n return planTargets(config).find((target) => target.kind === kind && target.slug === slug)\n}\n\nexport function collectionFields(config: SanitizedConfig, slug: string): Field[] | undefined {\n return config.collections?.find((collection) => collection.slug === slug)?.fields\n}\n\nexport function globalFields(config: SanitizedConfig, slug: string): Field[] | undefined {\n return config.globals?.find((global) => global.slug === slug)?.fields\n}\n\nexport function titleFieldOf(config: SanitizedConfig, slug: string): string {\n const collection = config.collections?.find((item) => item.slug === slug)\n return typeof collection?.admin.useAsTitle === 'string' ? collection.admin.useAsTitle : 'title'\n}\n\nexport function describeTarget(\n config: SanitizedConfig,\n kind: 'collection' | 'global',\n slug: string,\n rules: AiAssistantPluginOptions['collectionRules'],\n blocks: readonly string[] = [],\n):\n | {\n blocks?: string\n drafts: boolean\n guide: string\n note?: string\n rules?: string\n upload: boolean\n }\n | { error: string } {\n const target = findTarget(config, kind, slug)\n if (!target) {\n return { error: `Unknown ${kind} \"${slug}\".` }\n }\n const fields =\n (kind === 'collection' ? collectionFields(config, slug) : globalFields(config, slug)) ?? []\n const guide = formatFieldGuide(fields, { blocks })\n const known = blockSlugs(fields)\n const unknown = blocks.filter((block) => !known.includes(block))\n const listedOnly = known.filter((block) => !blocks.includes(block))\n const blockNote = [\n listedOnly.length > 0\n ? 'Blocks without fields underneath are listed by name only. Call describeTarget again with blocks set to the ones you will write.'\n : '',\n unknown.length > 0 ? `No block named: ${unknown.join(', ')}.` : '',\n ]\n .filter(Boolean)\n .join(' ')\n const rule = rules?.[slug as CollectionSlug]?.trim()\n const entity =\n kind === 'collection'\n ? config.collections?.find((item) => item.slug === slug)\n : config.globals?.find((item) => item.slug === slug)\n const note = entity ? assistantNote(entity) : undefined\n return {\n guide: guide || 'This target has no fields the assistant can fill.',\n ...(note ? { note } : {}),\n ...(blockNote ? { blocks: blockNote } : {}),\n ...(rule ? { rules: rule } : {}),\n drafts: target.drafts,\n upload: target.upload,\n }\n}\n\nexport interface SearchFilter {\n /** Dotted field path, such as `category` or `seo.noIndex`. Payload checks it against the schema and the editor's field access. */\n field: string\n operator: 'equals' | 'exists' | 'in' | 'like' | 'not_equals'\n value: (number | string)[] | boolean | number | string\n}\n\n/** A title search and any field filters, all of which must match. */\nexport function searchWhere(\n config: SanitizedConfig,\n slug: string,\n query: string,\n filters: readonly SearchFilter[] = [],\n): undefined | Where {\n const conditions: Where[] = []\n const trimmed = query.trim()\n if (trimmed) {\n conditions.push({ [titleFieldOf(config, slug)]: { like: trimmed } })\n }\n for (const filter of filters) {\n conditions.push({ [filter.field]: { [filter.operator]: filter.value } })\n }\n if (conditions.length === 0) {\n return undefined\n }\n return conditions.length === 1 ? conditions[0] : { and: conditions }\n}\n\nfunction collectionTarget(collection: SanitizedCollectionConfig): PlanTarget {\n const labels = collection.labels\n const label =\n labels && typeof labels === 'object' && typeof labels.singular === 'string'\n ? labels.singular\n : collection.slug\n return {\n slug: collection.slug,\n kind: 'collection',\n label,\n drafts: hasDrafts(collection.versions),\n upload: Boolean(collection.upload),\n }\n}\n\nfunction globalTarget(global: SanitizedGlobalConfig): PlanTarget {\n return {\n slug: global.slug,\n kind: 'global',\n label: typeof global.label === 'string' ? global.label : global.slug,\n drafts: hasDrafts(global.versions),\n upload: false,\n }\n}\n\nfunction hasDrafts(versions: { drafts?: unknown } | undefined): boolean {\n return Boolean(versions && typeof versions === 'object' && versions.drafts)\n}\n\nfunction mimeTypesFromUpload(upload: SanitizedCollectionConfig['upload']): string[] {\n if (!upload || typeof upload !== 'object' || !('mimeTypes' in upload)) {\n return []\n }\n const types = upload.mimeTypes\n return Array.isArray(types)\n ? types.filter((item): item is string => typeof item === 'string')\n : []\n}\n"],"names":["deniedSlugs","pluginRuntime","assistantNote","blockSlugs","formatFieldGuide","denied","Set","planTargets","config","collections","flatMap","collection","has","slug","auth","collectionTarget","globals","global","globalTarget","canChangeTarget","kind","findTarget","runtime","deniedGlobals","includes","startsWith","deniedCollections","uploadTargets","upload","mimeTypes","mimeTypesFromUpload","find","target","collectionFields","fields","globalFields","titleFieldOf","item","admin","useAsTitle","describeTarget","rules","blocks","error","guide","known","unknown","filter","block","listedOnly","blockNote","length","join","Boolean","rule","trim","entity","note","undefined","drafts","searchWhere","query","filters","conditions","trimmed","push","like","field","operator","value","and","labels","label","singular","hasDrafts","versions","types","Array","isArray"],"mappings":"AAYA,SAASA,WAAW,QAAQ,kBAAiB;AAC7C,SAASC,aAAa,QAAQ,gBAAe;AAC7C,SAASC,aAAa,EAAEC,UAAU,EAAEC,gBAAgB,QAAQ,sBAAqB;AAEjF,MAAMC,SAAS,IAAIC,IAAYN;AAE/B,sFAAsF,GACtF,OAAO,SAASO,YAAYC,MAAuB;IACjD,MAAMC,cAAc,AAACD,CAAAA,OAAOC,WAAW,IAAI,EAAE,AAAD,EAAGC,OAAO,CAAC,CAACC;QACtD,IAAIN,OAAOO,GAAG,CAACD,WAAWE,IAAI,KAAKF,WAAWG,IAAI,EAAE;YAClD,OAAO,EAAE;QACX;QACA,OAAO;YAACC,iBAAiBJ;SAAY;IACvC;IACA,MAAMK,UAAU,AAACR,CAAAA,OAAOQ,OAAO,IAAI,EAAE,AAAD,EAAGN,OAAO,CAAC,CAACO;QAC9C,IAAIZ,OAAOO,GAAG,CAACK,OAAOJ,IAAI,GAAG;YAC3B,OAAO,EAAE;QACX;QACA,OAAO;YAACK,aAAaD;SAAQ;IAC/B;IACA,OAAO;WAAIR;WAAgBO;KAAQ;AACrC;AAEA;;;CAGC,GACD,OAAO,SAASG,gBACdX,MAAuB,EACvBY,IAA6B,EAC7BP,IAAY;IAEZ,IAAI,CAACQ,WAAWb,QAAQY,MAAMP,OAAO;QACnC,OAAO;IACT;IACA,MAAMS,UAAUrB,cAAcO;IAC9B,IAAIY,SAAS,UAAU;QACrB,OAAO,CAACE,QAAQC,aAAa,CAACC,QAAQ,CAACX;IACzC;IACA,OAAO,CAACA,KAAKY,UAAU,CAAC,eAAe,CAACH,QAAQI,iBAAiB,CAACF,QAAQ,CAACX;AAC7E;AAEA,yDAAyD,GACzD,OAAO,SAASc,cAAcnB,MAAuB;IACnD,OAAO,AAACA,CAAAA,OAAOC,WAAW,IAAI,EAAE,AAAD,EAAGC,OAAO,CAAC,CAACC;QACzC,IAAI,CAACA,WAAWiB,MAAM,IAAI,CAACT,gBAAgBX,QAAQ,cAAcG,WAAWE,IAAI,GAAG;YACjF,OAAO,EAAE;QACX;QACA,OAAO;YAAC;gBAAEA,MAAMF,WAAWE,IAAI;gBAAEgB,WAAWC,oBAAoBnB,WAAWiB,MAAM;YAAE;SAAE;IACvF;AACF;AAEA,OAAO,SAASP,WACdb,MAAuB,EACvBY,IAA6B,EAC7BP,IAAY;IAEZ,OAAON,YAAYC,QAAQuB,IAAI,CAAC,CAACC,SAAWA,OAAOZ,IAAI,KAAKA,QAAQY,OAAOnB,IAAI,KAAKA;AACtF;AAEA,OAAO,SAASoB,iBAAiBzB,MAAuB,EAAEK,IAAY;IACpE,OAAOL,OAAOC,WAAW,EAAEsB,KAAK,CAACpB,aAAeA,WAAWE,IAAI,KAAKA,OAAOqB;AAC7E;AAEA,OAAO,SAASC,aAAa3B,MAAuB,EAAEK,IAAY;IAChE,OAAOL,OAAOQ,OAAO,EAAEe,KAAK,CAACd,SAAWA,OAAOJ,IAAI,KAAKA,OAAOqB;AACjE;AAEA,OAAO,SAASE,aAAa5B,MAAuB,EAAEK,IAAY;IAChE,MAAMF,aAAaH,OAAOC,WAAW,EAAEsB,KAAK,CAACM,OAASA,KAAKxB,IAAI,KAAKA;IACpE,OAAO,OAAOF,YAAY2B,MAAMC,eAAe,WAAW5B,WAAW2B,KAAK,CAACC,UAAU,GAAG;AAC1F;AAEA,OAAO,SAASC,eACdhC,MAAuB,EACvBY,IAA6B,EAC7BP,IAAY,EACZ4B,KAAkD,EAClDC,SAA4B,EAAE;IAW9B,MAAMV,SAASX,WAAWb,QAAQY,MAAMP;IACxC,IAAI,CAACmB,QAAQ;QACX,OAAO;YAAEW,OAAO,CAAC,QAAQ,EAAEvB,KAAK,EAAE,EAAEP,KAAK,EAAE,CAAC;QAAC;IAC/C;IACA,MAAMqB,SACJ,AAACd,CAAAA,SAAS,eAAea,iBAAiBzB,QAAQK,QAAQsB,aAAa3B,QAAQK,KAAI,KAAM,EAAE;IAC7F,MAAM+B,QAAQxC,iBAAiB8B,QAAQ;QAAEQ;IAAO;IAChD,MAAMG,QAAQ1C,WAAW+B;IACzB,MAAMY,UAAUJ,OAAOK,MAAM,CAAC,CAACC,QAAU,CAACH,MAAMrB,QAAQ,CAACwB;IACzD,MAAMC,aAAaJ,MAAME,MAAM,CAAC,CAACC,QAAU,CAACN,OAAOlB,QAAQ,CAACwB;IAC5D,MAAME,YAAY;QAChBD,WAAWE,MAAM,GAAG,IAChB,oIACA;QACJL,QAAQK,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAEL,QAAQM,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG;KACjE,CACEL,MAAM,CAACM,SACPD,IAAI,CAAC;IACR,MAAME,OAAOb,OAAO,CAAC5B,KAAuB,EAAE0C;IAC9C,MAAMC,SACJpC,SAAS,eACLZ,OAAOC,WAAW,EAAEsB,KAAK,CAACM,OAASA,KAAKxB,IAAI,KAAKA,QACjDL,OAAOQ,OAAO,EAAEe,KAAK,CAACM,OAASA,KAAKxB,IAAI,KAAKA;IACnD,MAAM4C,OAAOD,SAAStD,cAAcsD,UAAUE;IAC9C,OAAO;QACLd,OAAOA,SAAS;QAChB,GAAIa,OAAO;YAAEA;QAAK,IAAI,CAAC,CAAC;QACxB,GAAIP,YAAY;YAAER,QAAQQ;QAAU,IAAI,CAAC,CAAC;QAC1C,GAAII,OAAO;YAAEb,OAAOa;QAAK,IAAI,CAAC,CAAC;QAC/BK,QAAQ3B,OAAO2B,MAAM;QACrB/B,QAAQI,OAAOJ,MAAM;IACvB;AACF;AASA,mEAAmE,GACnE,OAAO,SAASgC,YACdpD,MAAuB,EACvBK,IAAY,EACZgD,KAAa,EACbC,UAAmC,EAAE;IAErC,MAAMC,aAAsB,EAAE;IAC9B,MAAMC,UAAUH,MAAMN,IAAI;IAC1B,IAAIS,SAAS;QACXD,WAAWE,IAAI,CAAC;YAAE,CAAC7B,aAAa5B,QAAQK,MAAM,EAAE;gBAAEqD,MAAMF;YAAQ;QAAE;IACpE;IACA,KAAK,MAAMjB,UAAUe,QAAS;QAC5BC,WAAWE,IAAI,CAAC;YAAE,CAAClB,OAAOoB,KAAK,CAAC,EAAE;gBAAE,CAACpB,OAAOqB,QAAQ,CAAC,EAAErB,OAAOsB,KAAK;YAAC;QAAE;IACxE;IACA,IAAIN,WAAWZ,MAAM,KAAK,GAAG;QAC3B,OAAOO;IACT;IACA,OAAOK,WAAWZ,MAAM,KAAK,IAAIY,UAAU,CAAC,EAAE,GAAG;QAAEO,KAAKP;IAAW;AACrE;AAEA,SAAShD,iBAAiBJ,UAAqC;IAC7D,MAAM4D,SAAS5D,WAAW4D,MAAM;IAChC,MAAMC,QACJD,UAAU,OAAOA,WAAW,YAAY,OAAOA,OAAOE,QAAQ,KAAK,WAC/DF,OAAOE,QAAQ,GACf9D,WAAWE,IAAI;IACrB,OAAO;QACLA,MAAMF,WAAWE,IAAI;QACrBO,MAAM;QACNoD;QACAb,QAAQe,UAAU/D,WAAWgE,QAAQ;QACrC/C,QAAQyB,QAAQ1C,WAAWiB,MAAM;IACnC;AACF;AAEA,SAASV,aAAaD,MAA6B;IACjD,OAAO;QACLJ,MAAMI,OAAOJ,IAAI;QACjBO,MAAM;QACNoD,OAAO,OAAOvD,OAAOuD,KAAK,KAAK,WAAWvD,OAAOuD,KAAK,GAAGvD,OAAOJ,IAAI;QACpE8C,QAAQe,UAAUzD,OAAO0D,QAAQ;QACjC/C,QAAQ;IACV;AACF;AAEA,SAAS8C,UAAUC,QAA0C;IAC3D,OAAOtB,QAAQsB,YAAY,OAAOA,aAAa,YAAYA,SAAShB,MAAM;AAC5E;AAEA,SAAS7B,oBAAoBF,MAA2C;IACtE,IAAI,CAACA,UAAU,OAAOA,WAAW,YAAY,CAAE,CAAA,eAAeA,MAAK,GAAI;QACrE,OAAO,EAAE;IACX;IACA,MAAMgD,QAAQhD,OAAOC,SAAS;IAC9B,OAAOgD,MAAMC,OAAO,CAACF,SACjBA,MAAM7B,MAAM,CAAC,CAACV,OAAyB,OAAOA,SAAS,YACvD,EAAE;AACR"}
@@ -0,0 +1,13 @@
1
+ import type { FieldChangeView } from '../types.js';
2
+ import type { StoredFile, StoredOperation } from './stored.js';
3
+ /**
4
+ * What an operation will write, field by field, with the current value beside it.
5
+ * Nested groups become dotted paths. Lists and blocks are shown whole.
6
+ * `current` is the document as the assistant reads it, so rich text is markdown on both sides.
7
+ */
8
+ export declare function fieldChanges({ operation, current, files, operations, }: {
9
+ current?: unknown;
10
+ files: readonly StoredFile[];
11
+ operation: StoredOperation;
12
+ operations: readonly StoredOperation[];
13
+ }): FieldChangeView[];
@@ -0,0 +1,87 @@
1
+ import { valueAtPath } from './tokens.js';
2
+ const maxChanges = 30;
3
+ const maxValueCharacters = 2_000;
4
+ /**
5
+ * What an operation will write, field by field, with the current value beside it.
6
+ * Nested groups become dotted paths. Lists and blocks are shown whole.
7
+ * `current` is the document as the assistant reads it, so rich text is markdown on both sides.
8
+ */ export function fieldChanges({ operation, current, files, operations }) {
9
+ const show = (value)=>display(value, files, operations);
10
+ const changes = leaves(operation.set).map(([path, value])=>{
11
+ const before = current === undefined ? undefined : valueAtPath(current, path);
12
+ return {
13
+ path,
14
+ ...isEmpty(before) ? {} : {
15
+ before: show(before)
16
+ },
17
+ after: show(value)
18
+ };
19
+ });
20
+ for (const append of operation.append){
21
+ changes.push({
22
+ path: append.path,
23
+ added: true,
24
+ after: show(append.blocks)
25
+ });
26
+ }
27
+ return changes.slice(0, maxChanges);
28
+ }
29
+ function leaves(value, prefix = '') {
30
+ return Object.entries(value).flatMap(([key, item])=>{
31
+ const path = prefix ? `${prefix}.${key}` : key;
32
+ if (isRecord(item) && !isToken(item) && Object.keys(item).length > 0) {
33
+ return leaves(item, path);
34
+ }
35
+ return [
36
+ [
37
+ path,
38
+ item
39
+ ]
40
+ ];
41
+ });
42
+ }
43
+ function display(value, files, operations) {
44
+ const named = withTokenNames(value, files, operations);
45
+ if (typeof named === 'string') {
46
+ return cut(named);
47
+ }
48
+ if (typeof named === 'number' || typeof named === 'boolean') {
49
+ return String(named);
50
+ }
51
+ return cut(JSON.stringify(named, null, 2) ?? '');
52
+ }
53
+ /** Swaps `{ "$file": 0 }` and `{ "$op": 1 }` for words the editor recognises. */ function withTokenNames(value, files, operations) {
54
+ if (Array.isArray(value)) {
55
+ return value.map((item)=>withTokenNames(item, files, operations));
56
+ }
57
+ if (!isRecord(value)) {
58
+ return value;
59
+ }
60
+ if (isToken(value)) {
61
+ if ('$file' in value) {
62
+ const file = files.find((item)=>item.index === value.$file);
63
+ return `Attached file: ${file?.title || file?.name || `#${String(value.$file)}`}`;
64
+ }
65
+ const index = typeof value.$op === 'number' ? value.$op : -1;
66
+ return `New document: ${operations[index]?.label ?? `step ${index + 1}`}`;
67
+ }
68
+ return Object.fromEntries(Object.entries(value).map(([key, item])=>[
69
+ key,
70
+ withTokenNames(item, files, operations)
71
+ ]));
72
+ }
73
+ function isToken(value) {
74
+ const keys = Object.keys(value);
75
+ return keys.length === 1 && (keys[0] === '$file' || keys[0] === '$op');
76
+ }
77
+ function isEmpty(value) {
78
+ return value === undefined || value === null || value === '' || Array.isArray(value) && value.length === 0;
79
+ }
80
+ function cut(value) {
81
+ return value.length > maxValueCharacters ? `${value.slice(0, maxValueCharacters)}…` : value;
82
+ }
83
+ function isRecord(value) {
84
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
85
+ }
86
+
87
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/plan/diff.ts"],"sourcesContent":["import type { FieldChangeView } from '../types.js'\nimport type { StoredFile, StoredOperation } from './stored.js'\n\nimport { valueAtPath } from './tokens.js'\n\nconst maxChanges = 30\nconst maxValueCharacters = 2_000\n\n/**\n * What an operation will write, field by field, with the current value beside it.\n * Nested groups become dotted paths. Lists and blocks are shown whole.\n * `current` is the document as the assistant reads it, so rich text is markdown on both sides.\n */\nexport function fieldChanges({\n operation,\n current,\n files,\n operations,\n}: {\n current?: unknown\n files: readonly StoredFile[]\n operation: StoredOperation\n operations: readonly StoredOperation[]\n}): FieldChangeView[] {\n const show = (value: unknown) => display(value, files, operations)\n const changes: FieldChangeView[] = leaves(operation.set).map(([path, value]) => {\n const before = current === undefined ? undefined : valueAtPath(current, path)\n return {\n path,\n ...(isEmpty(before) ? {} : { before: show(before) }),\n after: show(value),\n }\n })\n for (const append of operation.append) {\n changes.push({ path: append.path, added: true, after: show(append.blocks) })\n }\n return changes.slice(0, maxChanges)\n}\n\nfunction leaves(value: Record<string, unknown>, prefix = ''): [string, unknown][] {\n return Object.entries(value).flatMap(([key, item]): [string, unknown][] => {\n const path = prefix ? `${prefix}.${key}` : key\n if (isRecord(item) && !isToken(item) && Object.keys(item).length > 0) {\n return leaves(item, path)\n }\n return [[path, item]]\n })\n}\n\nfunction display(\n value: unknown,\n files: readonly StoredFile[],\n operations: readonly StoredOperation[],\n): string {\n const named = withTokenNames(value, files, operations)\n if (typeof named === 'string') {\n return cut(named)\n }\n if (typeof named === 'number' || typeof named === 'boolean') {\n return String(named)\n }\n return cut(JSON.stringify(named, null, 2) ?? '')\n}\n\n/** Swaps `{ \"$file\": 0 }` and `{ \"$op\": 1 }` for words the editor recognises. */\nfunction withTokenNames(\n value: unknown,\n files: readonly StoredFile[],\n operations: readonly StoredOperation[],\n): unknown {\n if (Array.isArray(value)) {\n return value.map((item) => withTokenNames(item, files, operations))\n }\n if (!isRecord(value)) {\n return value\n }\n if (isToken(value)) {\n if ('$file' in value) {\n const file = files.find((item) => item.index === value.$file)\n return `Attached file: ${file?.title || file?.name || `#${String(value.$file)}`}`\n }\n const index = typeof value.$op === 'number' ? value.$op : -1\n return `New document: ${operations[index]?.label ?? `step ${index + 1}`}`\n }\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, withTokenNames(item, files, operations)]),\n )\n}\n\nfunction isToken(value: Record<string, unknown>): boolean {\n const keys = Object.keys(value)\n return keys.length === 1 && (keys[0] === '$file' || keys[0] === '$op')\n}\n\nfunction isEmpty(value: unknown): boolean {\n return (\n value === undefined ||\n value === null ||\n value === '' ||\n (Array.isArray(value) && value.length === 0)\n )\n}\n\nfunction cut(value: string): string {\n return value.length > maxValueCharacters ? `${value.slice(0, maxValueCharacters)}…` : value\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n"],"names":["valueAtPath","maxChanges","maxValueCharacters","fieldChanges","operation","current","files","operations","show","value","display","changes","leaves","set","map","path","before","undefined","isEmpty","after","append","push","added","blocks","slice","prefix","Object","entries","flatMap","key","item","isRecord","isToken","keys","length","named","withTokenNames","cut","String","JSON","stringify","Array","isArray","file","find","index","$file","title","name","$op","label","fromEntries","Boolean"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { PlanTarget, StoredFile, StoredPlan } from './stored.js';
2
+ /** A file attached to the conversation, by its position in the conversation's attachments. */
3
+ export interface IncomingFile {
4
+ /** The stored attachment, read again when a plan that lists it is applied. */
5
+ attachmentId: string;
6
+ /** Where a plan would save it. Empty when no allowed upload collection accepts it, so it can be read but not saved. */
7
+ collection: string;
8
+ index: number;
9
+ kind: StoredFile['kind'];
10
+ name: string;
11
+ savesImmediately: boolean;
12
+ size: number;
13
+ type: string;
14
+ }
15
+ /**
16
+ * Turns the model's submitPlan arguments into a plan the server can run.
17
+ * Field values are kept, but only for collections the editor is allowed to change.
18
+ */
19
+ export declare function parsePlan(input: unknown, targets: readonly PlanTarget[], files: readonly IncomingFile[]): {
20
+ errors: string[];
21
+ ok: false;
22
+ } | {
23
+ ok: true;
24
+ plan: StoredPlan;
25
+ };
26
+ /** `{ "seo.image": id }` becomes `{ seo: { image: id } }`, the shape the fields expect. */
27
+ export declare function nestDottedKeys(value: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,322 @@
1
+ import { deniedSlugs } from '../constants.js';
2
+ import { topLevelSetPaths } from '../schema/listingFields.js';
3
+ const denied = new Set(deniedSlugs);
4
+ function isRecord(value) {
5
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
6
+ }
7
+ function text(value) {
8
+ return typeof value === 'string' ? value.trim() : '';
9
+ }
10
+ /**
11
+ * Turns the model's submitPlan arguments into a plan the server can run.
12
+ * Field values are kept, but only for collections the editor is allowed to change.
13
+ */ export function parsePlan(input, targets, files) {
14
+ if (!isRecord(input)) {
15
+ return {
16
+ ok: false,
17
+ errors: [
18
+ 'The plan must be an object.'
19
+ ]
20
+ };
21
+ }
22
+ const errors = [];
23
+ const summary = text(input.summary);
24
+ if (!summary) {
25
+ errors.push('Add a summary of what will change.');
26
+ }
27
+ const blocked = input.blocked === true;
28
+ const questions = stringList(input.questions);
29
+ const bySlug = new Map(targets.map((target)=>[
30
+ `${target.kind}:${target.slug}`,
31
+ target
32
+ ]));
33
+ const operations = Array.isArray(input.operations) ? input.operations : [];
34
+ if (!Array.isArray(input.operations)) {
35
+ errors.push('operations must be a list.');
36
+ }
37
+ if (blocked && operations.length > 0) {
38
+ errors.push('A blocked plan must not include operations.');
39
+ }
40
+ if (blocked && questions.length === 0) {
41
+ errors.push('Say what the editor needs to decide.');
42
+ }
43
+ const parsedOperations = operations.flatMap((item, index)=>{
44
+ const operation = parseOperation(item, index, bySlug, errors);
45
+ return operation ? [
46
+ operation
47
+ ] : [];
48
+ });
49
+ const referencedFiles = new Set();
50
+ for (const [index, operation] of parsedOperations.entries()){
51
+ const tokens = {
52
+ operationIndex: index,
53
+ fileCount: files.length,
54
+ referencedFiles,
55
+ errors
56
+ };
57
+ collectTokenErrors(operation.set, `operations.${index}.set`, tokens);
58
+ for (const append of operation.append){
59
+ collectTokenErrors(append.blocks, `operations.${index}.append`, tokens);
60
+ }
61
+ }
62
+ const fileNotes = parseFileNotes(input.files, files, errors);
63
+ for (const index of referencedFiles){
64
+ if (!fileNotes.has(index)) {
65
+ errors.push(`List file ${index} in files before a field points at it.`);
66
+ }
67
+ }
68
+ if (!blocked && parsedOperations.length === 0 && fileNotes.size === 0) {
69
+ errors.push('The plan needs an operation, or a file to save.');
70
+ }
71
+ const candidates = parseCandidates(input.candidates, bySlug, errors);
72
+ if (errors.length > 0) {
73
+ return {
74
+ ok: false,
75
+ errors
76
+ };
77
+ }
78
+ return {
79
+ ok: true,
80
+ plan: {
81
+ summary,
82
+ blocked,
83
+ questions,
84
+ candidates,
85
+ operations: parsedOperations,
86
+ files: files.flatMap((file)=>{
87
+ const note = fileNotes.get(file.index);
88
+ if (!note) {
89
+ return [];
90
+ }
91
+ return [
92
+ {
93
+ ...file,
94
+ alt: note.alt,
95
+ title: note.title,
96
+ note: note.note || 'Saved with this request.'
97
+ }
98
+ ];
99
+ })
100
+ }
101
+ };
102
+ }
103
+ function parseOperation(item, index, bySlug, errors) {
104
+ if (!isRecord(item)) {
105
+ errors.push(`Operation ${index + 1} must be an object.`);
106
+ return undefined;
107
+ }
108
+ const action = item.action === 'create' || item.action === 'update' ? item.action : undefined;
109
+ const kind = item.kind === 'global' ? 'global' : item.kind === 'collection' ? 'collection' : undefined;
110
+ const slug = text(item.slug);
111
+ const label = text(item.label);
112
+ const summary = text(item.summary);
113
+ if (!action) {
114
+ errors.push(`Operation ${index + 1} needs action create or update.`);
115
+ }
116
+ if (!kind) {
117
+ errors.push(`Operation ${index + 1} needs kind collection or global.`);
118
+ }
119
+ if (!slug) {
120
+ errors.push(`Operation ${index + 1} needs a slug.`);
121
+ }
122
+ if (!label) {
123
+ errors.push(`Operation ${index + 1} needs a label.`);
124
+ }
125
+ if (!summary) {
126
+ errors.push(`Operation ${index + 1} needs a summary.`);
127
+ }
128
+ if (slug && denied.has(slug)) {
129
+ errors.push(`The assistant cannot change ${slug}.`);
130
+ }
131
+ const target = kind && slug ? bySlug.get(`${kind}:${slug}`) : undefined;
132
+ if (kind && slug && !denied.has(slug) && !target) {
133
+ errors.push(`Unknown ${kind} "${slug}".`);
134
+ }
135
+ if (action === 'create' && kind === 'global') {
136
+ errors.push(`Globals cannot be created. Update ${slug || 'the global'}.`);
137
+ }
138
+ if (action === 'create' && target?.upload) {
139
+ errors.push(`Save files from the attachment list. Do not create a ${slug} document.`);
140
+ }
141
+ if (action === 'update' && !text(item.id) && kind === 'collection') {
142
+ errors.push(`Operation ${index + 1} updates ${slug || 'a document'} and needs an id from searchDocuments or readDocument.`);
143
+ }
144
+ const set = isRecord(item.set) ? item.set : undefined;
145
+ if (item.set !== undefined && !set) {
146
+ errors.push(`Operation ${index + 1} set must be an object.`);
147
+ }
148
+ const append = parseAppend(item.append, index, errors);
149
+ if (!action || !kind || !slug || !label || !summary) {
150
+ return undefined;
151
+ }
152
+ const data = nestDottedKeys(set ?? {});
153
+ return {
154
+ action,
155
+ kind,
156
+ slug,
157
+ id: text(item.id) || undefined,
158
+ label,
159
+ summary,
160
+ savesImmediately: target ? !target.drafts : true,
161
+ fields: topLevelSetPaths(data),
162
+ set: data,
163
+ append
164
+ };
165
+ }
166
+ /** `{ "seo.image": id }` becomes `{ seo: { image: id } }`, the shape the fields expect. */ export function nestDottedKeys(value) {
167
+ const nested = {};
168
+ for (const [key, item] of Object.entries(value)){
169
+ const keys = key.split('.').filter(Boolean);
170
+ let current = nested;
171
+ for (const part of keys.slice(0, -1)){
172
+ if (!isRecord(current[part])) {
173
+ current[part] = {};
174
+ }
175
+ current = current[part];
176
+ }
177
+ const leaf = keys.at(-1);
178
+ if (!leaf) {
179
+ continue;
180
+ }
181
+ const next = isRecord(item) && !isToken(item) ? nestDottedKeys(item) : item;
182
+ current[leaf] = isRecord(current[leaf]) && isRecord(next) ? {
183
+ ...current[leaf],
184
+ ...next
185
+ } : next;
186
+ }
187
+ return nested;
188
+ }
189
+ function isToken(value) {
190
+ const keys = Object.keys(value);
191
+ return keys.length === 1 && (keys[0] === '$file' || keys[0] === '$op');
192
+ }
193
+ function parseAppend(value, index, errors) {
194
+ if (value === undefined) {
195
+ return [];
196
+ }
197
+ if (!Array.isArray(value)) {
198
+ errors.push(`Operation ${index + 1} append must be a list.`);
199
+ return [];
200
+ }
201
+ return value.flatMap((item)=>{
202
+ if (!isRecord(item) || !text(item.path) || !Array.isArray(item.blocks)) {
203
+ errors.push(`Operation ${index + 1} append entries need a path and a blocks list.`);
204
+ return [];
205
+ }
206
+ const blocks = item.blocks.filter(isRecord);
207
+ if (blocks.length !== item.blocks.length) {
208
+ errors.push(`Operation ${index + 1} blocks must be objects with blockType.`);
209
+ }
210
+ return [
211
+ {
212
+ path: text(item.path),
213
+ blocks
214
+ }
215
+ ];
216
+ });
217
+ }
218
+ /**
219
+ * Only the files the plan lists are saved. A conversation keeps its attachments, so a later plan
220
+ * must not save a file again that an earlier plan already saved.
221
+ */ function parseFileNotes(value, files, errors) {
222
+ const notes = new Map();
223
+ if (value === undefined) {
224
+ return notes;
225
+ }
226
+ if (!Array.isArray(value)) {
227
+ errors.push('files must be a list.');
228
+ return notes;
229
+ }
230
+ for (const item of value){
231
+ if (!isRecord(item) || typeof item.index !== 'number') {
232
+ continue;
233
+ }
234
+ const file = files.find((candidate)=>candidate.index === item.index);
235
+ if (!file) {
236
+ errors.push(`File ${item.index} is not attached.`);
237
+ continue;
238
+ }
239
+ if (!file.collection) {
240
+ errors.push(`${file.name} cannot be saved: no upload collection the assistant may change accepts it.`);
241
+ continue;
242
+ }
243
+ const note = {
244
+ alt: text(item.alt),
245
+ title: text(item.title),
246
+ note: text(item.note)
247
+ };
248
+ if (file.kind === 'image' && !note.alt) {
249
+ errors.push(`Add alt text for ${file.name}.`);
250
+ }
251
+ notes.set(item.index, note);
252
+ }
253
+ return notes;
254
+ }
255
+ function parseCandidates(value, bySlug, errors) {
256
+ if (value === undefined) {
257
+ return [];
258
+ }
259
+ if (!Array.isArray(value)) {
260
+ errors.push('candidates must be a list.');
261
+ return [];
262
+ }
263
+ return value.flatMap((item)=>{
264
+ if (!isRecord(item)) {
265
+ return [];
266
+ }
267
+ const kind = item.kind === 'global' ? 'global' : 'collection';
268
+ const slug = text(item.slug);
269
+ const label = text(item.label);
270
+ if (!slug || !label) {
271
+ return [];
272
+ }
273
+ if (!bySlug.has(`${kind}:${slug}`) && !denied.has(slug)) {
274
+ errors.push(`Unknown candidate "${slug}".`);
275
+ }
276
+ return [
277
+ {
278
+ kind,
279
+ slug,
280
+ id: text(item.id) || undefined,
281
+ label
282
+ }
283
+ ];
284
+ });
285
+ }
286
+ function stringList(value) {
287
+ if (!Array.isArray(value)) {
288
+ return [];
289
+ }
290
+ return value.map(text).filter(Boolean);
291
+ }
292
+ function collectTokenErrors(value, path, check) {
293
+ if (Array.isArray(value)) {
294
+ value.forEach((item, index)=>collectTokenErrors(item, `${path}.${index}`, check));
295
+ return;
296
+ }
297
+ if (!isRecord(value)) {
298
+ return;
299
+ }
300
+ const keys = Object.keys(value);
301
+ if (keys.length === 1 && keys[0] === '$file') {
302
+ const index = value.$file;
303
+ if (typeof index !== 'number' || !Number.isInteger(index) || index < 0 || index >= check.fileCount) {
304
+ check.errors.push(`${path} refers to a file that was not attached.`);
305
+ } else {
306
+ check.referencedFiles.add(index);
307
+ }
308
+ return;
309
+ }
310
+ if (keys.length === 1 && keys[0] === '$op') {
311
+ const index = value.$op;
312
+ if (typeof index !== 'number' || !Number.isInteger(index) || index < 0 || index >= check.operationIndex) {
313
+ check.errors.push(`${path} must refer to an earlier operation.`);
314
+ }
315
+ return;
316
+ }
317
+ for (const [key, item] of Object.entries(value)){
318
+ collectTokenErrors(item, `${path}.${key}`, check);
319
+ }
320
+ }
321
+
322
+ //# sourceMappingURL=parsePlan.js.map