@fayz-ai/admin 0.62.0 → 0.63.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 (169) hide show
  1. package/dist/{CommandPalette-B5PMQFDG.js → CommandPalette-XDJJ5E3O.js} +3 -3
  2. package/dist/{CommandPalette-B5PMQFDG.js.map → CommandPalette-XDJJ5E3O.js.map} +1 -1
  3. package/dist/ConnectedBrandingSettings-43ZM3HCI.js +680 -0
  4. package/dist/ConnectedBrandingSettings-43ZM3HCI.js.map +1 -0
  5. package/dist/ConnectedCompanySettings-I4MALI2Q.js +197 -0
  6. package/dist/ConnectedCompanySettings-I4MALI2Q.js.map +1 -0
  7. package/dist/ConnectedModulesSettings-A2VJ5BMJ.js +563 -0
  8. package/dist/ConnectedModulesSettings-A2VJ5BMJ.js.map +1 -0
  9. package/dist/ConnectedSecuritySettings-25AHA4HJ.js +189 -0
  10. package/dist/ConnectedSecuritySettings-25AHA4HJ.js.map +1 -0
  11. package/dist/ConnectedUserProfile-E7USJRUE.js +424 -0
  12. package/dist/ConnectedUserProfile-E7USJRUE.js.map +1 -0
  13. package/dist/FieldRulesEditor-YGIPKI5T.js +129 -0
  14. package/dist/FieldRulesEditor-YGIPKI5T.js.map +1 -0
  15. package/dist/GettingStartedWidget-HMIJ3Y24.js +12 -0
  16. package/dist/GettingStartedWidget-HMIJ3Y24.js.map +1 -0
  17. package/dist/PermissionProfilesTab-3ZPRCHMZ.js +548 -0
  18. package/dist/PermissionProfilesTab-3ZPRCHMZ.js.map +1 -0
  19. package/dist/PersonTimeline-HQZT527S.js +3 -0
  20. package/dist/{PersonTimeline-X7JTPUWJ.js.map → PersonTimeline-HQZT527S.js.map} +1 -1
  21. package/dist/ProjectsSettings-7O2F27MD.js +5 -0
  22. package/dist/ProjectsSettings-7O2F27MD.js.map +1 -0
  23. package/dist/ScheduleEditor-54ELWR45.js +8 -0
  24. package/dist/{ScheduleEditor-L2UJXKMY.js.map → ScheduleEditor-54ELWR45.js.map} +1 -1
  25. package/dist/SeuSiteWidget-QLSUVC63.js +6 -0
  26. package/dist/SeuSiteWidget-QLSUVC63.js.map +1 -0
  27. package/dist/SubscriptionPage-5GE5HFJK.js +17 -0
  28. package/dist/{SubscriptionPage-IDVEFLZ5.js.map → SubscriptionPage-5GE5HFJK.js.map} +1 -1
  29. package/dist/TeamTab-6RAULFLW.js +502 -0
  30. package/dist/TeamTab-6RAULFLW.js.map +1 -0
  31. package/dist/TrialWidget-Z6CLUCU3.js +7 -0
  32. package/dist/TrialWidget-Z6CLUCU3.js.map +1 -0
  33. package/dist/app/AdminShell.d.ts.map +1 -1
  34. package/dist/app/admin-app.d.ts.map +1 -1
  35. package/dist/app/config.d.ts +14 -0
  36. package/dist/app/config.d.ts.map +1 -1
  37. package/dist/app/routing.d.ts +6 -0
  38. package/dist/app/routing.d.ts.map +1 -1
  39. package/dist/app/scaffold.d.ts.map +1 -1
  40. package/dist/billing/index.js +9 -4
  41. package/dist/chunk-2H46WDG5.js +53 -0
  42. package/dist/chunk-2H46WDG5.js.map +1 -0
  43. package/dist/chunk-3HLX7YB2.js +94 -0
  44. package/dist/chunk-3HLX7YB2.js.map +1 -0
  45. package/dist/chunk-3OUD4XBK.js +3 -0
  46. package/dist/chunk-3OUD4XBK.js.map +1 -0
  47. package/dist/{chunk-IXSY4GPK.js → chunk-5D5QLMOQ.js} +5 -3
  48. package/dist/chunk-5D5QLMOQ.js.map +1 -0
  49. package/dist/chunk-5WR62X2R.js +1081 -0
  50. package/dist/chunk-5WR62X2R.js.map +1 -0
  51. package/dist/{chunk-KPYRTOCK.js → chunk-6CQFGMEO.js} +4 -140
  52. package/dist/chunk-6CQFGMEO.js.map +1 -0
  53. package/dist/chunk-6MMSYC2I.js +116 -0
  54. package/dist/chunk-6MMSYC2I.js.map +1 -0
  55. package/dist/chunk-6QFOE3UD.js +1466 -0
  56. package/dist/chunk-6QFOE3UD.js.map +1 -0
  57. package/dist/chunk-6UUFVQ2E.js +283 -0
  58. package/dist/chunk-6UUFVQ2E.js.map +1 -0
  59. package/dist/chunk-7WNLL3F6.js +7889 -0
  60. package/dist/chunk-7WNLL3F6.js.map +1 -0
  61. package/dist/chunk-A74IHAQE.js +439 -0
  62. package/dist/chunk-A74IHAQE.js.map +1 -0
  63. package/dist/{chunk-JOBRLBIQ.js → chunk-AFA6IO23.js} +4 -3
  64. package/dist/chunk-AFA6IO23.js.map +1 -0
  65. package/dist/{chunk-P5UIGQFY.js → chunk-ALCSIN3Z.js} +5 -5
  66. package/dist/chunk-ALCSIN3Z.js.map +1 -0
  67. package/dist/chunk-AWM4AMBS.js +39 -0
  68. package/dist/chunk-AWM4AMBS.js.map +1 -0
  69. package/dist/{chunk-OQARLKZN.js → chunk-BG5Z6KLN.js} +6 -18
  70. package/dist/chunk-BG5Z6KLN.js.map +1 -0
  71. package/dist/chunk-DPNFZONC.js +19 -0
  72. package/dist/chunk-DPNFZONC.js.map +1 -0
  73. package/dist/chunk-DRCV643L.js +193 -0
  74. package/dist/chunk-DRCV643L.js.map +1 -0
  75. package/dist/chunk-E6DF3VR6.js +206 -0
  76. package/dist/chunk-E6DF3VR6.js.map +1 -0
  77. package/dist/chunk-ENHCVGXZ.js +120 -0
  78. package/dist/chunk-ENHCVGXZ.js.map +1 -0
  79. package/dist/chunk-FK6RHGI7.js +56 -0
  80. package/dist/chunk-FK6RHGI7.js.map +1 -0
  81. package/dist/chunk-G5HNA5QK.js +233 -0
  82. package/dist/chunk-G5HNA5QK.js.map +1 -0
  83. package/dist/chunk-HCFO4Z6A.js +53 -0
  84. package/dist/chunk-HCFO4Z6A.js.map +1 -0
  85. package/dist/chunk-JFIU6NQP.js +51 -0
  86. package/dist/chunk-JFIU6NQP.js.map +1 -0
  87. package/dist/chunk-JRZJBMOY.js +3 -0
  88. package/dist/chunk-JRZJBMOY.js.map +1 -0
  89. package/dist/{chunk-3YP7KW47.js → chunk-JVVOL4BB.js} +41 -512
  90. package/dist/chunk-JVVOL4BB.js.map +1 -0
  91. package/dist/chunk-KAQ3UIAA.js +415 -0
  92. package/dist/chunk-KAQ3UIAA.js.map +1 -0
  93. package/dist/{chunk-P6XUDT2P.js → chunk-KJELMULA.js} +4 -51
  94. package/dist/chunk-KJELMULA.js.map +1 -0
  95. package/dist/chunk-KLO7DKXX.js +1651 -0
  96. package/dist/chunk-KLO7DKXX.js.map +1 -0
  97. package/dist/chunk-M2CMDWUI.js +75 -0
  98. package/dist/chunk-M2CMDWUI.js.map +1 -0
  99. package/dist/chunk-PQ47MLTH.js +255 -0
  100. package/dist/chunk-PQ47MLTH.js.map +1 -0
  101. package/dist/{chunk-VX3WJLKK.js → chunk-QD7BMUDF.js} +17 -8
  102. package/dist/chunk-QD7BMUDF.js.map +1 -0
  103. package/dist/{chunk-E4MESMNR.js → chunk-QTSKCPSW.js} +7 -10
  104. package/dist/chunk-QTSKCPSW.js.map +1 -0
  105. package/dist/chunk-S56FQKPL.js +186 -0
  106. package/dist/chunk-S56FQKPL.js.map +1 -0
  107. package/dist/chunk-SGAFPNMA.js +75 -0
  108. package/dist/chunk-SGAFPNMA.js.map +1 -0
  109. package/dist/{chunk-OD7P4L3D.js → chunk-SOKCLVM4.js} +3 -3
  110. package/dist/{chunk-OD7P4L3D.js.map → chunk-SOKCLVM4.js.map} +1 -1
  111. package/dist/chunk-TL42NKTH.js +3 -0
  112. package/dist/chunk-TL42NKTH.js.map +1 -0
  113. package/dist/chunk-U6ZCO2PS.js +2964 -0
  114. package/dist/chunk-U6ZCO2PS.js.map +1 -0
  115. package/dist/chunk-VSXNROYH.js +16 -0
  116. package/dist/chunk-VSXNROYH.js.map +1 -0
  117. package/dist/chunk-WPFZCYZL.js +619 -0
  118. package/dist/chunk-WPFZCYZL.js.map +1 -0
  119. package/dist/chunk-XFTNIBDW.js +23 -0
  120. package/dist/chunk-XFTNIBDW.js.map +1 -0
  121. package/dist/chunk-Y66JDL6I.js +1020 -0
  122. package/dist/chunk-Y66JDL6I.js.map +1 -0
  123. package/dist/chunk-YEHIBPIX.js +137 -0
  124. package/dist/chunk-YEHIBPIX.js.map +1 -0
  125. package/dist/chunk-YKPPCV2Z.js +53 -0
  126. package/dist/chunk-YKPPCV2Z.js.map +1 -0
  127. package/dist/components/chat/markdown.d.ts.map +1 -1
  128. package/dist/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
  129. package/dist/components/settings/SettingsPage.d.ts.map +1 -1
  130. package/dist/connect-XKQJOWR7.js +18 -0
  131. package/dist/connect-XKQJOWR7.js.map +1 -0
  132. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  133. package/dist/crud/CrudListView.d.ts.map +1 -1
  134. package/dist/crud/CrudPage.d.ts.map +1 -1
  135. package/dist/crud/RelatedListTab.d.ts.map +1 -1
  136. package/dist/crud/archetypes/EntityLink.d.ts +1 -1
  137. package/dist/crud/archetypes/EntityLink.d.ts.map +1 -1
  138. package/dist/crud/record-chrome.d.ts.map +1 -1
  139. package/dist/entity-routes-PGU7EHAD.js +4 -0
  140. package/dist/{entity-routes-LXOBW5IB.js.map → entity-routes-PGU7EHAD.js.map} +1 -1
  141. package/dist/hooks/useModuleNavigation.d.ts.map +1 -1
  142. package/dist/impersonation/ImpersonateDialog.d.ts.map +1 -1
  143. package/dist/impersonation/session.d.ts.map +1 -1
  144. package/dist/index.d.ts +1 -1
  145. package/dist/index.d.ts.map +1 -1
  146. package/dist/index.js +11902 -33551
  147. package/dist/index.js.map +1 -1
  148. package/dist/lib/entity-routes.d.ts.map +1 -1
  149. package/dist/org/index.js +4 -2
  150. package/dist/plugins/PluginRegistryManager.d.ts.map +1 -1
  151. package/dist/settings/MessageTemplateRow.d.ts.map +1 -1
  152. package/dist/timeline/PersonTimeline.d.ts.map +1 -1
  153. package/dist/timeline/TimelineList.d.ts.map +1 -1
  154. package/dist/units/UnitRailPage.d.ts.map +1 -1
  155. package/dist/units/useUnitFilter.d.ts.map +1 -1
  156. package/package.json +5 -5
  157. package/dist/PersonTimeline-X7JTPUWJ.js +0 -3
  158. package/dist/ScheduleEditor-L2UJXKMY.js +0 -6
  159. package/dist/SubscriptionPage-IDVEFLZ5.js +0 -8
  160. package/dist/chunk-3YP7KW47.js.map +0 -1
  161. package/dist/chunk-E4MESMNR.js.map +0 -1
  162. package/dist/chunk-IXSY4GPK.js.map +0 -1
  163. package/dist/chunk-JOBRLBIQ.js.map +0 -1
  164. package/dist/chunk-KPYRTOCK.js.map +0 -1
  165. package/dist/chunk-OQARLKZN.js.map +0 -1
  166. package/dist/chunk-P5UIGQFY.js.map +0 -1
  167. package/dist/chunk-P6XUDT2P.js.map +0 -1
  168. package/dist/chunk-VX3WJLKK.js.map +0 -1
  169. package/dist/entity-routes-LXOBW5IB.js +0 -4
@@ -0,0 +1,1466 @@
1
+ import { checkAccess, guardLimit, useRightRailStore } from './chunk-PQ47MLTH.js';
2
+ import { invalidateLimit } from './chunk-S56FQKPL.js';
3
+ import { resolveEntityRoute, openEntity } from './chunk-ALCSIN3Z.js';
4
+ import { loadArea, useChatStore, CORE_QUERY_ENTITIES, resolveAttachmentDigest, listAttachmentNames, nonTabularAttachmentNames, readAllRows, ALWAYS_ON_TOOLS, lookupRecordRef, PROFILE_ACTIONS, resolveToolSearch } from './chunk-KLO7DKXX.js';
5
+ import { usePermissionsStore } from './chunk-UQ2NTFQC.js';
6
+ import { create } from 'zustand';
7
+ import { resolveDataProvider, emitDataChanged, deriveEntityKey, bulkCreate, matchesRange, getSupabaseClientOptional, getActiveTenantId, useTranslation, parseRangeFilter, isFilterObject } from '@fayz-ai/core';
8
+ import { profileHasSystemPermission } from '@fayz-ai/core/access';
9
+ import * as React from 'react';
10
+ import { WandSparkles } from 'lucide-react';
11
+ import { DropdownItem, Button, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalFooter } from '@fayz-ai/ui';
12
+ import { jsxs, jsx } from 'react/jsx-runtime';
13
+
14
+ var ASK_AI_HANDOFF_ID = "ask-ai.handoff";
15
+ var KEY_DISMISSED = "fayz.assistant.invite.dismissed";
16
+ function session() {
17
+ try {
18
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function dismissedIds() {
24
+ try {
25
+ return JSON.parse(session()?.getItem(KEY_DISMISSED) ?? "[]");
26
+ } catch {
27
+ return [];
28
+ }
29
+ }
30
+ function rememberDismissal(id) {
31
+ const store = session();
32
+ if (!store) return;
33
+ store.setItem(KEY_DISMISSED, JSON.stringify([.../* @__PURE__ */ new Set([...dismissedIds(), id])]));
34
+ }
35
+ var useAssistantInviteStore = create((set, get) => ({
36
+ invite: null,
37
+ stage: "dock",
38
+ dockBox: null,
39
+ leaving: null,
40
+ setInvite: (invite, id) => {
41
+ if (!invite) {
42
+ if (id && get().invite?.id !== id) return;
43
+ const { invite: going, dockBox } = get();
44
+ set({
45
+ invite: null,
46
+ stage: "dock",
47
+ dockBox: null,
48
+ leaving: going && dockBox ? { invite: going, box: dockBox } : null
49
+ });
50
+ return;
51
+ }
52
+ if (invite.role) loadArea(invite.role);
53
+ const hero = invite.auto !== false && !dismissedIds().includes(invite.id);
54
+ set({ invite, stage: hero ? "hero" : "dock", leaving: null });
55
+ },
56
+ setDockBox: (box) => {
57
+ const current = get().dockBox;
58
+ if (current?.left === box?.left && current?.width === box?.width) return;
59
+ set({ dockBox: box });
60
+ },
61
+ endLeave: () => set({ leaving: null }),
62
+ dismiss: () => {
63
+ const { invite } = get();
64
+ if (invite) rememberDismissal(invite.id);
65
+ set({ stage: "dock" });
66
+ }
67
+ }));
68
+
69
+ // src/lib/reveal-page.ts
70
+ function revealPage(delayMs = 1200) {
71
+ if (typeof window === "undefined" || window.matchMedia("(min-width: 768px)").matches) return;
72
+ const close = () => {
73
+ useChatStore.getState().setOpen(false);
74
+ if (useRightRailStore.getState().open) useRightRailStore.getState().close();
75
+ };
76
+ if (!useChatStore.getState().isStreaming) {
77
+ window.setTimeout(close, delayMs);
78
+ return;
79
+ }
80
+ const stop = useChatStore.subscribe((state, prev) => {
81
+ if (prev.isStreaming && !state.isStreaming) {
82
+ stop();
83
+ window.setTimeout(close, delayMs);
84
+ }
85
+ });
86
+ }
87
+
88
+ // src/lib/agent-surface.ts
89
+ var entries = [];
90
+ var version = 0;
91
+ var listeners = /* @__PURE__ */ new Set();
92
+ function emit() {
93
+ version++;
94
+ for (const l of listeners) l();
95
+ }
96
+ function registerSurface(entry) {
97
+ entries = [...entries.filter((e) => e.id !== entry.id), entry];
98
+ emit();
99
+ }
100
+ function unregisterSurface(id) {
101
+ const next = entries.filter((e) => e.id !== id);
102
+ if (next.length === entries.length) return;
103
+ entries = next;
104
+ emit();
105
+ }
106
+ function subscribeSurfaces(cb) {
107
+ listeners.add(cb);
108
+ return () => listeners.delete(cb);
109
+ }
110
+ function getSurfaceVersion() {
111
+ return version;
112
+ }
113
+ function getSurfaceTools() {
114
+ return [...entries].reverse().flatMap((e) => {
115
+ const c = e.get();
116
+ return c.tools?.map(({ tool }) => tool.role || !c.role ? tool : { ...tool, role: c.role }) ?? [];
117
+ });
118
+ }
119
+ function getActiveSurfaceContext() {
120
+ const snapshot = [];
121
+ const instructions = [];
122
+ for (const e of entries) {
123
+ const c = e.get();
124
+ if (c.describe) {
125
+ try {
126
+ snapshot.push(c.describe());
127
+ } catch {
128
+ }
129
+ }
130
+ if (c.instructions) instructions.push(c.instructions);
131
+ }
132
+ return { snapshot, instructions };
133
+ }
134
+ function requestChatConfirmation(action) {
135
+ return useChatStore.getState().requestConfirmation(action);
136
+ }
137
+ var SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
138
+ "id",
139
+ "tenant_id",
140
+ "tenantId",
141
+ "created_at",
142
+ "createdAt",
143
+ "updated_at",
144
+ "updatedAt",
145
+ "deleted_at",
146
+ "deletedAt",
147
+ "created_by",
148
+ "createdBy"
149
+ ]);
150
+ function normaliseOptions(raw) {
151
+ if (!Array.isArray(raw)) return void 0;
152
+ const out = [];
153
+ for (const o of raw) {
154
+ if (typeof o === "string") out.push({ value: o });
155
+ else if (o && typeof o === "object" && typeof o.value === "string") {
156
+ const rec = o;
157
+ out.push({ value: rec.value, label: typeof rec.label === "string" ? rec.label : void 0 });
158
+ }
159
+ }
160
+ return out.length ? out : void 0;
161
+ }
162
+ function cardFields(entity) {
163
+ return (entity.fields ?? []).filter((f) => f.showInForm !== false && !SYSTEM_FIELD_KEYS.has(String(f.key))).map((f) => {
164
+ const type = typeof f.type === "string" ? f.type : void 0;
165
+ return {
166
+ key: String(f.key),
167
+ label: typeof f.label === "string" ? f.label : void 0,
168
+ type,
169
+ required: f.required === true,
170
+ options: normaliseOptions(f.options),
171
+ placeholder: typeof f.placeholder === "string" ? f.placeholder : void 0,
172
+ hint: typeof f.hint === "string" ? f.hint : void 0,
173
+ min: typeof f.min === "number" ? f.min : void 0,
174
+ max: typeof f.max === "number" ? f.max : void 0,
175
+ default: f.defaultValue,
176
+ // A computed value has no input, and a relation needs its option list
177
+ // loaded from a table — neither is something to type into a chat card.
178
+ readOnly: type === "computed" || type === "relation" || !!f.relation
179
+ };
180
+ });
181
+ }
182
+ var handlers = /* @__PURE__ */ new Map();
183
+ function registerAIToolHandler(name, handler) {
184
+ handlers.set(name, handler);
185
+ }
186
+ function unregisterAIToolHandler(name) {
187
+ handlers.delete(name);
188
+ }
189
+ function hasAIToolHandler(name) {
190
+ return handlers.has(name);
191
+ }
192
+ var REGISTRY_PAGE_SIZE = 20;
193
+ var FILTERED_PAGE_SIZE = 200;
194
+ var FRESH = { cache: false };
195
+ function localDayBound(value, edge) {
196
+ const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value.trim());
197
+ if (!dateOnly) return value;
198
+ const time = edge === "start" ? "00:00:00.000" : "23:59:59.999";
199
+ const parsed = /* @__PURE__ */ new Date(`${value.trim()}T${time}`);
200
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString();
201
+ }
202
+ function dateRangeFilter(args) {
203
+ const from = typeof args.from === "string" && args.from.trim() ? args.from.trim() : void 0;
204
+ const to = typeof args.to === "string" && args.to.trim() ? args.to.trim() : void 0;
205
+ if (!from && !to) return null;
206
+ const field = typeof args.dateField === "string" && args.dateField.trim() ? args.dateField.trim() : "createdAt";
207
+ const range = {};
208
+ if (from) range.gte = localDayBound(from, "start");
209
+ if (to) range.lte = localDayBound(to, "end");
210
+ return { field, range };
211
+ }
212
+ var MAX_TOOLS_PER_TURN = 36;
213
+ function errorResult(error, message) {
214
+ return { ok: false, content: JSON.stringify({ error, message }) };
215
+ }
216
+ function attachRefs(entity, rows) {
217
+ const archetype = entity.data?.archetype;
218
+ if (!archetype) return rows;
219
+ const kind = entity.data?.archetypeKind;
220
+ const entityKey = deriveEntityKey(entity);
221
+ return rows.map(
222
+ (r) => r.id ? {
223
+ ...r,
224
+ ref: {
225
+ id: r.id,
226
+ resource: entity.data?.table,
227
+ archetype: kind ? `${archetype}:${kind}` : archetype,
228
+ entityKey
229
+ }
230
+ } : r
231
+ );
232
+ }
233
+ function rowContainsText(row, needle) {
234
+ const n = needle.toLowerCase();
235
+ return Object.values(row).some((v) => typeof v === "string" && v.toLowerCase().includes(n));
236
+ }
237
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
238
+ async function executeDataTool(target, args) {
239
+ const provider = resolveDataProvider(
240
+ target.entity,
241
+ target.mockData,
242
+ FRESH
243
+ );
244
+ const search = typeof args.search === "string" && args.search.trim() ? args.search.trim() : void 0;
245
+ if (search && UUID_RE.test(search)) {
246
+ try {
247
+ const byId = await provider.list({ filters: { id: search }, pageSize: 1 });
248
+ const row = byId.data[0];
249
+ if (row && String(row.id) === search) {
250
+ return {
251
+ entity: target.label,
252
+ search,
253
+ total: 1,
254
+ truncated: false,
255
+ records: attachRefs(target.entity, [row])
256
+ };
257
+ }
258
+ } catch {
259
+ }
260
+ }
261
+ const dateRange = dateRangeFilter(args);
262
+ const result = await provider.list({
263
+ search,
264
+ pageSize: dateRange ? FILTERED_PAGE_SIZE : REGISTRY_PAGE_SIZE,
265
+ ...dateRange ? { filters: { [dateRange.field]: dateRange.range } } : {},
266
+ ...typeof args.orderBy === "string" ? { sortBy: args.orderBy, sortDir: args.direction === "asc" ? "asc" : "desc" } : {}
267
+ });
268
+ let rows = result.data;
269
+ let total = result.total;
270
+ if (dateRange && rows.length) {
271
+ const key = resolveFieldKey(rows[0], dateRange.field);
272
+ if (!key) {
273
+ return {
274
+ error: "unknown_field",
275
+ message: `dateField "${dateRange.field}" does not exist on ${target.label}. Use one of the available fields and retry.`,
276
+ availableFields: Object.keys(rows[0])
277
+ };
278
+ }
279
+ const narrowed = rows.filter((r) => matchesRange(r[key], dateRange.range));
280
+ if (narrowed.length !== rows.length) total = narrowed.length;
281
+ rows = narrowed;
282
+ }
283
+ const unmatched = !!search && rows.length > 0 && !rows.some((r) => rowContainsText(r, search));
284
+ return {
285
+ entity: target.label,
286
+ ...search ? { search } : {},
287
+ ...dateRange ? { dateRange: { field: dateRange.field, ...dateRange.range } } : {},
288
+ total,
289
+ // Truthful cap: the model is told the list was trimmed so it can say so
290
+ // rather than presenting one page as the whole set.
291
+ truncated: total > rows.length,
292
+ records: attachRefs(target.entity, rows),
293
+ ...unmatched ? {
294
+ warning: "None of these rows contain the search text \u2014 they are RECENT records, not matches. Do NOT present them as the searched item; say it was not found or try another entity."
295
+ } : {}
296
+ };
297
+ }
298
+ function readOnlyRefusal(ctx, target, verb) {
299
+ const noun = target.entity.name.replace(/\s+/g, "").toLowerCase();
300
+ const plural = target.label.replace(/\s+/g, "").toLowerCase();
301
+ const wantedAction = verb === "created" ? "create" : "edit";
302
+ const candidates = (ctx.writeTools ?? []).filter(({ name, action }) => {
303
+ const n = name.toLowerCase();
304
+ if (n === "createrecord" || n === "updaterecord") return false;
305
+ if (!n.includes(noun) && !n.includes(plural)) return false;
306
+ if (action) return action === wantedAction;
307
+ return wantedAction === "create" ? /^(create|add|new)/.test(n) : /^(update|edit|set|change|move)/.test(n);
308
+ }).map((t) => t.name);
309
+ return {
310
+ error: "read_only",
311
+ message: `${target.label} is a read-only view \u2014 records cannot be ${verb} through this tool.` + (candidates.length ? ` Use ${candidates.join(" or ")} instead.` : ` This app has no tool that can change a ${target.entity.name.toLowerCase()}. Tell the user plainly that you cannot, and where in the app they can do it themselves.`) + " Do NOT substitute a create: making a new record instead of changing the existing one leaves the original in place and duplicates it.",
312
+ ...candidates.length ? { useInstead: candidates } : {}
313
+ };
314
+ }
315
+ function resolveFieldKey(row, name) {
316
+ if (name in row) return name;
317
+ const camel = name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
318
+ if (camel in row) return camel;
319
+ const snake = name.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
320
+ if (snake in row) return snake;
321
+ return null;
322
+ }
323
+ function resolveFilter(sample, label, name, value) {
324
+ const key = resolveFieldKey(sample, name);
325
+ if (!key) {
326
+ return {
327
+ error: "unknown_field",
328
+ message: `Filter field "${name}" does not exist on ${label}. Use one of the available fields and retry.`,
329
+ availableFields: Object.keys(sample)
330
+ };
331
+ }
332
+ const range = parseRangeFilter(value);
333
+ if (range) return { key, test: (row) => matchesRange(row[key], range) };
334
+ if (isFilterObject(value)) {
335
+ return {
336
+ error: "invalid_filter",
337
+ message: `Filter "${name}" is an object with no recognised bound. For a range use {"from":\u2026,"to":\u2026} (also accepted: gte/lte/gt/lt). For a date or period, prefer the from/to parameters of this tool instead of filters.`
338
+ };
339
+ }
340
+ return { key, test: (row) => String(row[key] ?? "") === String(value) };
341
+ }
342
+ async function executeAITool(name, args, context) {
343
+ const handler = handlers.get(name);
344
+ const dataTarget = context.dataTools.get(name);
345
+ if (!handler && !dataTarget) {
346
+ return errorResult(
347
+ "not_executable",
348
+ `The tool "${name}" is not available here. Do not retry it \u2014 tell the user what you cannot do, or use a different tool.`
349
+ );
350
+ }
351
+ try {
352
+ const result = handler ? await handler(args, context) : await executeDataTool(dataTarget, args);
353
+ return { ok: true, content: JSON.stringify(result ?? { ok: true }) };
354
+ } catch (error) {
355
+ return errorResult("execution_failed", describeThrown(error));
356
+ }
357
+ }
358
+ function describeThrown(error) {
359
+ if (error instanceof Error) return error.message;
360
+ if (error && typeof error === "object") {
361
+ const e = error;
362
+ const parts = [
363
+ typeof e.message === "string" ? e.message : null,
364
+ typeof e.hint === "string" ? e.hint : null,
365
+ typeof e.code === "string" ? `(${e.code})` : null
366
+ ].filter(Boolean);
367
+ if (parts.length) return parts.join(" ");
368
+ }
369
+ return typeof error === "string" && error ? error : "Unknown error";
370
+ }
371
+ function buildDataToolIndex(input) {
372
+ const index = /* @__PURE__ */ new Map();
373
+ for (const [pluginId, defs] of input.registries) {
374
+ for (const registry of defs) {
375
+ if (registry.readOnly) continue;
376
+ const target = {
377
+ label: registry.entity.namePlural ?? registry.entity.name,
378
+ entity: registry.entity,
379
+ mockData: registry.mockData
380
+ };
381
+ index.set(input.registryToolName(pluginId, registry.id), target);
382
+ index.set(`key:${pluginId}:${registry.id}`, target);
383
+ }
384
+ }
385
+ for (const entity of input.entities) {
386
+ if (!entity.entityDef || entity.entityDef.agentHidden) continue;
387
+ const target = {
388
+ label: entity.labelPlural,
389
+ entity: entity.entityDef,
390
+ mockData: entity.mockData
391
+ };
392
+ index.set(input.entityToolName(entity.entityKey), target);
393
+ index.set(`key:${entity.entityKey}`, target);
394
+ }
395
+ for (const q of [...input.queryEntities ?? [], ...CORE_QUERY_ENTITIES]) {
396
+ index.set(`key:${q.key}`, {
397
+ label: q.entity.namePlural ?? q.entity.name,
398
+ entity: q.entity,
399
+ readOnly: !q.writable
400
+ });
401
+ }
402
+ return index;
403
+ }
404
+ function openListWriteHint() {
405
+ const hasCreateHere = getSurfaceTools().some((t) => t.name === "createRecordHere");
406
+ return hasCreateHere ? " The list the user has open CAN create this one: call `createRecordHere` with `values`, using the field keys in the screen state." : "";
407
+ }
408
+ function resolveKeyedTarget(ctx, args) {
409
+ const key = typeof args.entity === "string" ? args.entity : "";
410
+ const target = ctx.dataTools.get(`key:${key}`);
411
+ if (!target) {
412
+ return {
413
+ error: {
414
+ ok: false,
415
+ error: "unknown_entity",
416
+ // Said as plainly as it can be said: the failure above this one was a
417
+ // reply that described a record nobody had written.
418
+ message: `NOTHING WAS WRITTEN. No entity "${key}" is available to these tools \u2014 do not tell the user it worked. Use one of the keys listed in the tool description.${openListWriteHint()}`
419
+ }
420
+ };
421
+ }
422
+ const permission = target.entity.permission;
423
+ if (permission) {
424
+ const access = checkAccess(permission.feature, permission.action);
425
+ if (!access.allowed) return { error: access };
426
+ }
427
+ return { target };
428
+ }
429
+ function sameField(a, b) {
430
+ const norm = (s) => s.replace(/[_-]/g, "").toLowerCase();
431
+ return norm(a) === norm(b);
432
+ }
433
+ function matchDeclaredFields(target, values) {
434
+ const declaredKeys = (target.entity.fields ?? []).map((f) => f.key);
435
+ const clean = {};
436
+ const unknown = [];
437
+ for (const [k, v] of Object.entries(values)) {
438
+ const match = declaredKeys.find((d) => sameField(d, k)) ?? null;
439
+ if (match) clean[match] = v;
440
+ else unknown.push(k);
441
+ }
442
+ if (!unknown.length) return { clean };
443
+ return {
444
+ error: {
445
+ error: "unknown_field",
446
+ message: `Unknown field(s) ${unknown.join(", ")} for ${target.label}. Use only the available fields and retry.`,
447
+ availableFields: declaredKeys
448
+ }
449
+ };
450
+ }
451
+ async function resolveAttachedRecords(ctx, matches) {
452
+ const ownerIds = /* @__PURE__ */ new Set();
453
+ for (const m of matches) {
454
+ const target = ctx.dataTools.get(`key:${m.key}`);
455
+ if (target?.entity.data?.archetype !== "person") continue;
456
+ for (const r of m.records) {
457
+ const id = r.id;
458
+ if (typeof id === "string") ownerIds.add(id);
459
+ }
460
+ }
461
+ if (!ownerIds.size) return null;
462
+ const out = {};
463
+ for (const q of CORE_QUERY_ENTITIES) {
464
+ const target = ctx.dataTools.get(`key:${q.key}`);
465
+ if (!target) continue;
466
+ const permission = target.entity.permission;
467
+ if (permission && !checkAccess(permission.feature, permission.action).allowed) continue;
468
+ try {
469
+ const provider = resolveDataProvider(target.entity, target.mockData);
470
+ const result = await provider.list({ pageSize: 50 });
471
+ const rows = result.data.filter((row) => {
472
+ const owner = row.ownerId ?? row.owner_id;
473
+ return typeof owner === "string" && ownerIds.has(owner);
474
+ });
475
+ if (rows.length) out[q.key] = rows;
476
+ } catch {
477
+ }
478
+ }
479
+ return Object.keys(out).length ? out : null;
480
+ }
481
+ registerAIToolHandler("findAnything", async (args, ctx) => {
482
+ const search = typeof args.search === "string" ? args.search.trim() : "";
483
+ if (!search) return { error: "missing_search", message: "Provide the name or text to look up." };
484
+ const seen = /* @__PURE__ */ new Set();
485
+ const candidates = [];
486
+ for (const [key, target] of ctx.dataTools) {
487
+ if (!key.startsWith("key:") || seen.has(target)) continue;
488
+ seen.add(target);
489
+ candidates.push({ key: key.slice(4), target });
490
+ }
491
+ let skippedForPermission = 0;
492
+ const allowed = candidates.filter(({ target }) => {
493
+ const permission = target.entity.permission;
494
+ if (!permission) return true;
495
+ if (checkAccess(permission.feature, permission.action).allowed) return true;
496
+ skippedForPermission += 1;
497
+ return false;
498
+ });
499
+ allowed.sort((a, b) => {
500
+ const ap = a.target.entity.data?.archetype === "person" ? 0 : 1;
501
+ const bp = b.target.entity.data?.archetype === "person" ? 0 : 1;
502
+ return ap - bp || a.key.localeCompare(b.key);
503
+ });
504
+ const MAX_TARGETS = 20;
505
+ const searched = allowed.slice(0, MAX_TARGETS);
506
+ const settled = await Promise.allSettled(
507
+ searched.map(async ({ key, target }) => {
508
+ const provider = resolveDataProvider(
509
+ target.entity,
510
+ target.mockData
511
+ );
512
+ const result = await provider.list({ search, pageSize: 5 });
513
+ return {
514
+ key,
515
+ label: target.label,
516
+ total: result.total,
517
+ records: attachRefs(target.entity, result.data)
518
+ };
519
+ })
520
+ );
521
+ const matches = settled.filter((r) => r.status === "fulfilled" && r.value.records.length > 0).map((r) => r.value);
522
+ const attached = await resolveAttachedRecords(ctx, matches);
523
+ return {
524
+ query: search,
525
+ matches,
526
+ ...attached ? { attached } : {},
527
+ searchedEntities: searched.length,
528
+ ...allowed.length > MAX_TARGETS ? { targetsTruncated: true } : {},
529
+ ...skippedForPermission ? { skippedForPermission } : {},
530
+ ...matches.length === 0 ? { hint: "No records matched anywhere the user can see. Say so \u2014 do not retry other search tools with the same text." } : {}
531
+ };
532
+ });
533
+ registerAIToolHandler("createRecord", async (args, ctx) => {
534
+ const resolved = resolveKeyedTarget(ctx, args);
535
+ if ("error" in resolved) return resolved.error;
536
+ const { target } = resolved;
537
+ if (target.readOnly) return readOnlyRefusal(ctx, target, "created");
538
+ const permission = target.entity.permission;
539
+ if (permission) {
540
+ const access = checkAccess(permission.feature, "create");
541
+ if (!access.allowed) return access;
542
+ }
543
+ if (target.entity.limitKey) {
544
+ const limit = await guardLimit(target.entity.limitKey);
545
+ if (!limit.allowed) return limit;
546
+ }
547
+ const values = args.values ?? {};
548
+ const declared = target.entity.fields ?? [];
549
+ const matched = matchDeclaredFields(target, values);
550
+ if ("error" in matched) return matched.error;
551
+ const { clean } = matched;
552
+ const isEmpty = (v) => v === void 0 || v === null || v === "";
553
+ const missingRequired = declared.filter((f) => f.required && isEmpty(clean[f.key]));
554
+ const mustResolveFirst = missingRequired.filter(
555
+ (f) => f.type === "computed" || f.type === "relation" || !!f.relation || f.showInForm === false
556
+ );
557
+ if (mustResolveFirst.length) {
558
+ return {
559
+ error: "missing_required",
560
+ message: `Missing required field(s) that must be resolved to a record first: ${mustResolveFirst.map((f) => f.key).join(", ")}. Look them up (search for the referenced record), then retry.`,
561
+ availableFields: declared.map((f) => f.key)
562
+ };
563
+ }
564
+ const dupName = typeof clean.name === "string" ? clean.name.trim() : "";
565
+ if (dupName && args.allowDuplicate !== true) {
566
+ const provider0 = resolveDataProvider(
567
+ target.entity,
568
+ target.mockData
569
+ );
570
+ const existing = await provider0.list({ search: dupName, pageSize: 3 });
571
+ const dupes = existing.data.filter(
572
+ (r) => String(r.name ?? "").trim().toLowerCase() === dupName.toLowerCase()
573
+ );
574
+ if (dupes.length) {
575
+ return {
576
+ error: "possible_duplicate",
577
+ message: `A ${target.entity.name} named "${dupName}" already exists. If the user wants to CHANGE that existing record, use updateRecord with its id; only retry createRecord with allowDuplicate:true if they explicitly want a second one.`,
578
+ matches: dupes
579
+ };
580
+ }
581
+ }
582
+ const decision = await requestChatConfirmation({
583
+ id: `create-${Date.now()}`,
584
+ toolName: "createRecord",
585
+ title: `Criar ${target.entity.name}`,
586
+ params: clean,
587
+ plane: "client",
588
+ // Scoped grant: "always create clients", never "always create anything".
589
+ scope: typeof args.entity === "string" ? args.entity : void 0,
590
+ scopeLabel: `Criar ${target.entity.name}`,
591
+ fields: cardFields(target.entity)
592
+ });
593
+ const approved = decision.approved;
594
+ if (decision.values) {
595
+ for (const key of Object.keys(clean)) delete clean[key];
596
+ Object.assign(clean, decision.values);
597
+ }
598
+ if (!approved) return { ok: false, cancelled: true, reason: "user_declined" };
599
+ const stillMissing = declared.filter((f) => f.required && isEmpty(clean[f.key])).map((f) => f.key);
600
+ if (stillMissing.length) {
601
+ return {
602
+ error: "missing_required",
603
+ message: `Still missing required field(s): ${stillMissing.join(", ")}. Ask the user for them, then retry.`,
604
+ availableFields: declared.map((f) => f.key)
605
+ };
606
+ }
607
+ const provider = resolveDataProvider(
608
+ target.entity,
609
+ target.mockData
610
+ );
611
+ const record = await provider.create(clean);
612
+ if (target.entity.limitKey) invalidateLimit(target.entity.limitKey);
613
+ emitDataChanged({
614
+ table: target.entity.data?.table,
615
+ entityKey: typeof args.entity === "string" ? args.entity : void 0,
616
+ archetype: target.entity.data?.archetype,
617
+ op: "create",
618
+ id: record.id,
619
+ source: "agent"
620
+ });
621
+ return {
622
+ ok: true,
623
+ record,
624
+ ref: {
625
+ id: record.id,
626
+ label: String(record.name ?? target.entity.name),
627
+ resource: target.entity.data?.table,
628
+ archetype: target.entity.data?.archetype ? `${target.entity.data.archetype}${target.entity.data.archetypeKind ? ":" + target.entity.data.archetypeKind : ""}` : void 0,
629
+ entityKey: deriveEntityKey(target.entity)
630
+ }
631
+ };
632
+ });
633
+ registerAIToolHandler("updateRecord", async (args, ctx) => {
634
+ const resolved = resolveKeyedTarget(ctx, args);
635
+ if ("error" in resolved) return resolved.error;
636
+ const { target } = resolved;
637
+ if (target.readOnly) return readOnlyRefusal(ctx, target, "updated");
638
+ const id = typeof args.id === "string" ? args.id : "";
639
+ if (!id) return { error: "missing_id", message: "Provide the id of the record to update (from this conversation or a search)." };
640
+ const permission = target.entity.permission;
641
+ if (permission) {
642
+ const access = checkAccess(permission.feature, "edit");
643
+ if (!access.allowed) return access;
644
+ }
645
+ const values = args.values ?? {};
646
+ const matched = matchDeclaredFields(target, values);
647
+ if ("error" in matched) return matched.error;
648
+ const { clean } = matched;
649
+ if (!Object.keys(clean).length) {
650
+ return { error: "empty_update", message: "No valid fields to change were provided." };
651
+ }
652
+ const decision = await requestChatConfirmation({
653
+ id: `update-${Date.now()}`,
654
+ toolName: "updateRecord",
655
+ title: `Atualizar ${target.entity.name}`,
656
+ params: { id, ...clean },
657
+ plane: "client",
658
+ scope: typeof args.entity === "string" ? args.entity : void 0,
659
+ scopeLabel: `Atualizar ${target.entity.name}`,
660
+ fields: cardFields(target.entity),
661
+ partial: true
662
+ });
663
+ if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
664
+ if (decision.values) {
665
+ for (const key of Object.keys(clean)) delete clean[key];
666
+ for (const [k, v] of Object.entries(decision.values)) if (k !== "id") clean[k] = v;
667
+ }
668
+ const provider = resolveDataProvider(
669
+ target.entity,
670
+ target.mockData
671
+ );
672
+ const record = await provider.update(id, clean);
673
+ emitDataChanged({
674
+ table: target.entity.data?.table,
675
+ entityKey: typeof args.entity === "string" ? args.entity : void 0,
676
+ archetype: target.entity.data?.archetype,
677
+ op: "update",
678
+ id,
679
+ source: "agent"
680
+ });
681
+ return {
682
+ ok: true,
683
+ record,
684
+ ref: {
685
+ id,
686
+ label: String(record.name ?? target.entity.name),
687
+ resource: target.entity.data?.table,
688
+ archetype: target.entity.data?.archetype ? `${target.entity.data.archetype}${target.entity.data.archetypeKind ? ":" + target.entity.data.archetypeKind : ""}` : void 0,
689
+ entityKey: deriveEntityKey(target.entity)
690
+ }
691
+ };
692
+ });
693
+ registerAIToolHandler("searchRecords", async (args, ctx) => {
694
+ const resolved = resolveKeyedTarget(ctx, args);
695
+ if ("error" in resolved) return resolved.error;
696
+ const base = await executeDataTool(resolved.target, args);
697
+ if ("error" in base) return base;
698
+ const filters = args.filters ?? {};
699
+ const keys = Object.keys(filters);
700
+ if (!keys.length) return base;
701
+ let rows = base.records;
702
+ for (const k of keys) {
703
+ const sample = rows[0] ?? base.records[0];
704
+ if (!sample) break;
705
+ const f = resolveFilter(sample, resolved.target.label, k, filters[k]);
706
+ if ("error" in f) return f;
707
+ rows = rows.filter(f.test);
708
+ }
709
+ return {
710
+ ...base,
711
+ records: rows,
712
+ filtered: keys,
713
+ total: rows.length,
714
+ // A filter that empties a non-empty page is ambiguous — "no record matches"
715
+ // and "the filter was wrong" look identical to the model, and it picks the
716
+ // first. Name the difference so it can retry instead of asserting absence.
717
+ ...rows.length === 0 && base.records.length > 0 ? {
718
+ warning: `${base.records.length} record(s) were read but the filters ${JSON.stringify(filters)} excluded all of them. Check the filter values against the fields on those records before telling the user nothing exists.`
719
+ } : {}
720
+ };
721
+ });
722
+ registerAIToolHandler("proposeImport", async (args, ctx) => {
723
+ const resolved = resolveKeyedTarget(ctx, args);
724
+ if ("error" in resolved) return resolved.error;
725
+ const { target } = resolved;
726
+ const fileName = typeof args.file === "string" ? args.file : void 0;
727
+ const digest = resolveAttachmentDigest(fileName);
728
+ if (!digest) {
729
+ const names = listAttachmentNames();
730
+ const other = nonTabularAttachmentNames();
731
+ return {
732
+ error: "no_file",
733
+ message: names.length ? `No readable file called "${fileName ?? ""}". Attached right now: ${names.join(", ")}.` : other.length ? `${other.join(", ")} is attached, but it has no columns \u2014 importing is for spreadsheets and CSVs. If it is a photo or a scan, READ IT: it is in front of you, so answer from what is written on it and use the tool that matches what the user asked for. Do not ask for a CSV.` : "There is no file attached to this conversation. Ask the user to attach a CSV or a spreadsheet."
734
+ };
735
+ }
736
+ const headers = (digest.headers ?? []).filter((h) => h.trim());
737
+ const fields = (target.entity.fields ?? []).filter(
738
+ (f) => f.showInForm !== false && !SYSTEM_FIELD_KEYS.has(String(f.key))
739
+ );
740
+ const fieldKeys = new Set(fields.map((f) => String(f.key)));
741
+ const proposed = args.mapping ?? {};
742
+ const accepted = {};
743
+ const rejected = [];
744
+ for (const [column, field] of Object.entries(proposed)) {
745
+ if (!headers.includes(column)) {
746
+ rejected.push(`column "${column}" is not in the file`);
747
+ continue;
748
+ }
749
+ if (typeof field !== "string" || !fieldKeys.has(field)) {
750
+ rejected.push(`"${column}" \u2192 "${String(field)}" is not a field of ${target.label}`);
751
+ continue;
752
+ }
753
+ accepted[column] = field;
754
+ }
755
+ const options = [
756
+ ...fields.map((f) => ({ value: String(f.key), label: f.label ?? String(f.key) }))
757
+ ];
758
+ const decision = await requestChatConfirmation({
759
+ id: `import-${digest.name}`,
760
+ toolName: "proposeImport",
761
+ title: `${digest.name} \u2192 ${target.label}`,
762
+ params: accepted,
763
+ plane: "client",
764
+ scope: typeof args.entity === "string" ? args.entity : void 0,
765
+ scopeLabel: `Importar para ${target.label}`,
766
+ // One row per column of the FILE, so a column the model skipped is visibly
767
+ // unmapped instead of silently absent.
768
+ fields: headers.map((header) => ({
769
+ key: header,
770
+ label: header,
771
+ options,
772
+ hint: digest.columnTypes?.find((c) => c.name === header)?.note
773
+ })),
774
+ partial: true
775
+ });
776
+ if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
777
+ const mapping = decision.values ?? accepted;
778
+ const mapped = Object.keys(mapping).length;
779
+ return {
780
+ ok: true,
781
+ file: digest.name,
782
+ entity: target.label,
783
+ rows: digest.rowCount,
784
+ mapping,
785
+ unmapped: headers.filter((h) => !mapping[h]),
786
+ ...rejected.length ? { ignoredFromYourProposal: rejected } : {},
787
+ nextStep: "commitImport",
788
+ message: `The user confirmed the mapping: ${mapped} of ${headers.length} columns are mapped for ${digest.rowCount ?? "?"} rows. NOW CALL commitImport with this exact mapping to write the rows \u2014 that is the next and final step, and the user is waiting for it. Do not announce it, do not ask again, and do not repeat the mapping back line by line.`
789
+ };
790
+ });
791
+ registerAIToolHandler("commitImport", async (args, ctx) => {
792
+ const resolved = resolveKeyedTarget(ctx, args);
793
+ if ("error" in resolved) return resolved.error;
794
+ const { target } = resolved;
795
+ const digest = resolveAttachmentDigest(typeof args.file === "string" ? args.file : void 0);
796
+ if (!digest) {
797
+ return {
798
+ error: "no_file",
799
+ message: "The file is no longer in this conversation. Ask the user to attach it again."
800
+ };
801
+ }
802
+ const mapping = args.mapping ?? {};
803
+ const columns = Object.keys(mapping).filter((c) => typeof mapping[c] === "string" && mapping[c]);
804
+ if (!columns.length) {
805
+ return {
806
+ error: "empty_mapping",
807
+ message: "No column is mapped. Run proposeImport first and pass the mapping it returns."
808
+ };
809
+ }
810
+ const permission = target.entity.permission;
811
+ if (permission) {
812
+ const access = checkAccess(permission.feature, "create");
813
+ if (!access.allowed) return access;
814
+ }
815
+ let rows;
816
+ try {
817
+ rows = await readAllRows(digest);
818
+ } catch (error) {
819
+ return { error: "unreadable", message: error instanceof Error ? error.message : "Could not read the file." };
820
+ }
821
+ const records = rows.map((row) => {
822
+ const record = {};
823
+ for (const column of columns) {
824
+ const value = row[column];
825
+ if (value !== void 0 && value !== "") record[mapping[column]] = value;
826
+ }
827
+ return record;
828
+ }).filter((r) => Object.keys(r).length);
829
+ if (!records.length) {
830
+ return { error: "no_rows", message: "Every row is empty under this mapping. Check the column names." };
831
+ }
832
+ if (target.entity.limitKey) {
833
+ const limit = await guardLimit(target.entity.limitKey, records.length);
834
+ if (!limit.allowed) return limit;
835
+ }
836
+ const decision = await requestChatConfirmation({
837
+ id: `commit-${digest.name}`,
838
+ toolName: "commitImport",
839
+ title: `Importar ${records.length} \xD7 ${target.entity.name}`,
840
+ params: {
841
+ arquivo: digest.name,
842
+ registros: records.length,
843
+ campos: columns.map((c) => `${c} \u2192 ${mapping[c]}`).join(", ")
844
+ },
845
+ plane: "client",
846
+ scope: typeof args.entity === "string" ? args.entity : void 0,
847
+ scopeLabel: `Importar ${target.label}`
848
+ });
849
+ if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
850
+ const batchId = `imp-${Date.now().toString(36)}`;
851
+ const canStamp = (target.entity.fields ?? []).some((f) => String(f.key) === "importBatchId");
852
+ const provider = resolveDataProvider(target.entity, target.mockData, FRESH);
853
+ const result = await bulkCreate(
854
+ provider,
855
+ canStamp ? records.map((r) => ({ ...r, importBatchId: batchId })) : records,
856
+ { chunkSize: 200 }
857
+ );
858
+ emitDataChanged({
859
+ table: target.entity.data?.table,
860
+ entityKey: typeof args.entity === "string" ? args.entity : void 0,
861
+ archetype: target.entity.data?.archetype,
862
+ op: "create",
863
+ source: "agent"
864
+ });
865
+ if (target.entity.limitKey) invalidateLimit(target.entity.limitKey);
866
+ return {
867
+ ok: result.created > 0,
868
+ ...canStamp ? { batchId } : {},
869
+ created: result.created,
870
+ failed: result.failed.length,
871
+ // Row numbers, not row contents — the model never needs the data, and a
872
+ // failure list of 200 rows would swamp the turn.
873
+ firstErrors: result.failed.slice(0, 5).map((f) => ({ row: f.index + 2, reason: f.message })),
874
+ message: `${result.created} of ${records.length} rows were created` + (result.failed.length ? `. ${result.failed.length} failed \u2014 tell the user which rows and why, and offer to fix and retry those.` : ". Tell the user it is done.")
875
+ };
876
+ });
877
+ registerAIToolHandler("queryData", async (args, ctx) => {
878
+ const resolved = resolveKeyedTarget(ctx, args);
879
+ if ("error" in resolved) return resolved.error;
880
+ const { target } = resolved;
881
+ const metric = args.metric === "sum" || args.metric === "avg" ? args.metric : "count";
882
+ let field = typeof args.field === "string" ? args.field : void 0;
883
+ if (metric !== "count" && !field) {
884
+ return { error: "missing_field", message: `metric "${metric}" needs a "field" to aggregate.` };
885
+ }
886
+ const provider = resolveDataProvider(
887
+ target.entity,
888
+ target.mockData,
889
+ FRESH
890
+ );
891
+ const dateRange = dateRangeFilter(args);
892
+ const page = await provider.list({
893
+ pageSize: 1e3,
894
+ ...dateRange ? { filters: { [dateRange.field]: dateRange.range } } : {}
895
+ });
896
+ let rows = page.data;
897
+ const sampleRow = rows[0];
898
+ const availableFields = sampleRow ? Object.keys(sampleRow) : [];
899
+ const requireField = (name) => {
900
+ if (!sampleRow) return name;
901
+ const resolved2 = resolveFieldKey(sampleRow, name);
902
+ if (resolved2) return resolved2;
903
+ return {
904
+ error: "unknown_field",
905
+ message: `Field "${name}" does not exist on ${target.label}. Use one of the available fields and retry.`,
906
+ availableFields
907
+ };
908
+ };
909
+ if (dateRange) {
910
+ const resolved2 = requireField(dateRange.field);
911
+ if (typeof resolved2 !== "string") return resolved2;
912
+ rows = rows.filter((r) => matchesRange(r[resolved2], dateRange.range));
913
+ }
914
+ const filters = args.filters ?? {};
915
+ for (const [k, v] of Object.entries(filters)) {
916
+ if (!sampleRow) break;
917
+ const f = resolveFilter(sampleRow, target.label, k, v);
918
+ if ("error" in f) return f;
919
+ rows = rows.filter(f.test);
920
+ }
921
+ const aggregate = (subset) => {
922
+ if (metric === "count") return subset.length;
923
+ const values = subset.map((r) => Number(r[field])).filter((n) => !Number.isNaN(n));
924
+ const sum = values.reduce((a, b) => a + b, 0);
925
+ if (metric === "sum") return Math.round(sum * 100) / 100;
926
+ return values.length ? Math.round(sum / values.length * 100) / 100 : 0;
927
+ };
928
+ if (field && rows[0]) {
929
+ const resolved2 = resolveFieldKey(rows[0], field);
930
+ if (!resolved2) {
931
+ return {
932
+ error: "unknown_field",
933
+ message: `Field "${field}" does not exist on ${target.label}. Use one of the available fields and retry.`,
934
+ availableFields
935
+ };
936
+ }
937
+ field = resolved2;
938
+ }
939
+ let groupBy = typeof args.groupBy === "string" ? args.groupBy : void 0;
940
+ if (groupBy && rows[0]) {
941
+ const resolved2 = resolveFieldKey(rows[0], groupBy);
942
+ if (!resolved2) {
943
+ return {
944
+ error: "unknown_field",
945
+ message: `Field "${groupBy}" does not exist on ${target.label}. Use one of the available fields and retry.`,
946
+ availableFields
947
+ };
948
+ }
949
+ groupBy = resolved2;
950
+ }
951
+ const sampled = page.total > page.data.length;
952
+ if (groupBy) {
953
+ const groups = /* @__PURE__ */ new Map();
954
+ for (const r of rows) {
955
+ const g = String(r[groupBy] ?? "\u2014");
956
+ if (!groups.has(g)) groups.set(g, []);
957
+ groups.get(g).push(r);
958
+ }
959
+ const labelKey = groupBy.endsWith("Id") ? resolveFieldKey(rows[0] ?? {}, groupBy.slice(0, -2) + "Name") : null;
960
+ return {
961
+ entity: target.label,
962
+ metric,
963
+ ...field ? { field } : {},
964
+ groupBy,
965
+ groups: Array.from(groups.entries()).map(([group, subset]) => ({
966
+ group,
967
+ ...labelKey && subset[0] ? { label: subset[0][labelKey] } : {},
968
+ value: aggregate(subset),
969
+ rows: subset.length
970
+ })).sort((a, b) => Number(b.value) - Number(a.value)),
971
+ matchedRows: rows.length,
972
+ sampled
973
+ };
974
+ }
975
+ return {
976
+ entity: target.label,
977
+ metric,
978
+ ...field ? { field } : {},
979
+ value: aggregate(rows),
980
+ matchedRows: rows.length,
981
+ sampled
982
+ };
983
+ });
984
+ function buildAgentToolset(tools, { dataTools, activePluginId, pinned, loaded, maxTools = MAX_TOOLS_PER_TURN }) {
985
+ const executable = tools.filter(
986
+ (t) => handlers.has(t.name) || dataTools.has(t.name) || t.execution?.plane === "server" && t.execution.kind === "rpc"
987
+ );
988
+ const ofPlugin = (t) => !!activePluginId && t.id.startsWith(`${activePluginId}.`);
989
+ const CORE_PINNED = /* @__PURE__ */ new Set([...ALWAYS_ON_TOOLS, "findTools"]);
990
+ const pinnedBand = executable.filter((t) => pinned?.has(t.name)).sort((a, b) => (CORE_PINNED.has(a.name) ? 0 : 1) - (CORE_PINNED.has(b.name) ? 0 : 1));
991
+ const ranked = pinned ? [
992
+ ...pinnedBand,
993
+ ...executable.filter((t) => !pinned.has(t.name) && loaded?.has(t.name)),
994
+ ...executable.filter((t) => !pinned.has(t.name) && !loaded?.has(t.name) && ofPlugin(t))
995
+ ] : activePluginId ? [...executable.filter(ofPlugin), ...executable.filter((t) => !ofPlugin(t))] : executable;
996
+ return ranked.slice(0, maxTools).map((tool) => ({
997
+ name: tool.name,
998
+ description: tool.description,
999
+ parameters: tool.parameters
1000
+ }));
1001
+ }
1002
+ async function resolveTeamRows(ctx) {
1003
+ if (ctx.loadTeam) {
1004
+ const team = await ctx.loadTeam();
1005
+ return team.map((p) => ({
1006
+ name: p.name ?? null,
1007
+ kind: p.kind,
1008
+ email: p.email ?? null,
1009
+ accessRole: p.membership?.profileName ?? null,
1010
+ hasLogin: !!p.membership
1011
+ }));
1012
+ }
1013
+ return ctx.members.map((m) => ({
1014
+ name: m.user?.fullName ?? null,
1015
+ email: m.user?.email ?? null,
1016
+ accessRole: m.profileName ?? null,
1017
+ hasLogin: true
1018
+ }));
1019
+ }
1020
+ registerAIToolHandler("getBusinessSummary", async (_args, ctx) => {
1021
+ if (!ctx.currentOrg) return { error: "no_active_business" };
1022
+ const org = ctx.currentOrg;
1023
+ return {
1024
+ name: org.name,
1025
+ slug: org.slug,
1026
+ plan: org.plan ?? null,
1027
+ vertical: org.verticalId ?? null,
1028
+ teamSize: (await resolveTeamRows(ctx)).length,
1029
+ createdAt: org.createdAt ?? null
1030
+ };
1031
+ });
1032
+ registerAIToolHandler("getTeamMembers", async (args, ctx) => {
1033
+ const filter = typeof args.role === "string" ? args.role.trim().toLowerCase() : void 0;
1034
+ const rows = await resolveTeamRows(ctx);
1035
+ const members = filter ? rows.filter(
1036
+ (r) => String(r.kind ?? "").toLowerCase() === filter || String(r.accessRole ?? "").toLowerCase() === filter
1037
+ ) : rows;
1038
+ return { total: members.length, members };
1039
+ });
1040
+ registerAIToolHandler("navigateTo", (args, ctx) => {
1041
+ const query = typeof args.page === "string" ? args.page.trim().toLowerCase() : "";
1042
+ if (!query) return { error: "missing_page" };
1043
+ const match = ctx.routes.find((r) => r.path.toLowerCase() === query || r.path.toLowerCase() === `/${query}`) ?? ctx.routes.find((r) => (r.label ?? "").toLowerCase() === query) ?? ctx.routes.find((r) => {
1044
+ const label = (r.label ?? "").toLowerCase();
1045
+ return !!label && (label.startsWith(query) || query.startsWith(label));
1046
+ });
1047
+ if (!match) {
1048
+ return {
1049
+ error: "page_not_found",
1050
+ message: "No page matches that. If the user meant a specific record, look it up and call openRecord with its id.",
1051
+ availablePages: ctx.routes.map((r) => r.label ?? r.path)
1052
+ };
1053
+ }
1054
+ ctx.navigate(match.path);
1055
+ revealPage();
1056
+ return { navigatedTo: match.path, label: match.label ?? match.path };
1057
+ });
1058
+ registerAIToolHandler("openRecord", async (args, ctx) => {
1059
+ const id = typeof args.id === "string" ? args.id.trim() : "";
1060
+ if (!id) return { error: "missing_id", message: "Provide the record id returned by an earlier tool." };
1061
+ if (ctx.currentPath.includes(id)) {
1062
+ return {
1063
+ alreadyOpen: true,
1064
+ message: "This record is already the page the user is looking at \u2014 nothing to navigate. To talk about it, read it with searchRecords filtered by this id."
1065
+ };
1066
+ }
1067
+ const ref = lookupRecordRef(id);
1068
+ if (!ref) {
1069
+ return {
1070
+ error: "unknown_record",
1071
+ message: "That id was not seen in this conversation. Search for the record first, then call openRecord with the id from the result."
1072
+ };
1073
+ }
1074
+ if (!resolveEntityRoute(ref.archetype, ref.kind, ref.entityKey)) {
1075
+ return { error: "no_page_for_record", message: `This app has no page for ${ref.label}.` };
1076
+ }
1077
+ openEntity(id, ref.archetype, ref.kind, void 0, ref.entityKey);
1078
+ revealPage();
1079
+ return { opened: ref.label, ref: { id, label: ref.label, archetype: ref.archetype } };
1080
+ });
1081
+ function validateGrants(features, raw) {
1082
+ const known = new Set(features.map((f) => f.id));
1083
+ const grants = {};
1084
+ const rejected = [];
1085
+ const isAction = (a) => PROFILE_ACTIONS.includes(a);
1086
+ for (const [featureId, value] of Object.entries(raw ?? {})) {
1087
+ if (!known.has(featureId)) {
1088
+ rejected.push(`${featureId}: not an area of this app`);
1089
+ continue;
1090
+ }
1091
+ const asked = (Array.isArray(value) ? value : [value]).map((a) => String(a).trim().toLowerCase());
1092
+ const unknown = asked.filter((a) => !isAction(a));
1093
+ if (unknown.length) rejected.push(`${featureId}: unknown action(s) ${unknown.join(", ")}`);
1094
+ const actions = asked.filter(isAction);
1095
+ if (actions.length) grants[featureId] = actions;
1096
+ }
1097
+ return { grants, rejected };
1098
+ }
1099
+ registerAIToolHandler("createPermissionProfile", async (args, ctx) => {
1100
+ const profile = usePermissionsStore.getState().currentProfile;
1101
+ if (!profileHasSystemPermission(profile, "manage_permissions")) {
1102
+ return {
1103
+ error: "forbidden",
1104
+ message: "This user cannot manage permissions. Say so \u2014 do not try another tool."
1105
+ };
1106
+ }
1107
+ if (!ctx.orgAdapter?.createProfile || !ctx.currentOrg) {
1108
+ return {
1109
+ error: "not_executable",
1110
+ message: "This app has no permission-profile store wired. Tell the user to create the profile in Settings \u2192 Permissions."
1111
+ };
1112
+ }
1113
+ const name = typeof args.name === "string" ? args.name.trim() : "";
1114
+ if (!name) return { error: "missing_name", message: "Give the profile a name." };
1115
+ const features = usePermissionsStore.getState().features;
1116
+ const byId = new Map(features.map((f) => [f.id, f]));
1117
+ const { grants, rejected } = validateGrants(features, args.grants);
1118
+ if (!Object.keys(grants).length) {
1119
+ return {
1120
+ error: "empty_grants",
1121
+ message: `No valid area was granted. Use the ids from the tool's area list and the actions ${PROFILE_ACTIONS.join("/")}.`,
1122
+ ...rejected.length ? { rejected } : {},
1123
+ availableAreas: features.map((f) => f.id)
1124
+ };
1125
+ }
1126
+ const existing = usePermissionsStore.getState().profiles;
1127
+ const clash = existing.find((p) => p.name.trim().toLowerCase() === name.toLowerCase());
1128
+ if (clash) {
1129
+ return {
1130
+ error: "name_taken",
1131
+ message: `A profile named "${clash.name}" already exists. Ask the user for a different name, or whether they meant to change that one (which happens in Settings \u2192 Permissions).`
1132
+ };
1133
+ }
1134
+ const summary = describeGrants(grants, byId);
1135
+ const decision = await requestChatConfirmation({
1136
+ id: `permission-profile-${Date.now()}`,
1137
+ toolName: "createPermissionProfile",
1138
+ title: `Criar perfil de acesso: ${name}`,
1139
+ params: {
1140
+ nome: name,
1141
+ ...typeof args.description === "string" && args.description.trim() ? { descri\u00E7\u00E3o: args.description.trim() } : {},
1142
+ acessos: summary
1143
+ },
1144
+ plane: "client",
1145
+ noAutoApproval: true
1146
+ });
1147
+ if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
1148
+ const finalName = typeof decision.values?.nome === "string" && decision.values.nome.trim() ? decision.values.nome.trim() : name;
1149
+ const finalDescription = typeof decision.values?.["descri\xE7\xE3o"] === "string" ? decision.values["descri\xE7\xE3o"].trim() || void 0 : typeof args.description === "string" && args.description.trim() ? args.description.trim() : void 0;
1150
+ let created;
1151
+ try {
1152
+ created = await ctx.orgAdapter.createProfile(ctx.currentOrg.id, {
1153
+ name: finalName,
1154
+ description: finalDescription,
1155
+ grants
1156
+ });
1157
+ } catch (error) {
1158
+ return {
1159
+ error: "create_failed",
1160
+ message: `The profile could not be saved: ${describeThrown(error)}. Report this to the user \u2014 do not retry.`
1161
+ };
1162
+ }
1163
+ if (ctx.orgAdapter.listProfiles) {
1164
+ try {
1165
+ usePermissionsStore.getState().setProfiles(await ctx.orgAdapter.listProfiles(ctx.currentOrg.id));
1166
+ } catch {
1167
+ }
1168
+ }
1169
+ return {
1170
+ ok: true,
1171
+ profile: { id: created.id, name: created.name },
1172
+ grants: summary,
1173
+ ...rejected.length ? { rejectedFromYourProposal: rejected } : {},
1174
+ message: "Profile created. Summarise for the user WHAT IT CAN REACH using the labels in `grants`, then say the profile is assigned to people on the Team screen. The link to review it is already in the reply.",
1175
+ ref: { id: created.id, label: created.name, path: PERMISSIONS_PATH }
1176
+ };
1177
+ });
1178
+ registerAIToolHandler("updatePermissionProfile", async (args, ctx) => {
1179
+ const actor = usePermissionsStore.getState().currentProfile;
1180
+ if (!profileHasSystemPermission(actor, "manage_permissions")) {
1181
+ return {
1182
+ error: "forbidden",
1183
+ message: "This user cannot manage permissions. Say so \u2014 do not try another tool."
1184
+ };
1185
+ }
1186
+ if (!ctx.orgAdapter?.updateProfile || !ctx.currentOrg) {
1187
+ return {
1188
+ error: "not_executable",
1189
+ message: "This app cannot change profiles from here. Tell the user to edit it in Settings \u2192 Permissions."
1190
+ };
1191
+ }
1192
+ const id = typeof args.id === "string" ? args.id.trim() : "";
1193
+ const { features, profiles } = usePermissionsStore.getState();
1194
+ const target = profiles.find((p) => p.id === id);
1195
+ if (!target) {
1196
+ return {
1197
+ error: "unknown_profile",
1198
+ message: `No profile with id "${id}". Existing: ${profiles.map((p) => `${p.id} (${p.name})`).join(", ")}.`
1199
+ };
1200
+ }
1201
+ if (target.isSystem) {
1202
+ return {
1203
+ error: "system_profile",
1204
+ message: `"${target.name}" is a built-in role and cannot be changed. Offer to create a new profile based on it instead (createPermissionProfile).`
1205
+ };
1206
+ }
1207
+ const byId = new Map(features.map((f) => [f.id, f]));
1208
+ const { grants: setAreas, rejected } = validateGrants(features, args.grants);
1209
+ const revoke = (Array.isArray(args.revoke) ? args.revoke : []).map((a) => String(a).trim()).filter(Boolean);
1210
+ const before = target.grants ?? {};
1211
+ const next = { ...before, ...setAreas };
1212
+ for (const area of revoke) {
1213
+ if (!(area in next)) rejected.push(`${area}: was not granted, nothing to revoke`);
1214
+ delete next[area];
1215
+ }
1216
+ const rename = typeof args.name === "string" && args.name.trim() && args.name.trim() !== target.name ? args.name.trim() : void 0;
1217
+ const redescribe = typeof args.description === "string" && args.description.trim() !== (target.description ?? "") ? args.description.trim() : void 0;
1218
+ const same = (a, b) => !!a && !!b && a.length === b.length && a.every((x) => b.includes(x));
1219
+ const untouched = Object.keys(next).length === Object.keys(before).length && Object.keys(next).every((k) => same(next[k], before[k]));
1220
+ if (untouched && !rename && redescribe === void 0) {
1221
+ return {
1222
+ error: "no_change",
1223
+ message: "That would leave the profile exactly as it is. Tell the user what it already has instead of writing it again.",
1224
+ current: describeGrants(before, byId),
1225
+ ...rejected.length ? { rejectedFromYourProposal: rejected } : {}
1226
+ };
1227
+ }
1228
+ const rows = [];
1229
+ for (const area of Object.keys(next)) {
1230
+ const label = byId.get(area)?.label ?? area;
1231
+ const now = (next[area] ?? []).map((a) => ACTION_LABELS[a] ?? a).join(", ");
1232
+ if (!(area in before)) rows.push({ label, type: `${now} (novo)` });
1233
+ else if (!same(next[area], before[area])) {
1234
+ const was = (before[area] ?? []).map((a) => ACTION_LABELS[a] ?? a).join(", ");
1235
+ rows.push({ label, type: `${now} (antes: ${was})` });
1236
+ } else rows.push({ label, type: now });
1237
+ }
1238
+ for (const area of Object.keys(before)) {
1239
+ if (!(area in next)) rows.push({ label: byId.get(area)?.label ?? area, type: "REMOVIDO" });
1240
+ }
1241
+ const decision = await requestChatConfirmation({
1242
+ id: `permission-profile-update-${Date.now()}`,
1243
+ toolName: "updatePermissionProfile",
1244
+ title: `Alterar perfil de acesso: ${rename ?? target.name}`,
1245
+ params: {
1246
+ perfil: target.name,
1247
+ ...rename ? { "novo nome": rename } : {},
1248
+ ...redescribe !== void 0 ? { descri\u00E7\u00E3o: redescribe } : {},
1249
+ acessos: rows
1250
+ },
1251
+ plane: "client",
1252
+ noAutoApproval: true
1253
+ });
1254
+ if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
1255
+ const finalName = typeof decision.values?.["novo nome"] === "string" && decision.values["novo nome"] ? decision.values["novo nome"].trim() : rename;
1256
+ try {
1257
+ await ctx.orgAdapter.updateProfile(ctx.currentOrg.id, target.id, {
1258
+ ...finalName ? { name: finalName } : {},
1259
+ ...redescribe !== void 0 ? { description: redescribe || void 0 } : {},
1260
+ grants: next
1261
+ });
1262
+ } catch (error) {
1263
+ return {
1264
+ error: "update_failed",
1265
+ message: `The profile could not be saved: ${describeThrown(error)}. Report this to the user \u2014 do not retry.`
1266
+ };
1267
+ }
1268
+ if (ctx.orgAdapter.listProfiles) {
1269
+ try {
1270
+ usePermissionsStore.getState().setProfiles(await ctx.orgAdapter.listProfiles(ctx.currentOrg.id));
1271
+ } catch {
1272
+ }
1273
+ }
1274
+ return {
1275
+ ok: true,
1276
+ profile: { id: target.id, name: finalName ?? target.name },
1277
+ grants: describeGrants(next, byId),
1278
+ ...rejected.length ? { rejectedFromYourProposal: rejected } : {},
1279
+ message: "Profile updated. Tell the user WHAT CHANGED and what the profile can reach now, using the labels in `grants` \u2014 not the ids. The link to review it is already in the reply.",
1280
+ ref: { id: target.id, label: finalName ?? target.name, path: PERMISSIONS_PATH }
1281
+ };
1282
+ });
1283
+ function describeGrants(grants, byId) {
1284
+ return Object.entries(grants).map(([featureId, actions]) => ({
1285
+ label: byId.get(featureId)?.label ?? featureId,
1286
+ type: actions.map((a) => ACTION_LABELS[a] ?? a).join(", ")
1287
+ }));
1288
+ }
1289
+ var PERMISSIONS_PATH = "/settings/permissions";
1290
+ var ACTION_LABELS = {
1291
+ read: "ver",
1292
+ create: "criar",
1293
+ edit: "editar",
1294
+ delete: "excluir"
1295
+ };
1296
+ async function executeRpcTool(rpcName, args, userId) {
1297
+ const supabase = getSupabaseClientOptional();
1298
+ const tenantId = getActiveTenantId();
1299
+ if (!supabase?.rpc || !tenantId || !userId) {
1300
+ return errorResult("not_executable", "No authenticated session/tenant available for this action.");
1301
+ }
1302
+ const { data, error } = await supabase.rpc(rpcName, {
1303
+ p_tenant_id: tenantId,
1304
+ p_actor_user_id: userId,
1305
+ p_payload: args
1306
+ });
1307
+ if (error) return errorResult("execution_failed", error.message);
1308
+ return { ok: true, content: JSON.stringify(data ?? { ok: true }) };
1309
+ }
1310
+ registerAIToolHandler(
1311
+ "findTools",
1312
+ (args, ctx) => resolveToolSearch(args, ctx.searchableTools ?? [], new Set(ctx.carriedTools ?? []))
1313
+ );
1314
+ function useAgentSurface(contribution) {
1315
+ const ref = React.useRef(contribution);
1316
+ ref.current = contribution;
1317
+ const id = contribution.id;
1318
+ React.useEffect(() => {
1319
+ registerSurface({ id, get: () => ref.current });
1320
+ const names = (ref.current.tools ?? []).map((t) => t.tool.name);
1321
+ for (const name of names) {
1322
+ registerAIToolHandler(name, (args) => {
1323
+ const entry = ref.current.tools?.find((t) => t.tool.name === name);
1324
+ if (!entry) return { error: "tool_unmounted", message: "That surface is no longer open." };
1325
+ return entry.execute(args);
1326
+ });
1327
+ }
1328
+ return () => {
1329
+ unregisterSurface(id);
1330
+ for (const name of names) unregisterAIToolHandler(name);
1331
+ };
1332
+ }, [id]);
1333
+ }
1334
+ var HANDOFF_ID = ASK_AI_HANDOFF_ID;
1335
+ var releaseIntro = null;
1336
+ function borrowIntro(intro, placeholder) {
1337
+ const store = useAssistantInviteStore.getState();
1338
+ const previous = store.invite?.id === HANDOFF_ID ? null : store.invite ?? null;
1339
+ if (previous || !releaseIntro) {
1340
+ releaseIntro = () => {
1341
+ if (useAssistantInviteStore.getState().invite?.id === HANDOFF_ID) {
1342
+ useAssistantInviteStore.getState().setInvite(previous);
1343
+ }
1344
+ releaseIntro = null;
1345
+ };
1346
+ }
1347
+ const def = {
1348
+ id: HANDOFF_ID,
1349
+ ...intro.role ? { role: intro.role } : {},
1350
+ persona: intro.persona,
1351
+ headline: intro.pitch,
1352
+ pitch: intro.pitch,
1353
+ description: intro.description,
1354
+ placeholder,
1355
+ suggestions: intro.suggestions,
1356
+ // Never the hero takeover: the user asked for the assistant from a screen
1357
+ // they are still looking at, and covering it would be answering a question
1358
+ // they did not ask.
1359
+ auto: false
1360
+ };
1361
+ store.setInvite(def);
1362
+ }
1363
+ function releaseOnClose() {
1364
+ const stop = useChatStore.subscribe((state, previous) => {
1365
+ if (previous.isOpen && !state.isOpen) {
1366
+ releaseIntro?.();
1367
+ stop();
1368
+ }
1369
+ });
1370
+ }
1371
+ function askAI({ hint, prompt, draft, media, intro } = {}) {
1372
+ const chat = useChatStore.getState();
1373
+ chat.setComposerHint(hint ?? null);
1374
+ chat.requestComposerFocus();
1375
+ if (media) chat.attachMedia(media);
1376
+ if (draft) chat.setDraft(draft);
1377
+ if (intro) {
1378
+ borrowIntro(intro, hint);
1379
+ releaseOnClose();
1380
+ }
1381
+ if (prompt) chat.queuePrompt(prompt);
1382
+ else chat.setOpen(true);
1383
+ }
1384
+ function AskAIMenuItem({
1385
+ hint,
1386
+ prompt,
1387
+ draft,
1388
+ media,
1389
+ intro,
1390
+ label,
1391
+ description,
1392
+ onHandoff
1393
+ }) {
1394
+ const t = useTranslation();
1395
+ return /* @__PURE__ */ jsxs(
1396
+ DropdownItem,
1397
+ {
1398
+ onSelect: () => {
1399
+ askAI({ hint, prompt, draft, media, intro });
1400
+ onHandoff?.();
1401
+ },
1402
+ className: "fz-ai-action items-start gap-2 py-2",
1403
+ children: [
1404
+ /* @__PURE__ */ jsx(WandSparkles, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
1405
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
1406
+ /* @__PURE__ */ jsx("span", { className: "block font-medium", children: label ?? t("assistant.askAI") }),
1407
+ description ? /* @__PURE__ */ jsx("span", { className: "block text-xs font-normal opacity-70", children: description }) : null
1408
+ ] })
1409
+ ]
1410
+ }
1411
+ );
1412
+ }
1413
+ function AskAIButton({
1414
+ hint,
1415
+ prompt,
1416
+ draft,
1417
+ media,
1418
+ intro,
1419
+ label,
1420
+ onHandoff,
1421
+ size = "sm",
1422
+ variant = "default",
1423
+ className,
1424
+ disabled
1425
+ }) {
1426
+ const t = useTranslation();
1427
+ return /* @__PURE__ */ jsxs(
1428
+ Button,
1429
+ {
1430
+ type: "button",
1431
+ size,
1432
+ variant,
1433
+ className,
1434
+ disabled,
1435
+ onClick: () => {
1436
+ askAI({ hint, prompt, draft, media, intro });
1437
+ onHandoff?.();
1438
+ },
1439
+ children: [
1440
+ /* @__PURE__ */ jsx(WandSparkles, { className: "mr-1.5 h-3.5 w-3.5" }),
1441
+ label ?? t("assistant.askAI")
1442
+ ]
1443
+ }
1444
+ );
1445
+ }
1446
+ function DeleteConfirmDialog({ open, onClose, onConfirm, entityName, displayValue, mode = "delete" }) {
1447
+ const t = useTranslation();
1448
+ const soft = mode === "deactivate";
1449
+ const key = (suffix) => soft ? `crud.deactivate.${suffix}` : `crud.delete.${suffix}`;
1450
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (isOpen) => {
1451
+ if (!isOpen) onClose();
1452
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { children: [
1453
+ /* @__PURE__ */ jsxs(ModalHeader, { children: [
1454
+ /* @__PURE__ */ jsx(ModalTitle, { children: t(key("title"), { entity: entityName }) }),
1455
+ /* @__PURE__ */ jsx(ModalDescription, { children: displayValue ? t(key("confirmNamed"), { displayValue }) : t(key("confirmGeneric"), { entity: entityName.toLowerCase() }) })
1456
+ ] }),
1457
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
1458
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("common.cancel") }),
1459
+ /* @__PURE__ */ jsx(Button, { variant: soft ? "default" : "destructive", onClick: onConfirm, children: soft ? t("crud.deactivate.action") : t("common.delete") })
1460
+ ] })
1461
+ ] }) });
1462
+ }
1463
+
1464
+ export { ASK_AI_HANDOFF_ID, AskAIButton, AskAIMenuItem, DeleteConfirmDialog, askAI, buildAgentToolset, buildDataToolIndex, executeAITool, executeRpcTool, getActiveSurfaceContext, getSurfaceTools, getSurfaceVersion, hasAIToolHandler, registerAIToolHandler, revealPage, subscribeSurfaces, unregisterAIToolHandler, useAgentSurface, useAssistantInviteStore };
1465
+ //# sourceMappingURL=chunk-6QFOE3UD.js.map
1466
+ //# sourceMappingURL=chunk-6QFOE3UD.js.map