@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,1651 @@
1
+ import { useOrganizationStore } from './chunk-YKPPCV2Z.js';
2
+ import '@fayz-ai/core';
3
+ import { taskEntityDef } from '@fayz-ai/tasks/data/supabase';
4
+ import { create } from 'zustand';
5
+ import { useAuthStore } from '@fayz-ai/auth';
6
+
7
+ // src/lib/report-attachments.ts
8
+ var turnFiles = [];
9
+ function setTurnReportFiles(files) {
10
+ if (files.length) turnFiles = files.slice(0, 8);
11
+ }
12
+ function takeTurnReportFiles() {
13
+ const out = turnFiles;
14
+ turnFiles = [];
15
+ return out;
16
+ }
17
+ function clearTurnReportFiles() {
18
+ turnFiles = [];
19
+ }
20
+ var addressEntity = {
21
+ name: "Address",
22
+ namePlural: "Addresses",
23
+ icon: "MapPin",
24
+ data: {
25
+ table: "addresses",
26
+ tenantScoped: true,
27
+ tenantIdColumn: "tenant_id",
28
+ searchColumns: ["label", "street", "district", "city", "postal_code", "recipient"]
29
+ },
30
+ fields: [
31
+ // owner_id/owner_type are how an address attaches to its person. They are
32
+ // required and described rather than hidden: the agent reaches this table
33
+ // from a record it already has in hand ("adiciona o endereço X pra ele"),
34
+ // so the id it must pass is the one it just searched for or created.
35
+ {
36
+ key: "owner_id",
37
+ label: "Owner record id",
38
+ type: "text",
39
+ required: true,
40
+ hint: "Id of the person (client, contact, supplier, staff\u2026) this address belongs to."
41
+ },
42
+ {
43
+ key: "owner_type",
44
+ label: "Owner kind",
45
+ type: "select",
46
+ options: ["person", "location", "tenant"],
47
+ hint: "Defaults to 'person'. Only set it for an address that belongs to a business location or to the tenant itself."
48
+ },
49
+ { key: "label", label: "Label", type: "text", searchable: true, hint: "Casa, Trabalho, Dep\xF3sito\u2026" },
50
+ {
51
+ key: "kind",
52
+ label: "Use",
53
+ type: "select",
54
+ options: ["both", "shipping", "billing"],
55
+ hint: "Defaults to 'both'."
56
+ },
57
+ { key: "postal_code", label: "Postal code", type: "text", required: true, searchable: true, hint: "CEP." },
58
+ { key: "street", label: "Street", type: "text", required: true, searchable: true },
59
+ { key: "number", label: "Number", type: "text" },
60
+ { key: "complement", label: "Complement", type: "text" },
61
+ { key: "district", label: "District", type: "text", searchable: true, hint: "Bairro." },
62
+ { key: "city", label: "City", type: "text", required: true, searchable: true },
63
+ { key: "state", label: "State", type: "text", required: true, hint: "Two-letter UF." },
64
+ { key: "country", label: "Country", type: "text", hint: "Defaults to 'BR'." },
65
+ { key: "recipient", label: "Recipient", type: "text", searchable: true, hint: "Who receives, if not the owner." },
66
+ { key: "phone", label: "Phone", type: "text" },
67
+ {
68
+ key: "is_default",
69
+ label: "Default address",
70
+ type: "boolean",
71
+ hint: "Only one default per owner and kind \u2014 clear the previous one before setting this."
72
+ }
73
+ ]
74
+ };
75
+ var CORE_QUERY_ENTITIES = [
76
+ {
77
+ key: "address",
78
+ entity: addressEntity,
79
+ writable: true,
80
+ agentHint: "`address` holds the street address of a PERSON (client, contact, supplier, staff). A person record has NO street/city/postal_code fields of its own. To read where someone lives, search `address` with filters {\"owner_id\": <the person's id>} \u2014 a person with no address field is not a person without an address. To record one, create an `address` with owner_id set to the person's id. Never write an address into a notes field."
81
+ },
82
+ {
83
+ key: "task",
84
+ entity: taskEntityDef,
85
+ writable: true,
86
+ agentHint: '`task` is the workspace to-do list shown in the assistant column. Rows with source = "onboarding" are the setup checklist and are assigned to whoever created the workspace. To close one, update its `status` to "done" \u2014 never delete it. Leave `source` and `source_key` alone when creating a task; they belong to the feature that seeded the row.'
87
+ }
88
+ ];
89
+
90
+ // src/lib/core-ai-tools.ts
91
+ var coreAITools = [
92
+ {
93
+ id: "core.business-summary",
94
+ name: "getBusinessSummary",
95
+ description: "Returns a summary of the current business: name, plan, vertical, team size, and key metrics.",
96
+ icon: "Building2",
97
+ mode: "read",
98
+ parameters: { type: "object", properties: {}, required: [] },
99
+ category: "Core",
100
+ // No opener suggestion: plugin-dashboard's kpi-summary already offers the
101
+ // "how is the business doing" prompt (localised). A second one here only
102
+ // duplicated it, in English, on the opening screen.
103
+ suggestions: []
104
+ },
105
+ {
106
+ id: "core.team-members",
107
+ name: "getTeamMembers",
108
+ description: "Lists the WORKSPACE TEAM exactly as the Team screen shows it: the people who staff this business. Each row carries `kind` (what they do here \u2014 teacher, staff\u2026), `accessRole` (their permission role, null when they have no login) and `hasLogin`. Answer headcount questions from this \u2014 never report the number of logins as the size of the team.",
109
+ icon: "Users",
110
+ mode: "read",
111
+ parameters: {
112
+ type: "object",
113
+ properties: {
114
+ role: {
115
+ type: "string",
116
+ description: "Optional filter, matched against either the person kind (teacher, staff\u2026) or the access role (owner, admin, manager, viewer)."
117
+ }
118
+ }
119
+ },
120
+ permission: { feature: "members", action: "read" },
121
+ category: "Core",
122
+ suggestions: [
123
+ { label: "Quem est\xE1 na minha equipe?" }
124
+ ]
125
+ },
126
+ {
127
+ id: "core.navigate",
128
+ name: "navigateTo",
129
+ description: "Opens one of the app's PAGES (Agenda, Clients, Financial\u2026). Takes a page name, never a person or a record \u2014 to open a specific record use openRecord.",
130
+ icon: "Compass",
131
+ mode: "read",
132
+ parameters: {
133
+ type: "object",
134
+ properties: {
135
+ page: { type: "string", description: "Name of the page, as listed in the app pages" }
136
+ },
137
+ required: ["page"]
138
+ },
139
+ category: "Core"
140
+ },
141
+ {
142
+ id: "core.find-connectors",
143
+ name: "findConnectors",
144
+ description: 'Finds the INTEGRATIONS this app can have with outside providers \u2014 payments (Pix, cart\xE3o, boleto), frete, nota fiscal/ERP, banco, CRM, agenda, mensagens. Call it WHENEVER someone asks for a provider by name ("tem AppMax?", "queria pagamento com a appmax") or by need ("quero receber por Pix", "preciso emitir nota fiscal"). You have NO other way to know what this app can connect \u2014 never answer that you cannot help with a specific provider without calling this first. Each result carries a `state`, and they mean different things: `connected` (already live), `needs_setup` (authorized, but it CANNOT work until something is chosen on its screen \u2014 never call this one ready or working), `ready` (installed here \u2014 the person can connect it right now), `soon` (a module this app has declared it, but it is not built yet \u2014 do NOT offer to connect it), and `needs_module` (it belongs to a module this app was not built with \u2014 the person CANNOT install a module, so say plainly that it would come with a new version of the app and offer nothing to click). The reply already renders a card per result, with the brand mark and the button; answer in one or two short sentences and do not repeat the list, the logos or any link. You never connect anything yourself \u2014 connecting is a human step on the connector screen.',
145
+ icon: "Plug",
146
+ mode: "read",
147
+ parameters: {
148
+ type: "object",
149
+ properties: {
150
+ need: {
151
+ type: "string",
152
+ description: 'What the person needs, in their own words \u2014 a provider name ("appmax") or a need ("pagamento", "receber por pix", "frete", "nota fiscal").'
153
+ }
154
+ },
155
+ required: ["need"]
156
+ },
157
+ category: "Core",
158
+ suggestions: [{ label: "D\xE1 pra receber por Pix aqui?" }]
159
+ },
160
+ {
161
+ id: "core.open-record",
162
+ name: "openRecord",
163
+ description: "Opens the detail page of a record already returned by a tool in THIS conversation. Takes that record's id \u2014 never a name.",
164
+ icon: "ExternalLink",
165
+ mode: "read",
166
+ parameters: {
167
+ type: "object",
168
+ properties: {
169
+ id: { type: "string", description: "Record id from an earlier tool result" }
170
+ },
171
+ required: ["id"]
172
+ },
173
+ category: "Core"
174
+ }
175
+ ];
176
+ function pascalCase(value) {
177
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
178
+ }
179
+ function registryToolName(pluginId, registryId) {
180
+ return `list${pascalCase(pluginId)}${pascalCase(registryId)}`;
181
+ }
182
+ function formatToolSignature(tool) {
183
+ const params = tool.parameters?.properties ? Object.entries(tool.parameters.properties).map(([key]) => tool.parameters?.required?.includes(key) ? key : `${key}?`).join(", ") : "";
184
+ return `${tool.name}(${params})`;
185
+ }
186
+ function entityToolName(entityKey) {
187
+ return `search${pascalCase(entityKey)}`;
188
+ }
189
+ function attachedRecordGuidance(writableKeys) {
190
+ const hints = CORE_QUERY_ENTITIES.filter((q) => q.agentHint && writableKeys.includes(q.key)).map((q) => q.agentHint);
191
+ return hints.length ? ` Records that attach to another record: ${hints.join(" ")}` : "";
192
+ }
193
+ var FIELD_CATALOGUE_POINTER = "\n\nField names per entity are listed in the `searchRecords` tool description (* = required). Send nothing outside them.";
194
+ function fieldCatalogue(entries) {
195
+ const lines = entries.filter((e) => e.fields.length > 0).map((e) => {
196
+ const names = e.fields.slice(0, 24).map((f) => f.required ? `${f.key}*` : f.key);
197
+ return `${e.key}: ${names.join(", ")}`;
198
+ });
199
+ if (!lines.length) return "";
200
+ return `
201
+
202
+ Fields by entity (* = required, send nothing else):
203
+ ${lines.join("\n")}`;
204
+ }
205
+ function buildDataPrimitiveTools(input) {
206
+ const options = [];
207
+ const schema = [];
208
+ for (const e of input.entities) {
209
+ if (e.entityDef && !e.entityDef.agentHidden) {
210
+ options.push({ key: e.entityKey, label: e.labelPlural });
211
+ schema.push({ key: e.entityKey, fields: e.entityDef.fields ?? [] });
212
+ }
213
+ }
214
+ for (const [pluginId, defs] of input.registries) {
215
+ for (const registry2 of defs) {
216
+ if (registry2.readOnly) continue;
217
+ const key = `${pluginId}:${registry2.id}`;
218
+ options.push({
219
+ key,
220
+ label: registry2.entity.namePlural ?? registry2.entity.name,
221
+ ...registry2.agentHint ? { hint: registry2.agentHint } : {}
222
+ });
223
+ schema.push({ key, fields: registry2.entity.fields ?? [] });
224
+ }
225
+ }
226
+ const writableKeys = options.map((o) => o.key);
227
+ for (const q of [...input.queryEntities ?? [], ...CORE_QUERY_ENTITIES]) {
228
+ options.push({ key: q.key, label: q.entity.namePlural ?? q.entity.name });
229
+ if (q.writable) writableKeys.push(q.key);
230
+ }
231
+ if (!options.length) return [];
232
+ const keys = options.map((o) => o.key);
233
+ const catalogLine = options.map((o) => `${o.key} (${o.label.toLowerCase()}${o.hint ? ` \u2014 ${o.hint}` : ""})`).join(", ");
234
+ return [
235
+ {
236
+ id: "data.find-anything",
237
+ name: "findAnything",
238
+ description: `Global search (like the app command center): looks a name/text up across ALL record types the user may see at once and returns grouped matches. Use this FIRST whenever you do not know what kind of record a name refers to ("quem \xE9 X?", "o que \xE9 Y?") \u2014 then drill down with searchRecords. It matches TEXT INSIDE each record, so records attached to a person are never returned by that person's name \u2014 reaching them takes a second searchRecords call filtered by owner_id.` + attachedRecordGuidance(keys),
239
+ icon: "Command",
240
+ mode: "read",
241
+ parameters: {
242
+ type: "object",
243
+ properties: {
244
+ search: { type: "string", description: "Name or text to look up everywhere." }
245
+ },
246
+ required: ["search"]
247
+ },
248
+ category: "Data",
249
+ suggestions: [{ label: "Quem \xE9 \u2026?" }]
250
+ },
251
+ ...writableKeys.length ? [
252
+ {
253
+ id: "data.create-record",
254
+ name: "createRecord",
255
+ description: "Creates ONE new record of an entity (client, service, supplier, \u2026). ALWAYS search first to avoid duplicates. Use ONLY the fields listed below for that entity. Do NOT interview the user field-by-field. Ask in chat only for required (*) fields you genuinely cannot infer, and call this tool the moment you have them \u2014 omit every optional field you were not given (its default applies; a boolean status like active defaults to on/true). The confirmation this opens is an EDITABLE FORM, not a yes/no receipt: the user reviews, fills and corrects anything there \u2014 including optionals \u2014 before it is written. Opening that form fast beats collecting everything in chat." + attachedRecordGuidance(writableKeys) + FIELD_CATALOGUE_POINTER,
256
+ icon: "Plus",
257
+ mode: "persist",
258
+ // The handler orchestrates its own guard chain (permission → plan
259
+ // cap → confirmation → create) because the target is dynamic.
260
+ requiresConfirmation: false,
261
+ parameters: {
262
+ type: "object",
263
+ properties: {
264
+ entity: { type: "string", description: "Which entity to create", enum: writableKeys },
265
+ values: { type: "object", description: 'Field values for the new record, e.g. {"name":"\u2026","phone":"\u2026"}' }
266
+ },
267
+ required: ["entity", "values"]
268
+ },
269
+ category: "Data"
270
+ }
271
+ ] : [],
272
+ ...writableKeys.length ? [
273
+ {
274
+ id: "data.update-record",
275
+ name: "updateRecord",
276
+ description: "Updates fields on an EXISTING record (add an email, fix a phone, change a name\u2026). Use the record id already known from this conversation or from a search. Only send the fields being changed, and only fields the entity actually has. The user confirms before anything is written." + FIELD_CATALOGUE_POINTER,
277
+ icon: "Pencil",
278
+ mode: "persist",
279
+ requiresConfirmation: false,
280
+ parameters: {
281
+ type: "object",
282
+ properties: {
283
+ entity: { type: "string", description: "Which entity the record belongs to", enum: writableKeys },
284
+ id: { type: "string", description: "Id of the record to update" },
285
+ values: { type: "object", description: 'Only the fields to change, e.g. {"email":"\u2026"}' }
286
+ },
287
+ required: ["entity", "id", "values"]
288
+ },
289
+ category: "Data"
290
+ }
291
+ ] : [],
292
+ {
293
+ id: "data.search-records",
294
+ name: "searchRecords",
295
+ description: `Searches or lists records of ONE entity and returns matching rows with their fields. Use \`search\` for names/text and \`filters\` for exact field values (e.g. {"status":"open"}) \u2014 never put a status word in search. For ANY date or period question ("hoje", "amanh\xE3", "esta semana"), use \`dateField\` + \`from\`/\`to\` \u2014 NOT \`filters\`. Plain dates (YYYY-MM-DD) are read as whole days in the business timezone, so \`from\` and \`to\` both being today is exactly "today". Entities: ${catalogLine}.${attachedRecordGuidance(keys)}` + fieldCatalogue(schema),
296
+ icon: "Search",
297
+ mode: "read",
298
+ parameters: {
299
+ type: "object",
300
+ properties: {
301
+ entity: { type: "string", description: "Which entity to search", enum: keys },
302
+ search: { type: "string", description: "Name or text to match. Omit to list recent records." },
303
+ filters: { type: "object", description: 'Optional exact-value filters {field: value}, e.g. {"status":"open"}. Do not use for dates.' },
304
+ dateField: { type: "string", description: "Date field the from/to window applies to (e.g. startsAt for agenda; default 'createdAt')." },
305
+ from: { type: "string", description: "Start of the window: YYYY-MM-DD (whole local day) or ISO datetime." },
306
+ to: { type: "string", description: "End of the window: YYYY-MM-DD (whole local day, inclusive) or ISO datetime." },
307
+ orderBy: { type: "string", description: 'Field to sort by (e.g. startsAt for "next/upcoming" questions).' },
308
+ direction: { type: "string", description: "Sort direction", enum: ["asc", "desc"] }
309
+ },
310
+ required: ["entity"]
311
+ },
312
+ category: "Data"
313
+ },
314
+ ...writableKeys.length ? [
315
+ {
316
+ id: "data.propose-import",
317
+ name: "proposeImport",
318
+ description: `Proposes how the columns of an ATTACHED spreadsheet/CSV map to the fields of one entity, and opens the mapping for the user to review and correct. Use this the moment a file is attached and the user talks about importing \u2014 it is the ONLY way to answer that, and it replaces writing the mapping out as text. Send one entry per column you can map; columns you leave out are shown as unmapped for the user to fill in. Field names come from the entity field list in createRecord. When it comes back ok, call commitImport with the same mapping \u2014 that is what actually writes the rows. Entities: ${catalogLine}.`,
319
+ icon: "ArrowRightLeft",
320
+ mode: "read",
321
+ category: "Data",
322
+ parameters: {
323
+ type: "object",
324
+ properties: {
325
+ entity: { type: "string", description: "Which entity the rows would become", enum: writableKeys },
326
+ mapping: {
327
+ type: "object",
328
+ description: 'Column name from the file \u2192 field name in the app, e.g. {"Nome":"name","Celular":"phone"}. Leave a column out when no field fits.'
329
+ },
330
+ file: { type: "string", description: "File name, when more than one is attached." }
331
+ },
332
+ required: ["entity", "mapping"]
333
+ }
334
+ },
335
+ {
336
+ id: "data.commit-import",
337
+ name: "commitImport",
338
+ description: `WRITES the rows of an attached file as records, using a mapping the user already confirmed through proposeImport. Call it only after proposeImport came back ok, and pass the SAME mapping it returned. The user confirms the row count before anything is written. Never call this to "check" anything \u2014 it creates records. Entities: ${catalogLine}.`,
339
+ icon: "Upload",
340
+ mode: "persist",
341
+ requiresConfirmation: false,
342
+ category: "Data",
343
+ parameters: {
344
+ type: "object",
345
+ properties: {
346
+ entity: { type: "string", description: "Which entity the rows become", enum: writableKeys },
347
+ mapping: {
348
+ type: "object",
349
+ description: "The confirmed mapping from proposeImport: file column -> app field."
350
+ },
351
+ file: { type: "string", description: "File name, when more than one is attached." }
352
+ },
353
+ required: ["entity", "mapping"]
354
+ }
355
+ }
356
+ ] : [],
357
+ {
358
+ id: "data.query-data",
359
+ name: "queryData",
360
+ description: `Aggregates over ONE entity: count, sum or avg of a field, with optional date range, equality filters and a group-by. Use for analytical questions (revenue this week, new clients this month, busiest professional). Entities: ${catalogLine}.`,
361
+ icon: "Sigma",
362
+ mode: "read",
363
+ parameters: {
364
+ type: "object",
365
+ properties: {
366
+ entity: { type: "string", description: "Which entity to aggregate", enum: keys },
367
+ metric: { type: "string", description: "Aggregation", enum: ["count", "sum", "avg"] },
368
+ field: { type: "string", description: "Field to sum/avg (record field name). Ignored for count." },
369
+ dateField: { type: "string", description: "Date field to range-filter (default 'createdAt'; use startsAt for agenda questions)." },
370
+ from: { type: "string", description: "Lower bound (inclusive): YYYY-MM-DD (whole local day) or ISO datetime." },
371
+ to: { type: "string", description: "Upper bound (inclusive): YYYY-MM-DD (whole local day) or ISO datetime." },
372
+ groupBy: { type: "string", description: "Optional field to group results by." },
373
+ filters: { type: "object", description: "Optional equality filters {field: value}." }
374
+ },
375
+ required: ["entity", "metric"]
376
+ },
377
+ category: "Data"
378
+ }
379
+ ];
380
+ }
381
+ var PROFILE_ACTIONS = ["read", "create", "edit", "delete"];
382
+ var MAX_FEATURES = 80;
383
+ function buildPermissionTools(features, profiles = []) {
384
+ const usable = features.filter((f) => f.id && f.label).slice(0, MAX_FEATURES);
385
+ if (!usable.length) return [];
386
+ const catalog = usable.map((f) => `${f.id} (${f.label}${f.group && f.group !== f.label ? ` \u2014 ${f.group}` : ""})`).join("; ");
387
+ const editable = profiles.filter((p) => !p.isSystem);
388
+ return [
389
+ {
390
+ id: "core.create-permission-profile",
391
+ name: "createPermissionProfile",
392
+ description: `Creates an ACCESS PROFILE (permission role) that people on the team can then be assigned to \u2014 "um perfil para Marketing", "um acesso s\xF3 de leitura para o estagi\xE1rio". YOU choose which areas the role needs from the catalog below and which actions on each: pick the smallest set that lets that role do its job, and grant \`delete\` only when the user asked for it. Use the exact ids from the catalog \u2014 an id that is not in it is rejected. After it comes back ok, tell the user in plain language what the profile can and cannot reach, using the labels \u2014 not the ids.
393
+
394
+ Areas (id (label \u2014 group)): ${catalog}
395
+ Actions on each area: ${PROFILE_ACTIONS.join(", ")}.`,
396
+ icon: "ShieldCheck",
397
+ mode: "persist",
398
+ // Own guard chain: system permission → validation → confirmation → write.
399
+ requiresConfirmation: false,
400
+ parameters: {
401
+ type: "object",
402
+ properties: {
403
+ name: { type: "string", description: 'Name of the profile, e.g. "Marketing".' },
404
+ description: { type: "string", description: "One line on what this role is for." },
405
+ grants: {
406
+ type: "object",
407
+ description: 'Area id \u2192 actions, e.g. {"crm.leads":["read","create","edit"],"agenda":["read"]}. Leave an area out entirely when the role should not reach it.'
408
+ }
409
+ },
410
+ required: ["name", "grants"]
411
+ },
412
+ category: "Core",
413
+ suggestions: [{ label: "Cria um perfil de acesso para Marketing" }]
414
+ },
415
+ ...editable.length ? [
416
+ {
417
+ id: "core.update-permission-profile",
418
+ name: "updatePermissionProfile",
419
+ description: `Changes an access profile that ALREADY exists \u2014 "d\xE1 edi\xE7\xE3o em cursos para o Marketing", "deixa o estagi\xE1rio s\xF3 com leitura", "renomeia para Growth". This is the ONLY way to change a profile: never reach for a record tool, a profile is not a record. Grants MERGE by area: an area you send replaces that area's actions, an area you leave out keeps what it had, and an area only goes away when you name it in \`revoke\`. So "s\xF3 leitura em cursos" is grants {courses:["read"]}, and "tira o financeiro" is revoke ["financial"]. YOU decide which areas and actions that means \u2014 ask the user only when the areas themselves are ambiguous, never to have them dictate the action lists.
420
+
421
+ Profiles you can change (id \u2014 name): ${editable.map((p) => `${p.id} \u2014 ${p.name}`).join("; ")}
422
+ Areas (id (label \u2014 group)): ${catalog}
423
+ Actions on each area: ${PROFILE_ACTIONS.join(", ")}.`,
424
+ icon: "ShieldCheck",
425
+ mode: "persist",
426
+ requiresConfirmation: false,
427
+ parameters: {
428
+ type: "object",
429
+ properties: {
430
+ id: { type: "string", description: "Id of the profile to change, from the list above.", enum: editable.map((p) => p.id) },
431
+ name: { type: "string", description: "New name, only when renaming." },
432
+ description: { type: "string", description: "New one-line description, only when changing it." },
433
+ grants: {
434
+ type: "object",
435
+ description: 'Areas to set, e.g. {"courses":["read","edit"]}. Replaces the actions of the areas named here and leaves every other area alone.'
436
+ },
437
+ revoke: {
438
+ type: "array",
439
+ items: { type: "string" },
440
+ description: "Area ids the profile should lose entirely."
441
+ }
442
+ },
443
+ required: ["id"]
444
+ },
445
+ category: "Core"
446
+ }
447
+ ] : []
448
+ ];
449
+ }
450
+
451
+ // src/lib/tool-search.ts
452
+ var ALWAYS_ON_TOOLS = /* @__PURE__ */ new Set([
453
+ "getBusinessSummary",
454
+ "getTeamMembers",
455
+ "navigateTo",
456
+ "openRecord",
457
+ "findAnything",
458
+ "searchRecords",
459
+ "queryData",
460
+ "createRecord",
461
+ "updateRecord",
462
+ "findTools",
463
+ // Reporting a problem has to work from ANY screen — that is where a user hits
464
+ // one. So the Dico carries it every turn instead of behind findTools; the
465
+ // plugin registers its executor, and a turn without the plugin simply has no
466
+ // such tool to ship. findSupportRequests rides along so the agent can check an
467
+ // existing ticket before opening a duplicate.
468
+ "reportIssue",
469
+ "findSupportRequests"
470
+ ]);
471
+ var loaded = /* @__PURE__ */ new Set();
472
+ var areas = /* @__PURE__ */ new Set();
473
+ var listeners = /* @__PURE__ */ new Set();
474
+ function bump() {
475
+ for (const fn of listeners) fn();
476
+ }
477
+ function loadedToolNames() {
478
+ return loaded;
479
+ }
480
+ function loadTools(names) {
481
+ const added = names.filter((n) => !loaded.has(n));
482
+ if (!added.length) return [];
483
+ for (const n of added) loaded.add(n);
484
+ bump();
485
+ return added;
486
+ }
487
+ function loadArea(area) {
488
+ if (areas.has(area)) return;
489
+ areas.add(area);
490
+ bump();
491
+ }
492
+ function resetLoadedTools() {
493
+ if (!loaded.size && !areas.size) return;
494
+ loaded.clear();
495
+ areas.clear();
496
+ bump();
497
+ }
498
+ function resolveLoadedNames(catalog) {
499
+ const names = new Set(loaded);
500
+ if (areas.size) {
501
+ for (const tool of catalog) {
502
+ if (tool.role && areas.has(tool.role)) names.add(tool.name);
503
+ }
504
+ }
505
+ return names;
506
+ }
507
+ function areaOf(tool) {
508
+ return tool.category ?? tool.id.split(".")[0] ?? "Outros";
509
+ }
510
+ function searchableTools(catalog, pinned) {
511
+ return catalog.filter((t) => !pinned.has(t.name) && !ALWAYS_ON_TOOLS.has(t.name));
512
+ }
513
+ function buildToolIndex(tools) {
514
+ const byArea = /* @__PURE__ */ new Map();
515
+ for (const tool of tools) {
516
+ const area = areaOf(tool);
517
+ if (!byArea.has(area)) byArea.set(area, []);
518
+ byArea.get(area).push(tool.name);
519
+ }
520
+ return [...byArea.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([area, names]) => `${area}: ${names.join(", ")}`).join("\n");
521
+ }
522
+ function buildToolSearchTool(searchable) {
523
+ if (!searchable.length) return [];
524
+ return [
525
+ {
526
+ id: "core.find-tools",
527
+ name: "findTools",
528
+ description: `Loads MORE tools. You carry the general-purpose ones plus the ones for the screen the user is on; everything else in this app is listed below and arrives only when you ask for it here. Call this the moment a request belongs to an area you have no tool for \u2014 BEFORE saying you cannot do something. The tools come back callable from your next step, so ask and then act; never announce that you are loading them.
529
+
530
+ What this app can also do:
531
+ ${buildToolIndex(searchable)}`,
532
+ icon: "Wrench",
533
+ mode: "read",
534
+ category: "Core",
535
+ parameters: {
536
+ type: "object",
537
+ properties: {
538
+ tools: {
539
+ type: "array",
540
+ items: { type: "string" },
541
+ description: "Tool names from the list above. Ask for everything the task needs at once."
542
+ },
543
+ query: {
544
+ type: "string",
545
+ description: "What you are trying to do, in a few words. Use when no name above clearly matches."
546
+ }
547
+ }
548
+ }
549
+ }
550
+ ];
551
+ }
552
+ function words(text) {
553
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
554
+ }
555
+ function matches(tool, query) {
556
+ const terms = words(query);
557
+ if (!terms.length) return false;
558
+ const haystack = new Set(words(`${tool.name} ${areaOf(tool)} ${tool.description}`));
559
+ return terms.some((t) => haystack.has(t));
560
+ }
561
+ function resolveToolSearch(args, searchable, carried = /* @__PURE__ */ new Set()) {
562
+ const asked = Array.isArray(args.tools) ? args.tools.filter((n) => typeof n === "string") : [];
563
+ const query = typeof args.query === "string" ? args.query : "";
564
+ const byName = new Map(searchable.map((t) => [t.name.toLowerCase(), t]));
565
+ const found = /* @__PURE__ */ new Map();
566
+ for (const name of asked) {
567
+ const hit = byName.get(name.trim().toLowerCase());
568
+ if (hit) found.set(hit.name, hit);
569
+ }
570
+ if (!found.size && query) {
571
+ for (const tool of searchable.filter((t) => matches(t, query)).slice(0, 8)) found.set(tool.name, tool);
572
+ }
573
+ if (!found.size && asked.length) {
574
+ for (const tool of searchable.filter((t) => matches(t, asked.join(" "))).slice(0, 8)) found.set(tool.name, tool);
575
+ }
576
+ const tools = [...found.values()];
577
+ if (!tools.length) {
578
+ const byName2 = new Map([...carried].map((n) => [n.toLowerCase(), n]));
579
+ const held = asked.map((n) => byName2.get(n.trim().toLowerCase())).filter((n) => !!n);
580
+ if (held.length) {
581
+ return {
582
+ loaded: [],
583
+ note: `You are already carrying ${held.join(", ")} this turn \u2014 call ${held.length > 1 ? "them" : "it"} directly instead of loading anything.`
584
+ };
585
+ }
586
+ }
587
+ if (!tools.length) {
588
+ return {
589
+ loaded: [],
590
+ available: buildToolIndex(searchable),
591
+ note: "Nothing matched. Pick names from the list above, or tell the user this app has no tool for that."
592
+ };
593
+ }
594
+ loadTools(tools.map((t) => t.name));
595
+ return {
596
+ loaded: tools.map((t) => ({
597
+ name: t.name,
598
+ signature: formatToolSignature(t),
599
+ // Truncated: the full schema arrives with the tool itself on the next
600
+ // step, so repeating it here pays for the same text twice.
601
+ description: t.description.slice(0, 400)
602
+ })),
603
+ note: "Available from your next step. Continue the task now \u2014 do not tell the user about this step."
604
+ };
605
+ }
606
+
607
+ // src/lib/attachments.ts
608
+ var SAMPLE_ROWS = 5;
609
+ var SCAN_ROWS = 200;
610
+ var MAX_HEADERS = 50;
611
+ var MAX_CELL_CHARS = 120;
612
+ var MAX_SHEETS = 8;
613
+ var PREVIEW_CHARS = 2e3;
614
+ var FULL_READ_LIMIT = 8 * 1024 * 1024;
615
+ var HEAD_SLICE = 512 * 1024;
616
+ var registry = /* @__PURE__ */ new Map();
617
+ var seq = 0;
618
+ function registerAttachment(file) {
619
+ const id = `att-${Date.now().toString(36)}-${(seq++).toString(36)}`;
620
+ registry.set(id, { file });
621
+ return id;
622
+ }
623
+ function getAttachmentFile(id) {
624
+ return registry.get(id)?.file;
625
+ }
626
+ function releaseAttachment(id) {
627
+ registry.delete(id);
628
+ }
629
+ function clearAttachmentRegistry() {
630
+ registry.clear();
631
+ }
632
+ function resolveAttachmentDigest(name) {
633
+ const readable = [...registry.values()].map((e) => e.parsed).filter((d) => !!d?.headers?.length);
634
+ if (name?.trim()) {
635
+ const wanted = name.trim().toLowerCase();
636
+ return readable.find((d) => d.name.toLowerCase() === wanted);
637
+ }
638
+ return readable[readable.length - 1];
639
+ }
640
+ async function readAllRows(digest) {
641
+ const entry = [...registry.values()].find((e) => e.parsed === digest);
642
+ if (!entry) throw new Error("The file is no longer available in this conversation.");
643
+ if (digest.kind !== "csv") {
644
+ throw new Error("Only CSV files can be read in full here. Ask the user to save the sheet as CSV.");
645
+ }
646
+ const text = await entry.file.text();
647
+ const delimiter = detectDelimiter(text, /\.tsv$/i.test(entry.file.name) ? " " : void 0);
648
+ const { rows } = parseDelimited(text, delimiter);
649
+ if (!rows.length) return [];
650
+ const headers = rows[0].map((h) => h.trim());
651
+ return rows.slice(1).filter((row) => row.some((cell) => cell.trim())).map((row) => {
652
+ const record = {};
653
+ headers.forEach((header, i) => {
654
+ if (header) record[header] = (row[i] ?? "").trim();
655
+ });
656
+ return record;
657
+ });
658
+ }
659
+ function listAttachmentNames() {
660
+ return [...registry.values()].map((e) => e.parsed).filter((d) => !!d?.headers?.length).map((d) => d.name);
661
+ }
662
+ function nonTabularAttachmentNames() {
663
+ return [...registry.values()].map((e) => e.parsed).filter((d) => !!d && !d.headers?.length && !d.sheets?.length).map((d) => d.name);
664
+ }
665
+ var SPREADSHEET_EXT = /\.(xlsx|xlsm|xls)$/i;
666
+ var CSV_EXT = /\.(csv|tsv)$/i;
667
+ var TEXT_EXT = /\.(txt|md|markdown|json|log)$/i;
668
+ function detectKind(file) {
669
+ const name = file.name;
670
+ const mime = file.type || "";
671
+ if (SPREADSHEET_EXT.test(name) || mime.includes("spreadsheetml") || mime === "application/vnd.ms-excel") {
672
+ return "spreadsheet";
673
+ }
674
+ if (CSV_EXT.test(name) || mime === "text/csv" || mime === "text/tab-separated-values") return "csv";
675
+ if (mime.startsWith("image/")) return "image";
676
+ if (mime === "application/pdf" || /\.pdf$/i.test(name)) return "pdf";
677
+ if (TEXT_EXT.test(name) || mime.startsWith("text/") || mime === "application/json") return "text";
678
+ return "binary";
679
+ }
680
+ async function parseAttachment(file, id) {
681
+ const attachmentId = id ?? registerAttachment(file);
682
+ const base = {
683
+ id: attachmentId,
684
+ name: file.name,
685
+ mime: file.type || "application/octet-stream",
686
+ size: file.size,
687
+ kind: detectKind(file)
688
+ };
689
+ let digest = base;
690
+ switch (base.kind) {
691
+ case "csv":
692
+ digest = await parseDelimitedFile(file, base);
693
+ break;
694
+ case "spreadsheet":
695
+ digest = await parseSpreadsheetFile(file, base);
696
+ break;
697
+ case "text":
698
+ digest = await parseTextFile(file, base);
699
+ break;
700
+ }
701
+ const entry = registry.get(attachmentId);
702
+ if (entry) entry.parsed = digest;
703
+ return digest;
704
+ }
705
+ async function readTextBounded(file) {
706
+ if (file.size <= FULL_READ_LIMIT) return { text: await file.text(), partial: false };
707
+ return { text: await file.slice(0, HEAD_SLICE).text(), partial: true };
708
+ }
709
+ async function parseTextFile(file, base) {
710
+ const { text } = await readTextBounded(file);
711
+ const preview = text.slice(0, PREVIEW_CHARS);
712
+ return {
713
+ ...base,
714
+ preview,
715
+ ...text.length > PREVIEW_CHARS ? { warnings: [`preview truncated to ${PREVIEW_CHARS} characters`] } : {}
716
+ };
717
+ }
718
+ async function parseDelimitedFile(file, base) {
719
+ const { text, partial } = await readTextBounded(file);
720
+ const warnings = [];
721
+ if (partial) {
722
+ warnings.push("file too large \u2014 only the first rows were read, row count unknown");
723
+ }
724
+ const delimiter = detectDelimiter(text, /\.tsv$/i.test(file.name) ? " " : void 0);
725
+ const sheet = buildSheet(file.name.replace(CSV_EXT, ""), parseDelimited(text, delimiter), warnings);
726
+ if (partial) delete sheet.rowCount;
727
+ return {
728
+ ...base,
729
+ headers: sheet.headers,
730
+ ...sheet.rowCount === void 0 ? {} : { rowCount: sheet.rowCount },
731
+ sample: sheet.sample,
732
+ columnTypes: sheet.columnTypes,
733
+ ...warnings.length ? { warnings } : {}
734
+ };
735
+ }
736
+ var loadModule = (name) => new Function("m", "return import(m)")(name);
737
+ async function parseSpreadsheetFile(file, base) {
738
+ let XLSX;
739
+ try {
740
+ XLSX = await loadModule("xlsx");
741
+ } catch {
742
+ return {
743
+ ...base,
744
+ unreadable: "spreadsheet",
745
+ warnings: [
746
+ "Excel files are not read here. Ask the user to save the sheet as CSV (File > Save as > CSV) and attach it again \u2014 CSV is read in full."
747
+ ]
748
+ };
749
+ }
750
+ const warnings = [];
751
+ let book;
752
+ try {
753
+ book = XLSX.read(await file.arrayBuffer(), { type: "array" });
754
+ } catch {
755
+ return { ...base, warnings: ["could not read this spreadsheet \u2014 the file may be corrupted"] };
756
+ }
757
+ const names = book.SheetNames ?? [];
758
+ const sheets = [];
759
+ for (const name of names.slice(0, MAX_SHEETS)) {
760
+ const ws = book.Sheets?.[name];
761
+ if (!ws) continue;
762
+ const raw = XLSX.utils.sheet_to_json(ws, { header: 1, raw: false, defval: "" });
763
+ const rows = raw.slice(0, SCAN_ROWS).map((row) => row.map((cell) => String(cell ?? "")));
764
+ sheets.push(buildSheet(name, { rows, count: raw.length }, warnings));
765
+ }
766
+ if (names.length > MAX_SHEETS) {
767
+ warnings.push(`${names.length - MAX_SHEETS} additional sheet(s) not described`);
768
+ }
769
+ const only = sheets.length === 1 ? sheets[0] : void 0;
770
+ return {
771
+ ...base,
772
+ ...only ? {
773
+ headers: only.headers,
774
+ ...only.rowCount === void 0 ? {} : { rowCount: only.rowCount },
775
+ sample: only.sample,
776
+ columnTypes: only.columnTypes
777
+ } : { sheets },
778
+ ...warnings.length ? { warnings } : {}
779
+ };
780
+ }
781
+ function detectDelimiter(text, forced) {
782
+ if (forced) return forced;
783
+ const firstLine = text.slice(0, 4e3).split(/\r?\n/)[0] ?? "";
784
+ let best = ",";
785
+ let bestCount = 0;
786
+ for (const candidate of [",", ";", " ", "|"]) {
787
+ const count = countOutsideQuotes(firstLine, candidate);
788
+ if (count > bestCount) {
789
+ best = candidate;
790
+ bestCount = count;
791
+ }
792
+ }
793
+ return best;
794
+ }
795
+ function countOutsideQuotes(line, char) {
796
+ let inQuotes = false;
797
+ let count = 0;
798
+ for (let i = 0; i < line.length; i++) {
799
+ const c = line[i];
800
+ if (c === '"') inQuotes = !inQuotes;
801
+ else if (!inQuotes && c === char) count++;
802
+ }
803
+ return count;
804
+ }
805
+ function parseDelimited(text, delimiter) {
806
+ const rows = [];
807
+ let count = 0;
808
+ let row = [];
809
+ let field = "";
810
+ let inQuotes = false;
811
+ const endField = () => {
812
+ row.push(field);
813
+ field = "";
814
+ };
815
+ const endRow = () => {
816
+ endField();
817
+ count++;
818
+ if (rows.length < SCAN_ROWS) rows.push(row);
819
+ row = [];
820
+ };
821
+ const source = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
822
+ for (let i = 0; i < source.length; i++) {
823
+ const c = source[i];
824
+ if (inQuotes) {
825
+ if (c === '"') {
826
+ if (source[i + 1] === '"') {
827
+ field += '"';
828
+ i++;
829
+ } else inQuotes = false;
830
+ } else field += c;
831
+ continue;
832
+ }
833
+ if (c === '"' && field === "") inQuotes = true;
834
+ else if (c === delimiter) endField();
835
+ else if (c === "\n") endRow();
836
+ else if (c === "\r") continue;
837
+ else field += c;
838
+ }
839
+ if (field !== "" || row.length) endRow();
840
+ return { rows, count };
841
+ }
842
+ function buildSheet(name, parsed, warnings) {
843
+ const [rawHeader = [], ...body] = parsed.rows;
844
+ const headers = rawHeader.slice(0, MAX_HEADERS).map((h, i) => truncateCell(h).trim() || `col_${i + 1}`);
845
+ if (rawHeader.length > MAX_HEADERS) {
846
+ warnings.push(`${name}: ${rawHeader.length - MAX_HEADERS} column(s) beyond the first ${MAX_HEADERS} not described`);
847
+ }
848
+ const sample = body.slice(0, SAMPLE_ROWS).map((row) => headers.map((_, i) => truncateCell(row[i] ?? "")));
849
+ const columnTypes = headers.map(
850
+ (header, i) => inferColumn(header, body.map((row) => row[i] ?? ""), name, warnings)
851
+ );
852
+ return { name, headers, rowCount: Math.max(0, parsed.count - 1), sample, columnTypes };
853
+ }
854
+ function truncateCell(value) {
855
+ const text = String(value ?? "");
856
+ return text.length > MAX_CELL_CHARS ? `${text.slice(0, MAX_CELL_CHARS)}\u2026` : text;
857
+ }
858
+ var SLASH_DATE = /^(\d{1,2})[/.-](\d{1,2})[/.-](\d{2,4})$/;
859
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?)?/;
860
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
861
+ var MASKED_PHONE = /^\+?[\d\s().-]{8,20}$/;
862
+ var BOOLEANS = /* @__PURE__ */ new Set(["sim", "n\xE3o", "nao", "true", "false", "yes", "no", "verdadeiro", "falso"]);
863
+ function inferColumn(name, values, sheetName = "", warnings = []) {
864
+ const filled = values.slice(0, SCAN_ROWS).map((v) => String(v ?? "").trim()).filter((v) => v !== "");
865
+ if (!filled.length) return { name, type: "text" };
866
+ if (filled.every((v) => BOOLEANS.has(v.toLowerCase()))) return { name, type: "boolean" };
867
+ if (filled.every((v) => EMAIL.test(v))) return { name, type: "email" };
868
+ const dates = inferDates(filled);
869
+ if (dates) {
870
+ if (dates.ambiguous) {
871
+ const note = dates.reason === "conflict" ? "mixed dd/mm and mm/dd rows \u2014 read as text" : "day/month order cannot be told apart (all values \u2264 12) \u2014 read as text";
872
+ warnings.push(`${sheetName ? `${sheetName}: ` : ""}column "${name}": ${note}`);
873
+ return { name, type: "text", note };
874
+ }
875
+ return { name, type: "date", format: dates.format };
876
+ }
877
+ const identifier = inferIdentifier(name, filled);
878
+ if (identifier) return identifier;
879
+ const numbers = inferNumbers(filled);
880
+ if (numbers) {
881
+ return {
882
+ name,
883
+ type: "number",
884
+ ...numbers.format ? { format: numbers.format } : {},
885
+ ...numbers.note ? { note: numbers.note } : {}
886
+ };
887
+ }
888
+ if (filled.every((v) => MASKED_PHONE.test(v) && /[\s().+-]/.test(v) && digitsOf(v).length >= 8)) {
889
+ return { name, type: "phone" };
890
+ }
891
+ return { name, type: "text" };
892
+ }
893
+ function digitsOf(value) {
894
+ return value.replace(/\D/g, "");
895
+ }
896
+ function inferDates(values) {
897
+ if (values.every((v) => ISO_DATE.test(v))) return { format: "yyyy-mm-dd", ambiguous: false };
898
+ if (!values.every((v) => SLASH_DATE.test(v))) return null;
899
+ let dayFirst = false;
900
+ let monthFirst = false;
901
+ for (const value of values) {
902
+ const match = SLASH_DATE.exec(value);
903
+ const a = Number(match[1]);
904
+ const b = Number(match[2]);
905
+ if (a > 12 && b <= 12) dayFirst = true;
906
+ if (b > 12 && a <= 12) monthFirst = true;
907
+ if (a > 31 || b > 31) return null;
908
+ }
909
+ if (dayFirst && monthFirst) return { ambiguous: true, reason: "conflict" };
910
+ if (dayFirst) return { format: "dd/mm/yyyy", ambiguous: false };
911
+ if (monthFirst) return { format: "mm/dd/yyyy", ambiguous: false };
912
+ return { ambiguous: true, reason: "undecidable" };
913
+ }
914
+ var BR_DECIMAL = /^-?\d{1,3}(\.\d{3})*,\d+$|^-?\d+,\d+$/;
915
+ var US_DECIMAL = /^-?\d{1,3}(,\d{3})*\.\d+$|^-?\d+\.\d+$/;
916
+ var PLAIN_INT = /^-?\d+$/;
917
+ var DOTTED_GROUP = /^-?\d{1,3}(\.\d{3})+$/;
918
+ var PHONE_HEADER = /(celular|telefone|fone|whats|tel\b|mobile|phone)/i;
919
+ var DOCUMENT_HEADER = /(cpf|cnpj|\brg\b|documento|\bdoc\b|inscri|matr[ií]cula|identificador|c[oó]digo)/i;
920
+ var CEP_HEADER = /(cep|zip|postal)/i;
921
+ function inferIdentifier(name, values) {
922
+ const bareDigits = values.every((v) => /^\d+$/.test(v));
923
+ if (!bareDigits) return null;
924
+ const lengths = values.map((v) => v.length);
925
+ const hasLeadingZero = values.some((v) => v.length > 1 && v.startsWith("0"));
926
+ if (PHONE_HEADER.test(name) && lengths.every((n) => n >= 8 && n <= 15)) {
927
+ return { name, type: "phone", note: "digits only, no mask" };
928
+ }
929
+ if (DOCUMENT_HEADER.test(name) || CEP_HEADER.test(name)) {
930
+ return {
931
+ name,
932
+ type: "text",
933
+ note: "identifier, not a quantity \u2014 keep it as text so leading zeros survive"
934
+ };
935
+ }
936
+ if (hasLeadingZero) {
937
+ return { name, type: "text", note: "leading zeros \u2014 read as text, not a number" };
938
+ }
939
+ return null;
940
+ }
941
+ function inferNumbers(values) {
942
+ const stripped = values.map((v) => v.replace(/[R$€£%\s ]/g, ""));
943
+ let br = false;
944
+ let us = false;
945
+ let dotted = false;
946
+ for (const value of stripped) {
947
+ if (BR_DECIMAL.test(value)) br = true;
948
+ else if (DOTTED_GROUP.test(value)) dotted = true;
949
+ else if (US_DECIMAL.test(value)) us = true;
950
+ else if (!PLAIN_INT.test(value)) return null;
951
+ }
952
+ if (br && us) return { note: "mixed decimal separators (, and .)" };
953
+ if (br) return { format: "1.234,56" };
954
+ if (us) return { format: "1,234.56" };
955
+ if (dotted) return { note: "dots could be thousand separators or decimals" };
956
+ return {};
957
+ }
958
+ async function readAttachmentBytes(id) {
959
+ const file = getAttachmentFile(id);
960
+ if (!file) return null;
961
+ return toInlineFile(file);
962
+ }
963
+ var VISION_MAX_EDGE = 1568;
964
+ async function shrinkForVision(file, maxEdge = VISION_MAX_EDGE) {
965
+ if (!file.type.startsWith("image/")) return file;
966
+ if (typeof document === "undefined" || typeof createImageBitmap !== "function") return file;
967
+ try {
968
+ const bitmap = await createImageBitmap(file);
969
+ const longest = Math.max(bitmap.width, bitmap.height);
970
+ if (longest <= maxEdge) {
971
+ bitmap.close?.();
972
+ return file;
973
+ }
974
+ const scale = maxEdge / longest;
975
+ const canvas = document.createElement("canvas");
976
+ canvas.width = Math.round(bitmap.width * scale);
977
+ canvas.height = Math.round(bitmap.height * scale);
978
+ const ctx = canvas.getContext("2d");
979
+ if (!ctx) {
980
+ bitmap.close?.();
981
+ return file;
982
+ }
983
+ ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
984
+ bitmap.close?.();
985
+ const blob = await new Promise(
986
+ (resolve) => canvas.toBlob((b) => resolve(b), "image/jpeg", 0.85)
987
+ );
988
+ if (!blob || blob.size >= file.size) return file;
989
+ return new File([blob], file.name.replace(/\.[^.]+$/, "") + ".jpg", { type: "image/jpeg" });
990
+ } catch {
991
+ return file;
992
+ }
993
+ }
994
+ async function toInlineFile(file) {
995
+ return {
996
+ name: file.name,
997
+ mime: file.type || "application/octet-stream",
998
+ data: await toBase64(file)
999
+ };
1000
+ }
1001
+ async function toBase64(file) {
1002
+ const bytes = new Uint8Array(await file.arrayBuffer());
1003
+ const CHUNK = 32768;
1004
+ let binary = "";
1005
+ for (let i = 0; i < bytes.length; i += CHUNK) {
1006
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
1007
+ }
1008
+ return btoa(binary);
1009
+ }
1010
+ function formatBytes(bytes) {
1011
+ if (bytes < 1024) return `${bytes} B`;
1012
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
1013
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1014
+ }
1015
+ var STORAGE_PREFIX = "fayz.agent.auto-approve.v1";
1016
+ var DESTRUCTIVE = /(delete|destroy|remove|cancel|archive|refund|void)/i;
1017
+ function isDestructiveTool(toolName) {
1018
+ return DESTRUCTIVE.test(toolName);
1019
+ }
1020
+ function approvalKey(toolName, scope) {
1021
+ return scope ? `${toolName}:${scope}` : toolName;
1022
+ }
1023
+ function storageKey() {
1024
+ const orgId = useOrganizationStore.getState().currentOrg?.id;
1025
+ const userId = useAuthStore.getState().user?.id;
1026
+ if (!orgId || !userId) return null;
1027
+ return `${STORAGE_PREFIX}:${orgId}:${userId}`;
1028
+ }
1029
+ function read() {
1030
+ const key = storageKey();
1031
+ if (!key || typeof localStorage === "undefined") return [];
1032
+ try {
1033
+ const raw = localStorage.getItem(key);
1034
+ const parsed = raw ? JSON.parse(raw) : [];
1035
+ return Array.isArray(parsed) ? parsed.filter((r) => r && r.key) : [];
1036
+ } catch {
1037
+ return [];
1038
+ }
1039
+ }
1040
+ function write(rules) {
1041
+ const key = storageKey();
1042
+ if (!key || typeof localStorage === "undefined") return;
1043
+ try {
1044
+ localStorage.setItem(key, JSON.stringify(rules));
1045
+ } catch {
1046
+ }
1047
+ }
1048
+ var sessionGrants = /* @__PURE__ */ new Set();
1049
+ var listeners2 = /* @__PURE__ */ new Set();
1050
+ function notify() {
1051
+ for (const listener of Array.from(listeners2)) listener();
1052
+ }
1053
+ function subscribeAutoApprovals(listener) {
1054
+ listeners2.add(listener);
1055
+ return () => listeners2.delete(listener);
1056
+ }
1057
+ function listAutoApprovals() {
1058
+ return read();
1059
+ }
1060
+ function isAutoApproved(toolName, scope) {
1061
+ const key = approvalKey(toolName, scope);
1062
+ if (sessionGrants.has(key)) return true;
1063
+ if (isDestructiveTool(toolName)) return false;
1064
+ return read().some((rule) => rule.key === key);
1065
+ }
1066
+ function grantAutoApproval(input) {
1067
+ const key = approvalKey(input.toolName, input.scope);
1068
+ if (input.session || isDestructiveTool(input.toolName)) {
1069
+ sessionGrants.add(key);
1070
+ notify();
1071
+ return;
1072
+ }
1073
+ const rules = read().filter((rule) => rule.key !== key);
1074
+ rules.push({
1075
+ key,
1076
+ toolName: input.toolName,
1077
+ scope: input.scope,
1078
+ label: input.label,
1079
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1080
+ });
1081
+ write(rules);
1082
+ notify();
1083
+ }
1084
+ function revokeAutoApproval(key) {
1085
+ sessionGrants.delete(key);
1086
+ write(read().filter((rule) => rule.key !== key));
1087
+ notify();
1088
+ }
1089
+ function revokeAllAutoApprovals() {
1090
+ sessionGrants.clear();
1091
+ write([]);
1092
+ notify();
1093
+ }
1094
+
1095
+ // src/lib/record-refs.ts
1096
+ var MAX_ENTRIES = 300;
1097
+ var refs = /* @__PURE__ */ new Map();
1098
+ var LABEL_FIELDS = [
1099
+ "name",
1100
+ "full_name",
1101
+ "fullName",
1102
+ "title",
1103
+ "label",
1104
+ "client_name",
1105
+ "customer_name",
1106
+ "service_name",
1107
+ "display_name",
1108
+ "email"
1109
+ ];
1110
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1111
+ function looksLikeRecordId(value) {
1112
+ return typeof value === "string" && UUID.test(value);
1113
+ }
1114
+ function bestLabel(row, ref) {
1115
+ if (typeof ref.label === "string" && ref.label.trim()) return ref.label.trim();
1116
+ for (const field of LABEL_FIELDS) {
1117
+ const value = row[field];
1118
+ if (typeof value === "string" && value.trim()) return value.trim();
1119
+ }
1120
+ return null;
1121
+ }
1122
+ function remember(link) {
1123
+ const existing = refs.get(link.id);
1124
+ const merged = {
1125
+ ...link,
1126
+ ...!link.archetype && existing?.archetype ? { archetype: existing.archetype, ...existing.kind ? { kind: existing.kind } : {} } : {},
1127
+ ...!link.entityKey && existing?.entityKey ? { entityKey: existing.entityKey } : {}
1128
+ };
1129
+ refs.delete(link.id);
1130
+ refs.set(link.id, merged);
1131
+ while (refs.size > MAX_ENTRIES) {
1132
+ const oldest = refs.keys().next().value;
1133
+ if (oldest === void 0) break;
1134
+ refs.delete(oldest);
1135
+ }
1136
+ }
1137
+ function rememberRecordRefs(content) {
1138
+ let parsed;
1139
+ try {
1140
+ parsed = JSON.parse(content);
1141
+ } catch {
1142
+ return;
1143
+ }
1144
+ walk(parsed, 0);
1145
+ }
1146
+ function walk(node, depth) {
1147
+ if (depth > 6 || !node || typeof node !== "object") return;
1148
+ if (Array.isArray(node)) {
1149
+ for (const item of node) walk(item, depth + 1);
1150
+ return;
1151
+ }
1152
+ const row = node;
1153
+ const ref = row.ref;
1154
+ if (ref && typeof ref === "object" && !Array.isArray(ref)) {
1155
+ const r = ref;
1156
+ const id = typeof r.id === "string" ? r.id : null;
1157
+ if (id) {
1158
+ const [archetype, kind] = String(r.archetype ?? "").split(":");
1159
+ const label = bestLabel(row, r);
1160
+ if (label) {
1161
+ remember({
1162
+ id,
1163
+ label,
1164
+ ...archetype ? { archetype } : {},
1165
+ ...kind ? { kind } : {},
1166
+ ...typeof r.entityKey === "string" ? { entityKey: r.entityKey } : {}
1167
+ });
1168
+ }
1169
+ }
1170
+ } else if (looksLikeRecordId(row.id)) {
1171
+ const label = bestLabel(row, {});
1172
+ if (label) remember({ id: row.id, label });
1173
+ }
1174
+ for (const value of Object.values(row)) walk(value, depth + 1);
1175
+ }
1176
+ function rememberSurfaceRefs(snapshot) {
1177
+ if (!snapshot) return;
1178
+ const list = Array.isArray(snapshot) ? snapshot : [snapshot];
1179
+ for (const entry of list) {
1180
+ if (!entry || typeof entry !== "object") continue;
1181
+ const record = entry.record;
1182
+ if (record && typeof record === "object") {
1183
+ const r = record;
1184
+ if (looksLikeRecordId(r.id)) {
1185
+ const label = typeof r.label === "string" && r.label.trim() || bestLabel(r.summary ?? {}, {});
1186
+ if (label) {
1187
+ remember({
1188
+ id: r.id,
1189
+ label,
1190
+ ...typeof r.entity === "string" ? { entityKey: r.entity } : {}
1191
+ });
1192
+ }
1193
+ }
1194
+ }
1195
+ walk(entry, 0);
1196
+ }
1197
+ }
1198
+ function lookupRecordRef(id) {
1199
+ if (!looksLikeRecordId(id)) return null;
1200
+ return refs.get(id) ?? null;
1201
+ }
1202
+ function clearRecordRefs() {
1203
+ refs.clear();
1204
+ }
1205
+ function extractRecordLinks(content) {
1206
+ let parsed;
1207
+ try {
1208
+ parsed = JSON.parse(content);
1209
+ } catch {
1210
+ return [];
1211
+ }
1212
+ const links = [];
1213
+ const push = (ref, fallbackLabel) => {
1214
+ if (!ref || typeof ref !== "object") return;
1215
+ const r = ref;
1216
+ if (!r.id) return;
1217
+ const [archetype, kind] = (r.archetype ?? "").split(":");
1218
+ links.push({
1219
+ id: r.id,
1220
+ label: String(r.label ?? fallbackLabel ?? "registro"),
1221
+ ...archetype ? { archetype } : {},
1222
+ ...kind ? { kind } : {},
1223
+ ...r.entityKey ? { entityKey: r.entityKey } : {},
1224
+ // A destination the route map cannot derive (a settings tab) — the only
1225
+ // way a non-record result gets a working button.
1226
+ ...typeof r.path === "string" && r.path.startsWith("/") ? { path: r.path } : {}
1227
+ });
1228
+ };
1229
+ const record = parsed.record;
1230
+ push(parsed.ref, record?.name);
1231
+ push(record?.ref, record?.client_name ?? record?.service_name);
1232
+ const singleFrom = (rows) => {
1233
+ if (Array.isArray(rows) && rows.length === 1) {
1234
+ const row = rows[0];
1235
+ push(row.ref, row.name);
1236
+ }
1237
+ };
1238
+ singleFrom(parsed.records);
1239
+ for (const m of Array.isArray(parsed.matches) ? parsed.matches : []) {
1240
+ singleFrom(m.records);
1241
+ }
1242
+ return links;
1243
+ }
1244
+ function dedupeLinks(links) {
1245
+ const seen = /* @__PURE__ */ new Map();
1246
+ for (const l of links) if (!seen.has(l.id)) seen.set(l.id, l);
1247
+ return Array.from(seen.values()).slice(0, 3);
1248
+ }
1249
+
1250
+ // src/lib/agent-threads.ts
1251
+ var KEY = "fayz.agent.thread-roles";
1252
+ var CAP = 200;
1253
+ function read2() {
1254
+ if (typeof localStorage === "undefined") return {};
1255
+ try {
1256
+ const raw = localStorage.getItem(KEY);
1257
+ return raw ? JSON.parse(raw) : {};
1258
+ } catch {
1259
+ return {};
1260
+ }
1261
+ }
1262
+ function write2(map) {
1263
+ if (typeof localStorage === "undefined") return;
1264
+ try {
1265
+ const entries = Object.entries(map);
1266
+ const kept = entries.length > CAP ? entries.slice(entries.length - CAP) : entries;
1267
+ localStorage.setItem(KEY, JSON.stringify(Object.fromEntries(kept)));
1268
+ } catch {
1269
+ }
1270
+ }
1271
+ function tagConversation(conversationId, role) {
1272
+ const map = read2();
1273
+ if (map[conversationId] === role) return;
1274
+ map[conversationId] = role;
1275
+ write2(map);
1276
+ }
1277
+
1278
+ // src/lib/assistant-text.ts
1279
+ var TOOL_IN_BRACES = /\{\s*[a-z][A-Za-z0-9_]{2,}\s*(?:\([^()]*\))?\s*\}/g;
1280
+ var FAKE_BUTTON_LINE = /^[ \t]*\[[^\][\n]{2,60}\][ \t]*$(?!\()/gm;
1281
+ function mapOutsideCode(text, transform) {
1282
+ return text.split(/(```[\s\S]*?```|`[^`\n]*`)/g).map((chunk, index) => index % 2 === 1 ? chunk : transform(chunk)).join("");
1283
+ }
1284
+ function sanitizeAssistantText(text) {
1285
+ if (!text || !text.includes("{") && !text.includes("[")) return text;
1286
+ const cleaned = mapOutsideCode(
1287
+ text,
1288
+ (chunk) => chunk.replace(TOOL_IN_BRACES, "").replace(FAKE_BUTTON_LINE, "")
1289
+ );
1290
+ if (cleaned === text) return text;
1291
+ return cleaned.replace(/[ \t]+$/gm, "").replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
1292
+ }
1293
+
1294
+ // src/stores/chat.store.ts
1295
+ function chatCardKey(card) {
1296
+ if (card.kind === "connector") return `connector:${card.slug}`;
1297
+ return `choice:${card.id}`;
1298
+ }
1299
+ function revokePreview(url) {
1300
+ if (url) URL.revokeObjectURL(url);
1301
+ }
1302
+ function matchesAccept(file, accept) {
1303
+ if (!accept) return true;
1304
+ return accept.split(",").map((rule) => rule.trim().toLowerCase()).some((rule) => {
1305
+ if (!rule) return false;
1306
+ if (rule.startsWith(".")) return file.name.toLowerCase().endsWith(rule);
1307
+ if (rule.endsWith("/*")) return file.type.toLowerCase().startsWith(rule.slice(0, -1));
1308
+ return file.type.toLowerCase() === rule;
1309
+ });
1310
+ }
1311
+ var MAX_ATTACHMENTS = 5;
1312
+ var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
1313
+ var pendingQueue = [];
1314
+ function headState() {
1315
+ const head = pendingQueue[0];
1316
+ if (!head) return { pendingAction: null, pendingSameTool: 0 };
1317
+ return {
1318
+ pendingAction: head.action,
1319
+ // Siblings only. A queue of "create a product" + "delete a client" is two
1320
+ // decisions, and offering one button for both is how the wrong thing gets
1321
+ // approved by someone answering the card in front of them.
1322
+ pendingSameTool: pendingQueue.filter((e, i) => i > 0 && e.action.toolName === head.action.toolName).length
1323
+ };
1324
+ }
1325
+ function declineAllPending() {
1326
+ const queued = pendingQueue;
1327
+ pendingQueue = [];
1328
+ for (const entry of queued) entry.resolve({ approved: false });
1329
+ }
1330
+ var VOICE_REPLIES_KEY = "fayz.agent.voice-replies";
1331
+ function readVoiceRepliesPref() {
1332
+ if (typeof localStorage === "undefined") return false;
1333
+ try {
1334
+ return localStorage.getItem(VOICE_REPLIES_KEY) === "1";
1335
+ } catch {
1336
+ return false;
1337
+ }
1338
+ }
1339
+ var useChatStore = create((set, get) => ({
1340
+ messages: [],
1341
+ isOpen: false,
1342
+ isStreaming: false,
1343
+ conversationId: null,
1344
+ // Fechar o painel encerra o handoff junto: uma dica sobrevivente devolveria
1345
+ // "Ex.: receita por mesa" a quem abriu o assistente para outra coisa.
1346
+ setOpen: (open) => set(open ? { isOpen: true } : { isOpen: false, composerHint: null, composerFocusRequested: false }),
1347
+ toggleOpen: () => set((s) => ({ isOpen: !s.isOpen })),
1348
+ // Stamped with whoever is on duty, here rather than at each call site: every
1349
+ // message in the transcript needs a face, and a caller that forgets leaves an
1350
+ // anonymous bubble in the middle of a conversation with a named colleague.
1351
+ addMessage: (message) => set((s) => ({
1352
+ messages: [...s.messages, s.threadRole && !message.agentKey ? { ...message, agentKey: s.threadRole } : message]
1353
+ })),
1354
+ setMessageContent: (id, content) => set((s) => {
1355
+ const i = s.messages.findIndex((m) => m.id === id);
1356
+ if (i < 0) return s;
1357
+ const msgs = [...s.messages];
1358
+ msgs[i] = { ...msgs[i], content: sanitizeAssistantText(content) };
1359
+ return { messages: msgs };
1360
+ }),
1361
+ updateLastAssistant: (content) => set((s) => {
1362
+ const msgs = [...s.messages];
1363
+ const lastIdx = msgs.length - 1;
1364
+ if (lastIdx >= 0 && msgs[lastIdx].role === "assistant") {
1365
+ msgs[lastIdx] = { ...msgs[lastIdx], content: sanitizeAssistantText(content) };
1366
+ }
1367
+ return { messages: msgs };
1368
+ }),
1369
+ setStreaming: (isStreaming) => set({ isStreaming }),
1370
+ setConversationId: (conversationId) => {
1371
+ const role = get().threadRole;
1372
+ if (conversationId && role) tagConversation(conversationId, role);
1373
+ set({ conversationId });
1374
+ },
1375
+ threadRole: null,
1376
+ threads: {},
1377
+ switchThread: (role, seed) => {
1378
+ const s = get();
1379
+ if (s.threadRole === role && !seed) return;
1380
+ declineAllPending();
1381
+ clearAttachmentRegistry();
1382
+ clearTurnReportFiles();
1383
+ const threads = { ...s.threads };
1384
+ if (s.threadRole) {
1385
+ threads[s.threadRole] = { messages: s.messages, conversationId: s.conversationId };
1386
+ }
1387
+ const parked = threads[role] ?? { messages: [], conversationId: null };
1388
+ const target = !s.threadRole && s.messages.length ? {
1389
+ messages: [...s.messages, ...parked.messages],
1390
+ conversationId: s.conversationId ?? parked.conversationId
1391
+ } : parked;
1392
+ delete threads[role];
1393
+ set({
1394
+ threads,
1395
+ threadRole: role,
1396
+ messages: seed ? [...target.messages, seed] : target.messages,
1397
+ conversationId: target.conversationId,
1398
+ pendingAction: null,
1399
+ pendingSameTool: 0,
1400
+ activeTools: [],
1401
+ autoApprovedTools: [],
1402
+ attachments: []
1403
+ });
1404
+ },
1405
+ reset: () => {
1406
+ declineAllPending();
1407
+ clearRecordRefs();
1408
+ resetLoadedTools();
1409
+ clearAttachmentRegistry();
1410
+ clearTurnReportFiles();
1411
+ set({
1412
+ messages: [],
1413
+ isStreaming: false,
1414
+ conversationId: null,
1415
+ pendingAction: null,
1416
+ pendingSameTool: 0,
1417
+ activeTools: [],
1418
+ autoApprovedTools: [],
1419
+ attachments: []
1420
+ // suggestionsOpen is a standing preference, not thread state.
1421
+ });
1422
+ },
1423
+ pendingAction: null,
1424
+ pendingSameTool: 0,
1425
+ autoApprovedTools: [],
1426
+ requestConfirmation: (action) => {
1427
+ const destructive = action.destructive ?? isDestructiveTool(action.toolName);
1428
+ if (!action.noAutoApproval && isAutoApproved(action.toolName, action.scope)) {
1429
+ set((s) => ({
1430
+ autoApprovedTools: s.autoApprovedTools.includes(action.toolName) ? s.autoApprovedTools : [...s.autoApprovedTools, action.toolName]
1431
+ }));
1432
+ return Promise.resolve({ approved: true });
1433
+ }
1434
+ return new Promise((resolve) => {
1435
+ pendingQueue = [...pendingQueue, { action: { ...action, destructive }, resolve }];
1436
+ set(headState());
1437
+ });
1438
+ },
1439
+ resolvePendingAction: (approved, values) => {
1440
+ const [head, ...rest] = pendingQueue;
1441
+ if (!head) return;
1442
+ pendingQueue = rest;
1443
+ set(headState());
1444
+ head.resolve({ approved, values });
1445
+ },
1446
+ resolveAllPending: (approved) => {
1447
+ const head = pendingQueue[0];
1448
+ if (!head) return;
1449
+ const toolName = head.action.toolName;
1450
+ const matched = [];
1451
+ const kept = [];
1452
+ for (const entry of pendingQueue) {
1453
+ (entry.action.toolName === toolName ? matched : kept).push(entry);
1454
+ }
1455
+ pendingQueue = kept;
1456
+ set(headState());
1457
+ for (const entry of matched) entry.resolve({ approved });
1458
+ },
1459
+ voiceReplies: readVoiceRepliesPref(),
1460
+ setVoiceReplies: (enabled) => {
1461
+ try {
1462
+ localStorage?.setItem(VOICE_REPLIES_KEY, enabled ? "1" : "0");
1463
+ } catch {
1464
+ }
1465
+ set({ voiceReplies: enabled });
1466
+ },
1467
+ draft: "",
1468
+ setDraft: (draft) => set({ draft }),
1469
+ attachments: [],
1470
+ addAttachments: async (files) => {
1471
+ const room = MAX_ATTACHMENTS - get().attachments.length;
1472
+ if (room <= 0) return;
1473
+ const staged = files.slice(0, room).map((file) => ({
1474
+ file,
1475
+ attachment: {
1476
+ id: registerAttachment(file),
1477
+ name: file.name,
1478
+ size: file.size,
1479
+ mime: file.type || "application/octet-stream",
1480
+ kind: detectKind(file),
1481
+ // The picture IS the label for an image: "image.png · 1.3 MB" tells the
1482
+ // user nothing about which photo they just attached.
1483
+ ...file.type.startsWith("image/") ? { previewUrl: URL.createObjectURL(file) } : {},
1484
+ status: file.size > MAX_ATTACHMENT_BYTES ? "error" : "parsing",
1485
+ ...file.size > MAX_ATTACHMENT_BYTES ? { error: "tooLarge" } : {}
1486
+ }
1487
+ }));
1488
+ if (!staged.length) return;
1489
+ set((s) => ({ attachments: [...s.attachments, ...staged.map((e) => e.attachment)] }));
1490
+ const settle = (id, patch) => set((s) => ({
1491
+ attachments: s.attachments.map((a) => a.id === id ? { ...a, ...patch } : a)
1492
+ }));
1493
+ await Promise.all(
1494
+ staged.map(async ({ file, attachment }) => {
1495
+ if (attachment.status === "error") return;
1496
+ try {
1497
+ settle(attachment.id, { status: "ready", digest: await parseAttachment(file, attachment.id) });
1498
+ } catch {
1499
+ settle(attachment.id, { status: "error", error: "unreadable" });
1500
+ }
1501
+ })
1502
+ );
1503
+ },
1504
+ removeAttachment: (id) => {
1505
+ releaseAttachment(id);
1506
+ revokePreview(get().attachments.find((a) => a.id === id)?.previewUrl);
1507
+ set((s) => ({ attachments: s.attachments.filter((a) => a.id !== id) }));
1508
+ },
1509
+ clearAttachments: () => {
1510
+ for (const attachment of get().attachments) {
1511
+ releaseAttachment(attachment.id);
1512
+ revokePreview(attachment.previewUrl);
1513
+ }
1514
+ set({ attachments: [] });
1515
+ },
1516
+ // Sending clears the CHIPS, never the registry. The file is what the rest of
1517
+ // the conversation is about: released here, "ta certo isso ai" two turns later
1518
+ // reached proposeImport as "there is no readable file attached". The registry
1519
+ // is emptied when the thread is — reset() and setMessages() do it.
1520
+ consumePendingAttachments: () => {
1521
+ const staged = get().attachments;
1522
+ if (!staged.length) return [];
1523
+ const digests = staged.flatMap((a) => a.status === "ready" && a.digest ? [a.digest] : []);
1524
+ set({ attachments: [] });
1525
+ return digests;
1526
+ },
1527
+ composerMedia: null,
1528
+ attachMedia: (composerMedia) => set({ composerMedia }),
1529
+ clearComposerMedia: () => set({ composerMedia: null }),
1530
+ uploadRequest: null,
1531
+ requestUpload: (request) => {
1532
+ const id = `upload-${Date.now()}`;
1533
+ set({ uploadRequest: { ...request, id, status: "waiting", files: [] }, isOpen: true });
1534
+ return id;
1535
+ },
1536
+ addUploadFiles: (files) => {
1537
+ const request = get().uploadRequest;
1538
+ if (!request || files.length === 0) return;
1539
+ const staged = (request.multiple ? files : files.slice(0, 1)).map((file) => ({
1540
+ file,
1541
+ previewUrl: file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0
1542
+ }));
1543
+ const kept = request.multiple ? [...request.files, ...staged] : staged;
1544
+ if (!request.multiple) for (const previous of request.files) revokePreview(previous.previewUrl);
1545
+ set({ uploadRequest: { ...request, files: kept } });
1546
+ },
1547
+ removeUploadFile: (index) => {
1548
+ const request = get().uploadRequest;
1549
+ if (!request) return;
1550
+ revokePreview(request.files[index]?.previewUrl);
1551
+ set({ uploadRequest: { ...request, files: request.files.filter((_, i) => i !== index) } });
1552
+ },
1553
+ submitUpload: async () => {
1554
+ const request = get().uploadRequest;
1555
+ if (!request || request.files.length === 0) return;
1556
+ set({ uploadRequest: { ...request, status: "working" } });
1557
+ try {
1558
+ await request.onFiles(request.files.map((staged) => staged.file));
1559
+ } finally {
1560
+ for (const staged of request.files) revokePreview(staged.previewUrl);
1561
+ if (get().uploadRequest?.id === request.id) set({ uploadRequest: null });
1562
+ }
1563
+ },
1564
+ clearUploadRequest: () => {
1565
+ const request = get().uploadRequest;
1566
+ if (request) for (const staged of request.files) revokePreview(staged.previewUrl);
1567
+ set({ uploadRequest: null });
1568
+ },
1569
+ receiveFiles: (files) => {
1570
+ if (files.length === 0) return;
1571
+ const request = get().uploadRequest;
1572
+ if (request && request.status !== "working") {
1573
+ const wanted = files.filter((file) => matchesAccept(file, request.accept));
1574
+ if (wanted.length) {
1575
+ get().addUploadFiles(wanted);
1576
+ const rest = files.filter((file) => !wanted.includes(file));
1577
+ if (rest.length) void get().addAttachments(rest);
1578
+ return;
1579
+ }
1580
+ }
1581
+ void get().addAttachments(files);
1582
+ },
1583
+ queuedPrompt: null,
1584
+ queuedFilesToRead: null,
1585
+ queuePrompt: (queuedPrompt, filesToRead) => set({ queuedPrompt, queuedFilesToRead: filesToRead?.length ? filesToRead : null, isOpen: true }),
1586
+ consumeQueuedFilesToRead: () => {
1587
+ const files = get().queuedFilesToRead;
1588
+ if (files) set({ queuedFilesToRead: null });
1589
+ return files;
1590
+ },
1591
+ composerHint: null,
1592
+ setComposerHint: (composerHint) => set({ composerHint }),
1593
+ composerFocusRequested: false,
1594
+ requestComposerFocus: () => set({ composerFocusRequested: true }),
1595
+ consumeComposerFocus: () => set({ composerFocusRequested: false }),
1596
+ consumeQueuedPrompt: () => {
1597
+ const prompt = get().queuedPrompt;
1598
+ if (prompt) set({ queuedPrompt: null });
1599
+ return prompt;
1600
+ },
1601
+ // Open to begin with; collapsing sticks for the session.
1602
+ suggestionsOpen: true,
1603
+ setSuggestionsOpen: (suggestionsOpen) => set({ suggestionsOpen }),
1604
+ voiceStartRequested: false,
1605
+ requestVoiceStart: () => set({ isOpen: true, voiceStartRequested: true }),
1606
+ consumeVoiceStart: () => set({ voiceStartRequested: false }),
1607
+ activeTools: [],
1608
+ setActiveTools: (activeTools) => set({ activeTools }),
1609
+ appendToolCallsToLastAssistant: (calls) => set((s) => {
1610
+ if (!calls.length) return s;
1611
+ const msgs = [...s.messages];
1612
+ const lastIdx = msgs.length - 1;
1613
+ if (lastIdx >= 0 && msgs[lastIdx].role === "assistant") {
1614
+ msgs[lastIdx] = { ...msgs[lastIdx], toolCalls: [...msgs[lastIdx].toolCalls ?? [], ...calls] };
1615
+ }
1616
+ return { messages: msgs };
1617
+ }),
1618
+ appendLinksToLastAssistant: (links) => set((s) => {
1619
+ if (!links.length) return s;
1620
+ const msgs = [...s.messages];
1621
+ const lastIdx = msgs.length - 1;
1622
+ if (lastIdx >= 0 && msgs[lastIdx].role === "assistant") {
1623
+ const seen = new Map((msgs[lastIdx].links ?? []).map((l) => [l.id, l]));
1624
+ for (const l of links) if (!seen.has(l.id)) seen.set(l.id, l);
1625
+ msgs[lastIdx] = { ...msgs[lastIdx], links: Array.from(seen.values()).slice(0, 3) };
1626
+ }
1627
+ return { messages: msgs };
1628
+ }),
1629
+ appendCardsToLastAssistant: (cards) => set((s) => {
1630
+ if (!cards.length) return s;
1631
+ const msgs = [...s.messages];
1632
+ const lastIdx = msgs.length - 1;
1633
+ if (lastIdx >= 0 && msgs[lastIdx].role === "assistant") {
1634
+ const seen = new Map((msgs[lastIdx].cards ?? []).map((c) => [chatCardKey(c), c]));
1635
+ for (const c of cards) if (!seen.has(chatCardKey(c))) seen.set(chatCardKey(c), c);
1636
+ msgs[lastIdx] = { ...msgs[lastIdx], cards: Array.from(seen.values()).slice(0, 4) };
1637
+ }
1638
+ return { messages: msgs };
1639
+ }),
1640
+ conversations: [],
1641
+ conversationsLoaded: false,
1642
+ setConversations: (conversations) => set({ conversations, conversationsLoaded: true }),
1643
+ setMessages: (messages) => {
1644
+ clearAttachmentRegistry();
1645
+ set({ messages, attachments: [] });
1646
+ }
1647
+ }));
1648
+
1649
+ export { ALWAYS_ON_TOOLS, CORE_QUERY_ENTITIES, PROFILE_ACTIONS, buildDataPrimitiveTools, buildPermissionTools, buildToolSearchTool, chatCardKey, coreAITools, dedupeLinks, detectKind, entityToolName, extractRecordLinks, formatBytes, formatToolSignature, getAttachmentFile, grantAutoApproval, listAttachmentNames, listAutoApprovals, loadArea, loadTools, loadedToolNames, looksLikeRecordId, lookupRecordRef, nonTabularAttachmentNames, readAllRows, readAttachmentBytes, registryToolName, rememberRecordRefs, rememberSurfaceRefs, resetLoadedTools, resolveAttachmentDigest, resolveLoadedNames, resolveToolSearch, revokeAllAutoApprovals, revokeAutoApproval, searchableTools, setTurnReportFiles, shrinkForVision, subscribeAutoApprovals, takeTurnReportFiles, toInlineFile, useChatStore };
1650
+ //# sourceMappingURL=chunk-KLO7DKXX.js.map
1651
+ //# sourceMappingURL=chunk-KLO7DKXX.js.map