gufi-cli 0.1.25 → 0.1.27

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 (2) hide show
  1. package/dist/mcp.js +1257 -135
  2. package/package.json +1 -1
package/dist/mcp.js CHANGED
@@ -122,6 +122,204 @@ async function developerRequest(path, options = {}, retry = true) {
122
122
  return res.json();
123
123
  }
124
124
  // ════════════════════════════════════════════════════════════════════════════
125
+ // File Field Normalization
126
+ // ════════════════════════════════════════════════════════════════════════════
127
+ /**
128
+ * MIME types by extension
129
+ */
130
+ const MIME_TYPES = {
131
+ // Images
132
+ png: "image/png",
133
+ jpg: "image/jpeg",
134
+ jpeg: "image/jpeg",
135
+ gif: "image/gif",
136
+ webp: "image/webp",
137
+ svg: "image/svg+xml",
138
+ // Documents
139
+ pdf: "application/pdf",
140
+ doc: "application/msword",
141
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
142
+ xls: "application/vnd.ms-excel",
143
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
144
+ // Other
145
+ json: "application/json",
146
+ xml: "application/xml",
147
+ zip: "application/zip",
148
+ txt: "text/plain",
149
+ csv: "text/csv",
150
+ };
151
+ /**
152
+ * Get MIME type from filename extension
153
+ */
154
+ function getMimeFromFilename(filename) {
155
+ const ext = filename.split(".").pop()?.toLowerCase() || "";
156
+ return MIME_TYPES[ext] || "application/octet-stream";
157
+ }
158
+ /**
159
+ * Generate unique file ID
160
+ */
161
+ function generateFileId() {
162
+ return `file_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
163
+ }
164
+ /**
165
+ * Normalize a single file object - ensures all required fields are present
166
+ * Input: { url, name } (minimum required)
167
+ * Output: { id, url, name, mime, uploaded_at }
168
+ */
169
+ function normalizeFileObject(file) {
170
+ // Validate required fields
171
+ if (!file.url || typeof file.url !== "string") {
172
+ throw new Error("File field error: 'url' is required and must be a string");
173
+ }
174
+ if (!file.name || typeof file.name !== "string") {
175
+ throw new Error("File field error: 'name' is required and must be a string");
176
+ }
177
+ // Validate url format (must be company_X/... path or full URL)
178
+ const isValidPath = file.url.startsWith("company_") || file.url.startsWith("http");
179
+ if (!isValidPath) {
180
+ throw new Error(`File field error: 'url' must start with 'company_X/' or be a full URL. Got: ${file.url}`);
181
+ }
182
+ // Return normalized object with auto-generated fields
183
+ return {
184
+ id: file.id || generateFileId(),
185
+ url: file.url,
186
+ name: file.name,
187
+ mime: file.mime || getMimeFromFilename(file.name),
188
+ uploaded_at: file.uploaded_at || new Date().toISOString(),
189
+ };
190
+ }
191
+ /**
192
+ * Normalize file fields in data object
193
+ * Detects file fields (arrays with objects containing 'url') and normalizes them
194
+ */
195
+ function normalizeFileFields(data) {
196
+ if (!data || typeof data !== "object")
197
+ return data;
198
+ const normalized = { ...data };
199
+ for (const [key, value] of Object.entries(normalized)) {
200
+ // Check if it's a file field (array with objects containing 'url')
201
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
202
+ // Check if first item has 'url' property (indicates file field)
203
+ if ("url" in value[0]) {
204
+ normalized[key] = value.map(normalizeFileObject);
205
+ }
206
+ }
207
+ }
208
+ return normalized;
209
+ }
210
+ // ════════════════════════════════════════════════════════════════════════════
211
+ // Relation Field Validation
212
+ // ════════════════════════════════════════════════════════════════════════════
213
+ /**
214
+ * Extract entity ID from table name (e.g., "m617_t20413" → "20413")
215
+ */
216
+ function extractEntityIdFromTable(tableName) {
217
+ const match = tableName.match(/m\d+_t(\d+)/);
218
+ return match ? match[1] : null;
219
+ }
220
+ /**
221
+ * Cache for entity schemas to avoid repeated API calls
222
+ */
223
+ const entitySchemaCache = new Map();
224
+ const CACHE_TTL = 60000; // 1 minute
225
+ /**
226
+ * Get entity schema with caching
227
+ */
228
+ async function getEntitySchema(entityId, companyId) {
229
+ const cacheKey = `${companyId}:${entityId}`;
230
+ const cached = entitySchemaCache.get(cacheKey);
231
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
232
+ return cached.fields;
233
+ }
234
+ try {
235
+ const data = await apiRequest(`/api/schema/entities/${entityId}`, {
236
+ headers: { "X-Company-ID": companyId },
237
+ }, companyId);
238
+ const fields = data.fields || [];
239
+ entitySchemaCache.set(cacheKey, { fields, timestamp: Date.now() });
240
+ return fields;
241
+ }
242
+ catch {
243
+ return [];
244
+ }
245
+ }
246
+ /**
247
+ * Resolve a reference path (e.g., "ecommerce.clientes") to a table name
248
+ */
249
+ async function resolveRefToTable(ref, companyId) {
250
+ try {
251
+ const schemaData = await apiRequest("/api/company/schema", {
252
+ headers: { "X-Company-ID": companyId },
253
+ }, companyId);
254
+ const modules = schemaData.modules || schemaData.data?.modules || [];
255
+ const [moduleName, entityName] = ref.split(".");
256
+ for (const mod of modules) {
257
+ if (mod.name !== moduleName)
258
+ continue;
259
+ for (const sub of mod.submodules || []) {
260
+ for (const ent of sub.entities || []) {
261
+ if (ent.name === entityName) {
262
+ return ent.ui?.tableName || `m${ent.ui?.moduleId}_t${ent.ui?.entityId}`;
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ catch {
269
+ // Ignore errors, validation is best-effort
270
+ }
271
+ return null;
272
+ }
273
+ /**
274
+ * Validate relation fields in data - checks if referenced IDs exist
275
+ * Returns { valid: true } or { valid: false, errors: [...] }
276
+ */
277
+ async function validateRelationFields(tableName, companyId, data) {
278
+ const errors = [];
279
+ // Extract entity ID from table name
280
+ const entityId = extractEntityIdFromTable(tableName);
281
+ if (!entityId) {
282
+ return { valid: true, errors: [] }; // Can't validate without entity ID
283
+ }
284
+ // Get entity schema to find relation fields
285
+ const fields = await getEntitySchema(entityId, companyId);
286
+ if (!fields.length) {
287
+ return { valid: true, errors: [] }; // Can't validate without schema
288
+ }
289
+ // Find relation fields
290
+ const relationFields = fields.filter((f) => f.type === "relation" && f.settings?.ref);
291
+ for (const field of relationFields) {
292
+ const fieldName = field.name;
293
+ const value = data[fieldName];
294
+ // Skip if no value provided
295
+ if (value === undefined || value === null || value === "")
296
+ continue;
297
+ // Get the ID to validate
298
+ const refId = typeof value === "object" ? value.id : value;
299
+ if (!refId || typeof refId !== "number")
300
+ continue;
301
+ // Resolve the reference to a table name
302
+ const refTable = await resolveRefToTable(field.settings.ref, companyId);
303
+ if (!refTable)
304
+ continue;
305
+ // Check if the ID exists in the referenced table
306
+ try {
307
+ const result = await apiRequest(`/api/tables/${refTable}/getOne`, {
308
+ method: "POST",
309
+ body: JSON.stringify({ id: refId }),
310
+ }, companyId);
311
+ if (!result || !result.data) {
312
+ errors.push(`Relation '${fieldName}': ID ${refId} does not exist in ${field.settings.ref}`);
313
+ }
314
+ }
315
+ catch (e) {
316
+ // If we get an error, the ID probably doesn't exist
317
+ errors.push(`Relation '${fieldName}': ID ${refId} not found in ${field.settings.ref} (${e.message || 'not found'})`);
318
+ }
319
+ }
320
+ return { valid: errors.length === 0, errors };
321
+ }
322
+ // ════════════════════════════════════════════════════════════════════════════
125
323
  // Tool Definitions - Descriptions loaded from docs/mcp/tool-descriptions.json
126
324
  // ════════════════════════════════════════════════════════════════════════════
127
325
  // Load tool descriptions from centralized documentation
@@ -172,8 +370,35 @@ const TOOLS = [
172
370
  type: "object",
173
371
  properties: {
174
372
  company_id: { type: "string", description: "Company ID (optional if user has only one company)" },
175
- module_id: { type: "string", description: "Filter by specific module ID" },
373
+ format: { type: "string", description: "Output format: 'flat' (text, default) or 'json' (structured)" },
374
+ },
375
+ },
376
+ },
377
+ {
378
+ name: "gufi_schema_modify",
379
+ description: getDesc("gufi_schema_modify"),
380
+ inputSchema: {
381
+ type: "object",
382
+ properties: {
383
+ company_id: { type: "string", description: "Company ID (optional if user has only one company)" },
384
+ preview: { type: "boolean", description: "If true, show what would be done without executing (dry run)" },
385
+ operations: {
386
+ type: "array",
387
+ description: "Array of operations to execute",
388
+ items: {
389
+ type: "object",
390
+ properties: {
391
+ op: { type: "string", description: "Operation: add_field, update_field, remove_field, add_entity, update_entity, remove_entity" },
392
+ entity: { type: "string", description: "Entity reference: id, name, or 'module.entity'" },
393
+ module: { type: "string", description: "Module reference (for add_entity)" },
394
+ field: { type: "object", description: "Field definition (for add_field)" },
395
+ field_name: { type: "string", description: "Field name (for update_field, remove_field)" },
396
+ changes: { type: "object", description: "Changes to apply (for update_*)" },
397
+ },
398
+ },
399
+ },
176
400
  },
401
+ required: ["operations"],
177
402
  },
178
403
  },
179
404
  {
@@ -226,33 +451,47 @@ const TOOLS = [
226
451
  inputSchema: {
227
452
  type: "object",
228
453
  properties: {
229
- module_id: { type: "string", description: "Module ID (auto-detects company)" },
454
+ module_id: { type: "string", description: "Module ID" },
455
+ company_id: { type: "string", description: "Company ID (required - module IDs are local per company)" },
230
456
  },
231
- required: ["module_id"],
457
+ required: ["module_id", "company_id"],
232
458
  },
233
459
  },
234
460
  {
235
- name: "gufi_module_update",
236
- description: getDesc("gufi_module_update"),
461
+ name: "gufi_entity",
462
+ description: getDesc("gufi_entity"),
237
463
  inputSchema: {
238
464
  type: "object",
239
465
  properties: {
240
- module_id: { type: "string", description: "Module ID" },
241
- definition: { type: "object", description: "Full module JSON definition with name, displayName, icon, submodules array" },
466
+ entity_id: { type: "string", description: "Entity ID" },
467
+ company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
242
468
  },
243
- required: ["module_id", "definition"],
469
+ required: ["entity_id", "company_id"],
244
470
  },
245
471
  },
246
472
  {
247
- name: "gufi_module_create",
248
- description: getDesc("gufi_module_create"),
473
+ name: "gufi_field",
474
+ description: getDesc("gufi_field"),
249
475
  inputSchema: {
250
476
  type: "object",
251
477
  properties: {
252
- company_id: { type: "string", description: "Company ID" },
253
- definition: { type: "object", description: "Module JSON definition" },
478
+ entity: { type: "string", description: "Entity reference: id, name, or 'module.entity'" },
479
+ field: { type: "string", description: "Field name" },
480
+ company_id: { type: "string", description: "Company ID (required)" },
254
481
  },
255
- required: ["company_id", "definition"],
482
+ required: ["entity", "field", "company_id"],
483
+ },
484
+ },
485
+ {
486
+ name: "gufi_relations",
487
+ description: getDesc("gufi_relations"),
488
+ inputSchema: {
489
+ type: "object",
490
+ properties: {
491
+ module_id: { type: "string", description: "Module ID (optional - if omitted, shows all relations in company)" },
492
+ company_id: { type: "string", description: "Company ID (required)" },
493
+ },
494
+ required: ["company_id"],
256
495
  },
257
496
  },
258
497
  // ─────────────────────────────────────────────────────────────────────────
@@ -276,8 +515,9 @@ const TOOLS = [
276
515
  type: "object",
277
516
  properties: {
278
517
  automation_id: { type: "string", description: "Automation script ID" },
518
+ company_id: { type: "string", description: "Company ID (required - automation IDs are local per company)" },
279
519
  },
280
- required: ["automation_id"],
520
+ required: ["automation_id", "company_id"],
281
521
  },
282
522
  },
283
523
  {
@@ -301,8 +541,9 @@ const TOOLS = [
301
541
  type: "object",
302
542
  properties: {
303
543
  entity_id: { type: "string", description: "Entity ID (from module schema)" },
544
+ company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
304
545
  },
305
- required: ["entity_id"],
546
+ required: ["entity_id", "company_id"],
306
547
  },
307
548
  },
308
549
  {
@@ -312,12 +553,13 @@ const TOOLS = [
312
553
  type: "object",
313
554
  properties: {
314
555
  entity_id: { type: "string", description: "Entity ID" },
556
+ company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
315
557
  automations: {
316
558
  type: "object",
317
559
  description: "Automation configuration with on_create, on_update, on_delete, on_click arrays",
318
560
  },
319
561
  },
320
- required: ["entity_id", "automations"],
562
+ required: ["entity_id", "company_id", "automations"],
321
563
  },
322
564
  },
323
565
  {
@@ -336,21 +578,41 @@ const TOOLS = [
336
578
  // ─────────────────────────────────────────────────────────────────────────
337
579
  // Data (CRUD)
338
580
  // ─────────────────────────────────────────────────────────────────────────
581
+ {
582
+ name: "gufi_data",
583
+ description: getDesc("gufi_data"),
584
+ inputSchema: {
585
+ type: "object",
586
+ properties: {
587
+ action: { type: "string", description: "Action: list, get, create, update, delete" },
588
+ table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
589
+ company_id: { type: "string", description: "Company ID (required)" },
590
+ id: { type: "number", description: "Row ID (for get, update, delete)" },
591
+ data: { type: "object", description: "Row data (for create, update)" },
592
+ limit: { type: "number", description: "Number of rows for list (default 20)" },
593
+ offset: { type: "number", description: "Offset for pagination (default 0)" },
594
+ sort: { type: "string", description: "Field to sort by (default: id)" },
595
+ order: { type: "string", description: "ASC or DESC (default: DESC)" },
596
+ filter: { type: "string", description: "Filter expression (field=value)" },
597
+ },
598
+ required: ["action", "table", "company_id"],
599
+ },
600
+ },
339
601
  {
340
602
  name: "gufi_rows",
341
603
  description: getDesc("gufi_rows"),
342
604
  inputSchema: {
343
605
  type: "object",
344
606
  properties: {
345
- table: { type: "string", description: "Table name (physical ID like m360_t16192, NOT logical names)" },
346
- company_id: { type: "string", description: "Company ID (required if table is not physical ID format)" },
607
+ table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
608
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
347
609
  limit: { type: "number", description: "Number of rows (default 20)" },
348
610
  offset: { type: "number", description: "Offset for pagination (default 0)" },
349
611
  sort: { type: "string", description: "Field to sort by (default: id)" },
350
612
  order: { type: "string", description: "ASC or DESC (default: DESC)" },
351
613
  filter: { type: "string", description: "Filter expression (field=value)" },
352
614
  },
353
- required: ["table"],
615
+ required: ["table", "company_id"],
354
616
  },
355
617
  },
356
618
  {
@@ -360,10 +622,10 @@ const TOOLS = [
360
622
  type: "object",
361
623
  properties: {
362
624
  table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
363
- company_id: { type: "string", description: "Company ID (required if table is not physical ID format)" },
625
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
364
626
  id: { type: "number", description: "Row ID" },
365
627
  },
366
- required: ["table", "id"],
628
+ required: ["table", "company_id", "id"],
367
629
  },
368
630
  },
369
631
  {
@@ -373,10 +635,10 @@ const TOOLS = [
373
635
  type: "object",
374
636
  properties: {
375
637
  table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
376
- company_id: { type: "string", description: "Company ID (required if table is not physical ID format)" },
638
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
377
639
  data: { type: "object", description: "Row data as key-value pairs" },
378
640
  },
379
- required: ["table", "data"],
641
+ required: ["table", "company_id", "data"],
380
642
  },
381
643
  },
382
644
  {
@@ -386,11 +648,11 @@ const TOOLS = [
386
648
  type: "object",
387
649
  properties: {
388
650
  table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
389
- company_id: { type: "string", description: "Company ID (required if table is not physical ID format)" },
651
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
390
652
  id: { type: "number", description: "Row ID" },
391
653
  data: { type: "object", description: "Fields to update" },
392
654
  },
393
- required: ["table", "id", "data"],
655
+ required: ["table", "company_id", "id", "data"],
394
656
  },
395
657
  },
396
658
  {
@@ -400,10 +662,10 @@ const TOOLS = [
400
662
  type: "object",
401
663
  properties: {
402
664
  table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
403
- company_id: { type: "string", description: "Company ID (required if table is not physical ID format)" },
665
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
404
666
  id: { type: "number", description: "Row ID" },
405
667
  },
406
- required: ["table", "id"],
668
+ required: ["table", "company_id", "id"],
407
669
  },
408
670
  },
409
671
  // ─────────────────────────────────────────────────────────────────────────
@@ -427,8 +689,9 @@ const TOOLS = [
427
689
  properties: {
428
690
  key: { type: "string", description: "Variable name (uppercase recommended)" },
429
691
  value: { type: "string", description: "Variable value" },
692
+ company_id: { type: "string", description: "Company ID (required)" },
430
693
  },
431
- required: ["key", "value"],
694
+ required: ["key", "value", "company_id"],
432
695
  },
433
696
  },
434
697
  {
@@ -438,8 +701,9 @@ const TOOLS = [
438
701
  type: "object",
439
702
  properties: {
440
703
  key: { type: "string", description: "Variable name to delete" },
704
+ company_id: { type: "string", description: "Company ID (required)" },
441
705
  },
442
- required: ["key"],
706
+ required: ["key", "company_id"],
443
707
  },
444
708
  },
445
709
  // ─────────────────────────────────────────────────────────────────────────
@@ -503,6 +767,20 @@ const TOOLS = [
503
767
  required: ["view_id"],
504
768
  },
505
769
  },
770
+ {
771
+ name: "gufi_view_create",
772
+ description: getDesc("gufi_view_create"),
773
+ inputSchema: {
774
+ type: "object",
775
+ properties: {
776
+ name: { type: "string", description: "View name (e.g., 'Sales Dashboard')" },
777
+ template: { type: "string", description: "Template: 'dashboard', 'table', 'form', or 'blank'" },
778
+ package_id: { type: "string", description: "Package ID to add the view to (optional)" },
779
+ description: { type: "string", description: "View description (optional)" },
780
+ },
781
+ required: ["name", "template"],
782
+ },
783
+ },
506
784
  // ─────────────────────────────────────────────────────────────────────────
507
785
  // Packages (Marketplace Distribution)
508
786
  // ─────────────────────────────────────────────────────────────────────────
@@ -691,6 +969,22 @@ const TOOLS = [
691
969
  required: ["package_id"],
692
970
  },
693
971
  },
972
+ {
973
+ name: "gufi_link_view",
974
+ description: getDesc("gufi_link_view"),
975
+ inputSchema: {
976
+ type: "object",
977
+ properties: {
978
+ package_id: { type: "string", description: "Package ID" },
979
+ module_id: { type: "string", description: "Package module ID (from gufi_package)" },
980
+ submodule_name: { type: "string", description: "Submodule name to add the view to" },
981
+ view_id: { type: "number", description: "Marketplace view ID to link" },
982
+ label: { type: "string", description: "Display name for the view in the menu" },
983
+ icon: { type: "string", description: "Lucide icon name (e.g., 'BarChart3', 'Package')" },
984
+ },
985
+ required: ["package_id", "module_id", "submodule_name", "view_id", "label"],
986
+ },
987
+ },
694
988
  ];
695
989
  // ════════════════════════════════════════════════════════════════════════════
696
990
  // Tool Handlers
@@ -966,54 +1260,95 @@ const toolHandlers = {
966
1260
  }
967
1261
  },
968
1262
  async gufi_schema(params) {
969
- // Use /api/company/schema which has full entity info including IDs
970
- const data = await apiRequest("/api/company/schema", {
971
- headers: params.company_id ? { "X-Company-ID": params.company_id } : {},
972
- }, params.company_id);
973
- const modulesRaw = data.modules || data.data?.modules || [];
974
- // Build modules with proper entity IDs
975
- const modules = [];
976
- for (const mod of modulesRaw) {
977
- let moduleId;
978
- const entities = [];
979
- // First pass: find moduleId
980
- for (const sub of mod.submodules || []) {
981
- for (const ent of sub.entities || []) {
982
- if (ent.ui?.moduleId) {
983
- moduleId = ent.ui.moduleId;
984
- break;
1263
+ // Use the new /api/schema/readable endpoint for FLAT format (default)
1264
+ // This returns human-readable text optimized for Claude comprehension
1265
+ if (params.format === "json") {
1266
+ // JSON format for programmatic use
1267
+ const data = await apiRequest("/api/company/schema", {
1268
+ headers: params.company_id ? { "X-Company-ID": params.company_id } : {},
1269
+ }, params.company_id);
1270
+ const modulesRaw = data.modules || data.data?.modules || [];
1271
+ const modules = [];
1272
+ for (const mod of modulesRaw) {
1273
+ let moduleId;
1274
+ const entities = [];
1275
+ for (const sub of mod.submodules || []) {
1276
+ for (const ent of sub.entities || []) {
1277
+ if (ent.ui?.moduleId)
1278
+ moduleId = ent.ui.moduleId;
1279
+ const entityId = ent.ui?.entityId || ent.pk_id || ent.id;
1280
+ entities.push({
1281
+ id: entityId,
1282
+ name: ent.name,
1283
+ label: ent.label,
1284
+ table: moduleId && entityId ? `m${moduleId}_t${entityId}` : null,
1285
+ fields: (ent.fields || []).map((f) => ({
1286
+ name: f.name,
1287
+ type: f.type,
1288
+ label: f.label,
1289
+ required: f.required,
1290
+ })),
1291
+ });
985
1292
  }
986
1293
  }
987
- if (moduleId)
988
- break;
989
- }
990
- // Second pass: build entities with proper table names
991
- for (const sub of mod.submodules || []) {
992
- for (const ent of sub.entities || []) {
993
- const entityId = ent.ui?.entityId || ent.pk_id || ent.id;
994
- entities.push({
995
- id: entityId,
996
- name: ent.name,
997
- label: ent.label,
998
- table: moduleId && entityId ? `m${moduleId}_t${entityId}` : null,
999
- fields: (ent.fields || []).map((f) => ({
1000
- name: f.name,
1001
- type: f.type,
1002
- label: f.label,
1003
- })),
1004
- });
1005
- }
1006
- }
1007
- if (params.module_id && String(moduleId) !== params.module_id) {
1008
- continue;
1294
+ modules.push({
1295
+ id: moduleId,
1296
+ name: mod.name || mod.displayName,
1297
+ entities,
1298
+ });
1009
1299
  }
1010
- modules.push({
1011
- id: moduleId,
1012
- name: mod.name || mod.displayName,
1013
- entities,
1014
- });
1300
+ return { format: "json", modules };
1301
+ }
1302
+ // Default: FLAT text format optimized for Claude comprehension
1303
+ const url = `${getApiUrl()}/api/schema/readable`;
1304
+ let token = getToken();
1305
+ if (!token) {
1306
+ token = await autoLogin();
1307
+ if (!token)
1308
+ throw new Error("Not logged in. Run: gufi login");
1309
+ }
1310
+ const headers = {
1311
+ Authorization: `Bearer ${token}`,
1312
+ "X-Client": "mcp",
1313
+ };
1314
+ if (params.company_id) {
1315
+ headers["X-Company-ID"] = params.company_id;
1316
+ }
1317
+ const response = await fetch(url, { headers });
1318
+ if (!response.ok) {
1319
+ const text = await response.text();
1320
+ throw new Error(`API Error ${response.status}: ${text}`);
1015
1321
  }
1016
- return { modules };
1322
+ // Return as plain text for maximum readability
1323
+ const schema = await response.text();
1324
+ return { format: "flat", schema };
1325
+ },
1326
+ async gufi_schema_modify(params) {
1327
+ // Use the new /api/schema/operations endpoint for semantic operations
1328
+ const result = await apiRequest("/api/schema/operations", {
1329
+ method: "POST",
1330
+ body: JSON.stringify({
1331
+ operations: params.operations,
1332
+ preview: params.preview || false,
1333
+ }),
1334
+ }, params.company_id);
1335
+ if (params.preview) {
1336
+ return {
1337
+ preview: true,
1338
+ operations_count: params.operations.length,
1339
+ would_execute: result.preview || result.results?.map((r) => ({
1340
+ op: r.op,
1341
+ description: r.message,
1342
+ sql: r.sql || null,
1343
+ })),
1344
+ hint: "Set preview: false to execute these operations",
1345
+ };
1346
+ }
1347
+ return {
1348
+ success: result.success,
1349
+ results: result.results,
1350
+ summary: result.results?.map((r) => r.message).join("; "),
1351
+ };
1017
1352
  },
1018
1353
  async gufi_docs(params) {
1019
1354
  // Map topics to file names
@@ -1144,49 +1479,201 @@ const toolHandlers = {
1144
1479
  };
1145
1480
  },
1146
1481
  async gufi_module(params) {
1147
- const result = await detectCompanyFromModule(params.module_id);
1148
- if (!result) {
1149
- throw new Error(`Module ${params.module_id} not found`);
1482
+ // 💜 Use relational endpoint /api/schema/modules/:id
1483
+ const data = await apiRequest(`/api/schema/modules/${params.module_id}`, {
1484
+ headers: { "X-Company-ID": params.company_id },
1485
+ }, params.company_id);
1486
+ const { module, submodules, entities } = data;
1487
+ if (!module) {
1488
+ throw new Error(`Module ${params.module_id} not found in company ${params.company_id}`);
1150
1489
  }
1151
- const { module, companyId } = result;
1152
- // Clean JSON definition
1153
- const cleanSubmodules = (module.submodules || []).map((sub) => ({
1154
- ...sub,
1155
- entities: (sub.entities || []).map((ent) => {
1156
- const { permissions, ui, ...cleanEnt } = ent;
1157
- return cleanEnt;
1158
- }),
1159
- }));
1490
+ // Build submodules with entities (lightweight, no field definitions)
1491
+ const submodulesWithEntities = submodules.map((sub) => {
1492
+ const subEntities = entities.filter((e) => e.submodule_id === sub.id);
1493
+ return {
1494
+ id: sub.id,
1495
+ name: sub.name,
1496
+ label: sub.label,
1497
+ position: sub.position,
1498
+ entities: subEntities.map((e) => ({
1499
+ id: e.id,
1500
+ name: e.name,
1501
+ label: e.label,
1502
+ kind: e.kind,
1503
+ tableName: `m${module.id}_t${e.id}`,
1504
+ fieldCount: parseInt(e.field_count) || 0,
1505
+ // Include view_spec if present
1506
+ ...(e.view_spec && { view_spec: e.view_spec }),
1507
+ })),
1508
+ };
1509
+ });
1510
+ // Extract relations from entities for quick reference
1511
+ const relations = [];
1512
+ // Note: Full field details require gufi_entity call
1160
1513
  return {
1161
- module_id: module._moduleId,
1162
- company_id: companyId,
1163
- definition: {
1514
+ module_id: params.module_id,
1515
+ company_id: params.company_id,
1516
+ module: {
1517
+ id: module.id,
1164
1518
  name: module.name,
1165
- displayName: module.displayName || module.label,
1519
+ display_name: module.display_name,
1166
1520
  icon: module.icon,
1167
- submodules: cleanSubmodules,
1521
+ position: module.position,
1168
1522
  },
1523
+ submodules: submodulesWithEntities,
1524
+ entityCount: entities.length,
1525
+ hint: "Use gufi_entity({ entity_id, company_id }) to see fields and relations for a specific entity",
1169
1526
  };
1170
1527
  },
1171
- async gufi_module_update(params) {
1172
- const result = await detectCompanyFromModule(params.module_id);
1173
- if (!result) {
1174
- throw new Error(`Module ${params.module_id} not found`);
1528
+ async gufi_entity(params) {
1529
+ // 💜 Fetch entity with all fields from relational endpoint
1530
+ const data = await apiRequest(`/api/schema/entities/${params.entity_id}`, {
1531
+ headers: { "X-Company-ID": params.company_id },
1532
+ }, params.company_id);
1533
+ const { entity, fields } = data;
1534
+ if (!entity) {
1535
+ throw new Error(`Entity ${params.entity_id} not found in company ${params.company_id}`);
1175
1536
  }
1176
- const response = await apiRequest(`/api/company/modules/${params.module_id}`, {
1177
- method: "PUT",
1178
- body: JSON.stringify({ json: params.definition }),
1179
- headers: { "X-Company-ID": result.companyId },
1537
+ const tableName = `m${entity.module_id}_t${entity.id}`;
1538
+ // Extract relations for quick reference
1539
+ const relations = fields
1540
+ .filter((f) => f.type === "relation" && f.settings?.ref)
1541
+ .map((f) => ({
1542
+ field: f.name,
1543
+ ref: f.settings.ref,
1544
+ display: f.settings.display || "id",
1545
+ }));
1546
+ // Format fields for readability
1547
+ const formattedFields = fields.map((f) => {
1548
+ const constraints = [];
1549
+ if (f.required)
1550
+ constraints.push("required");
1551
+ if (f.settings?.unique)
1552
+ constraints.push("unique");
1553
+ if (f.settings?.ref)
1554
+ constraints.push(`→ ${f.settings.ref}`);
1555
+ if (f.settings?.options?.length > 0) {
1556
+ const opts = f.settings.options.slice(0, 5).map((o) => o.value || o);
1557
+ constraints.push(`[${opts.join("|")}${f.settings.options.length > 5 ? "..." : ""}]`);
1558
+ }
1559
+ return {
1560
+ name: f.name,
1561
+ type: f.type,
1562
+ label: f.label,
1563
+ ...(constraints.length > 0 && { constraints: constraints.join(", ") }),
1564
+ ...(f.settings && Object.keys(f.settings).length > 0 && { settings: f.settings }),
1565
+ };
1180
1566
  });
1181
- return { success: true, module_id: params.module_id };
1567
+ return {
1568
+ entity_id: params.entity_id,
1569
+ company_id: params.company_id,
1570
+ entity: {
1571
+ id: entity.id,
1572
+ name: entity.name,
1573
+ label: entity.label,
1574
+ kind: entity.kind,
1575
+ tableName,
1576
+ moduleName: entity.module_name,
1577
+ submoduleName: entity.submodule_name,
1578
+ ...(entity.view_spec && { view_spec: entity.view_spec }),
1579
+ },
1580
+ fields: formattedFields,
1581
+ relations,
1582
+ fieldCount: fields.length,
1583
+ hint: "Use gufi_schema_modify to add/update/remove fields",
1584
+ };
1182
1585
  },
1183
- async gufi_module_create(params) {
1184
- const response = await apiRequest("/api/company/modules", {
1185
- method: "POST",
1186
- body: JSON.stringify({ json: params.definition }),
1586
+ async gufi_field(params) {
1587
+ // 💜 Get a single field's full details
1588
+ // First resolve entity to ID if needed
1589
+ const schemaData = await apiRequest("/api/company/schema", {
1187
1590
  headers: { "X-Company-ID": params.company_id },
1188
- });
1189
- return { success: true, module: response.data || response };
1591
+ }, params.company_id);
1592
+ const modules = schemaData.modules || schemaData.data?.modules || [];
1593
+ let targetEntity = null;
1594
+ let targetField = null;
1595
+ // Search for entity by id, name, or module.entity path
1596
+ const entityRef = params.entity;
1597
+ const [modulePart, entityPart] = entityRef.includes(".") ? entityRef.split(".") : [null, entityRef];
1598
+ for (const mod of modules) {
1599
+ for (const sub of mod.submodules || []) {
1600
+ for (const ent of sub.entities || []) {
1601
+ const matchesId = String(ent.ui?.entityId || ent.id) === entityRef;
1602
+ const matchesName = ent.name === (entityPart || entityRef);
1603
+ const matchesPath = modulePart && mod.name === modulePart && ent.name === entityPart;
1604
+ if (matchesId || matchesName || matchesPath) {
1605
+ targetEntity = { ...ent, moduleName: mod.name, submoduleName: sub.name };
1606
+ targetField = (ent.fields || []).find((f) => f.name === params.field);
1607
+ break;
1608
+ }
1609
+ }
1610
+ if (targetEntity)
1611
+ break;
1612
+ }
1613
+ if (targetEntity)
1614
+ break;
1615
+ }
1616
+ if (!targetEntity) {
1617
+ throw new Error(`Entity "${params.entity}" not found in company ${params.company_id}`);
1618
+ }
1619
+ if (!targetField) {
1620
+ const availableFields = (targetEntity.fields || []).map((f) => f.name).slice(0, 10);
1621
+ throw new Error(`Field "${params.field}" not found in entity "${targetEntity.name}". Available: ${availableFields.join(", ")}${availableFields.length >= 10 ? "..." : ""}`);
1622
+ }
1623
+ return {
1624
+ entity: targetEntity.name,
1625
+ field: {
1626
+ name: targetField.name,
1627
+ type: targetField.type,
1628
+ label: targetField.label,
1629
+ required: targetField.required || false,
1630
+ settings: targetField.settings || {},
1631
+ },
1632
+ modify_hint: `gufi_schema_modify({ operations: [{ op: "update_field", entity: "${targetEntity.name}", field_name: "${params.field}", changes: { ... } }] })`,
1633
+ };
1634
+ },
1635
+ async gufi_relations(params) {
1636
+ // 💜 Get all relations in a module (or all modules if not specified)
1637
+ const schemaData = await apiRequest("/api/company/schema", {
1638
+ headers: { "X-Company-ID": params.company_id },
1639
+ }, params.company_id);
1640
+ const modules = schemaData.modules || schemaData.data?.modules || [];
1641
+ const relations = [];
1642
+ for (const mod of modules) {
1643
+ // Skip if module_id specified and doesn't match
1644
+ const moduleId = mod.submodules?.[0]?.entities?.[0]?.ui?.moduleId;
1645
+ if (params.module_id && String(moduleId) !== params.module_id)
1646
+ continue;
1647
+ for (const sub of mod.submodules || []) {
1648
+ for (const ent of sub.entities || []) {
1649
+ const entityPath = `${mod.name}.${ent.name}`;
1650
+ for (const field of ent.fields || []) {
1651
+ if (field.type === "relation" && field.settings?.ref) {
1652
+ relations.push({
1653
+ from: entityPath,
1654
+ field: field.name,
1655
+ to: field.settings.ref,
1656
+ display: field.settings.display || "id",
1657
+ });
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+ // Group by source entity for readability
1664
+ const grouped = {};
1665
+ for (const rel of relations) {
1666
+ if (!grouped[rel.from])
1667
+ grouped[rel.from] = [];
1668
+ grouped[rel.from].push({ field: rel.field, to: rel.to, display: rel.display });
1669
+ }
1670
+ return {
1671
+ company_id: params.company_id,
1672
+ ...(params.module_id && { module_id: params.module_id }),
1673
+ total_relations: relations.length,
1674
+ relations_by_entity: grouped,
1675
+ flat_list: relations,
1676
+ };
1190
1677
  },
1191
1678
  // ─────────────────────────────────────────────────────────────────────────
1192
1679
  // Automations
@@ -1205,16 +1692,21 @@ const toolHandlers = {
1205
1692
  };
1206
1693
  },
1207
1694
  async gufi_automation(params) {
1208
- const result = await detectCompanyFromAutomation(params.automation_id);
1209
- if (!result) {
1210
- throw new Error(`Automation ${params.automation_id} not found`);
1695
+ // 💜 Fetch automation directly from specified company (IDs are local per company)
1696
+ const data = await apiRequest("/api/automation-scripts", {
1697
+ headers: { "X-Company-ID": params.company_id },
1698
+ });
1699
+ const automations = Array.isArray(data) ? data : data.data || [];
1700
+ const found = automations.find((a) => String(a.id) === params.automation_id);
1701
+ if (!found) {
1702
+ throw new Error(`Automation ${params.automation_id} not found in company ${params.company_id}`);
1211
1703
  }
1212
1704
  return {
1213
- id: result.automation.id,
1214
- name: result.automation.name,
1215
- description: result.automation.description,
1216
- company_id: result.companyId,
1217
- code: result.automation.code || "",
1705
+ id: found.id,
1706
+ name: found.name,
1707
+ description: found.description,
1708
+ company_id: params.company_id,
1709
+ code: found.code || "",
1218
1710
  };
1219
1711
  },
1220
1712
  async gufi_automation_create(params) {
@@ -1230,16 +1722,24 @@ const toolHandlers = {
1230
1722
  return { success: true, automation: response.data || response };
1231
1723
  },
1232
1724
  async gufi_entity_automations(params) {
1233
- const response = await apiRequest(`/api/entities/${params.entity_id}/automations`);
1725
+ const response = await apiRequest(`/api/entities/${params.entity_id}/automations`, {
1726
+ headers: { "X-Company-ID": params.company_id },
1727
+ }, params.company_id);
1234
1728
  const data = response.data || response;
1235
- return { automations: data.automations || data || [] };
1729
+ return {
1730
+ entity_id: params.entity_id,
1731
+ company_id: params.company_id,
1732
+ automations: data.automations || data || [],
1733
+ availableScripts: data.availableScripts || [],
1734
+ };
1236
1735
  },
1237
1736
  async gufi_entity_automations_update(params) {
1238
1737
  const response = await apiRequest(`/api/entities/${params.entity_id}/automations`, {
1239
1738
  method: "PUT",
1240
1739
  body: JSON.stringify(params.automations),
1241
- });
1242
- return { success: true };
1740
+ headers: { "X-Company-ID": params.company_id },
1741
+ }, params.company_id);
1742
+ return { success: true, entity_id: params.entity_id, company_id: params.company_id };
1243
1743
  },
1244
1744
  async gufi_automations_executions(params) {
1245
1745
  let endpoint = `/api/automation-executions?limit=${params.limit || 20}`;
@@ -1254,8 +1754,82 @@ const toolHandlers = {
1254
1754
  // ─────────────────────────────────────────────────────────────────────────
1255
1755
  // Data (CRUD)
1256
1756
  // ─────────────────────────────────────────────────────────────────────────
1757
+ async gufi_data(params) {
1758
+ // 💜 Unified data tool - all CRUD operations in one
1759
+ const { action, table, company_id } = params;
1760
+ switch (action) {
1761
+ case "list": {
1762
+ const limit = params.limit || 20;
1763
+ const offset = params.offset || 0;
1764
+ const body = {
1765
+ pagination: { current: Math.floor(offset / limit) + 1, pageSize: limit },
1766
+ sorters: [{ field: params.sort || "id", order: params.order || "desc" }],
1767
+ filters: [],
1768
+ };
1769
+ if (params.filter) {
1770
+ const [field, value] = params.filter.split("=");
1771
+ if (field && value)
1772
+ body.filters.push({ field, operator: "eq", value });
1773
+ }
1774
+ const result = await apiRequest(`/api/tables/${table}/list`, {
1775
+ method: "POST",
1776
+ body: JSON.stringify(body),
1777
+ }, company_id);
1778
+ return { action: "list", table, total: result.total, rows: result.data || [] };
1779
+ }
1780
+ case "get": {
1781
+ if (!params.id)
1782
+ throw new Error("id is required for 'get' action");
1783
+ const result = await apiRequest(`/api/tables/${table}/getOne`, {
1784
+ method: "POST",
1785
+ body: JSON.stringify({ id: params.id }),
1786
+ }, company_id);
1787
+ return { action: "get", table, id: params.id, row: result.data || result };
1788
+ }
1789
+ case "create": {
1790
+ if (!params.data)
1791
+ throw new Error("data is required for 'create' action");
1792
+ // 💜 Validate relation fields before creating
1793
+ const createValidation = await validateRelationFields(table, company_id, params.data);
1794
+ if (!createValidation.valid) {
1795
+ throw new Error(`Relation validation failed:\n${createValidation.errors.join("\n")}`);
1796
+ }
1797
+ const result = await apiRequest(`/api/tables/${table}`, {
1798
+ method: "POST",
1799
+ body: JSON.stringify(params.data),
1800
+ }, company_id);
1801
+ return { action: "create", table, success: true, id: result.id || result.data?.id, row: result.data || result };
1802
+ }
1803
+ case "update": {
1804
+ if (!params.id)
1805
+ throw new Error("id is required for 'update' action");
1806
+ if (!params.data)
1807
+ throw new Error("data is required for 'update' action");
1808
+ // 💜 Validate relation fields before updating
1809
+ const updateValidation = await validateRelationFields(table, company_id, params.data);
1810
+ if (!updateValidation.valid) {
1811
+ throw new Error(`Relation validation failed:\n${updateValidation.errors.join("\n")}`);
1812
+ }
1813
+ const result = await apiRequest(`/api/tables/${table}/${params.id}`, {
1814
+ method: "PUT",
1815
+ body: JSON.stringify(params.data),
1816
+ }, company_id);
1817
+ return { action: "update", table, success: true, id: params.id, row: result.data || result };
1818
+ }
1819
+ case "delete": {
1820
+ if (!params.id)
1821
+ throw new Error("id is required for 'delete' action");
1822
+ await apiRequest(`/api/tables/${table}/${params.id}`, {
1823
+ method: "DELETE",
1824
+ }, company_id);
1825
+ return { action: "delete", table, success: true, id: params.id };
1826
+ }
1827
+ default:
1828
+ throw new Error(`Unknown action: ${action}. Use: list, get, create, update, delete`);
1829
+ }
1830
+ },
1257
1831
  async gufi_rows(params) {
1258
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1832
+ // 💜 company_id is now required - IDs are local per company
1259
1833
  const limit = params.limit || 20;
1260
1834
  const offset = params.offset || 0;
1261
1835
  const body = {
@@ -1272,51 +1846,64 @@ const toolHandlers = {
1272
1846
  const result = await apiRequest(`/api/tables/${params.table}/list`, {
1273
1847
  method: "POST",
1274
1848
  body: JSON.stringify(body),
1275
- }, companyId);
1849
+ }, params.company_id);
1276
1850
  return {
1277
1851
  table: params.table,
1852
+ company_id: params.company_id,
1278
1853
  total: result.total,
1279
1854
  rows: result.data || [],
1280
1855
  };
1281
1856
  },
1282
1857
  async gufi_row(params) {
1283
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1284
1858
  const result = await apiRequest(`/api/tables/${params.table}/getOne`, {
1285
1859
  method: "POST",
1286
1860
  body: JSON.stringify({ id: params.id }),
1287
- }, companyId);
1861
+ }, params.company_id);
1288
1862
  return {
1289
1863
  table: params.table,
1864
+ company_id: params.company_id,
1290
1865
  id: params.id,
1291
1866
  row: result.data || result,
1292
1867
  };
1293
1868
  },
1294
1869
  async gufi_row_create(params) {
1295
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1870
+ // Normalize file fields (auto-generate id, mime, uploaded_at)
1871
+ const normalizedData = normalizeFileFields(params.data);
1872
+ // 💜 Validate relation fields before creating
1873
+ const validation = await validateRelationFields(params.table, params.company_id, normalizedData);
1874
+ if (!validation.valid) {
1875
+ throw new Error(`Relation validation failed:\n${validation.errors.join("\n")}`);
1876
+ }
1296
1877
  const result = await apiRequest(`/api/tables/${params.table}`, {
1297
1878
  method: "POST",
1298
- body: JSON.stringify(params.data),
1299
- }, companyId);
1879
+ body: JSON.stringify(normalizedData),
1880
+ }, params.company_id);
1300
1881
  return {
1301
1882
  success: true,
1883
+ company_id: params.company_id,
1302
1884
  id: result.id || result.data?.id,
1303
1885
  row: result.data || result,
1304
1886
  };
1305
1887
  },
1306
1888
  async gufi_row_update(params) {
1307
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1889
+ // Normalize file fields (auto-generate id, mime, uploaded_at)
1890
+ const normalizedData = normalizeFileFields(params.data);
1891
+ // 💜 Validate relation fields before updating
1892
+ const validation = await validateRelationFields(params.table, params.company_id, normalizedData);
1893
+ if (!validation.valid) {
1894
+ throw new Error(`Relation validation failed:\n${validation.errors.join("\n")}`);
1895
+ }
1308
1896
  const result = await apiRequest(`/api/tables/${params.table}/${params.id}`, {
1309
1897
  method: "PUT",
1310
- body: JSON.stringify(params.data),
1311
- }, companyId);
1312
- return { success: true, row: result.data || result };
1898
+ body: JSON.stringify(normalizedData),
1899
+ }, params.company_id);
1900
+ return { success: true, company_id: params.company_id, row: result.data || result };
1313
1901
  },
1314
1902
  async gufi_row_delete(params) {
1315
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1316
1903
  await apiRequest(`/api/tables/${params.table}/${params.id}`, {
1317
1904
  method: "DELETE",
1318
- }, companyId);
1319
- return { success: true };
1905
+ }, params.company_id);
1906
+ return { success: true, company_id: params.company_id };
1320
1907
  },
1321
1908
  // ─────────────────────────────────────────────────────────────────────────
1322
1909
  // Environment Variables
@@ -1329,14 +1916,14 @@ const toolHandlers = {
1329
1916
  await apiRequest("/api/cli/env", {
1330
1917
  method: "POST",
1331
1918
  body: JSON.stringify({ key: params.key, value: params.value }),
1332
- });
1333
- return { success: true };
1919
+ }, params.company_id);
1920
+ return { success: true, company_id: params.company_id };
1334
1921
  },
1335
1922
  async gufi_env_delete(params) {
1336
1923
  await apiRequest(`/api/cli/env/${encodeURIComponent(params.key)}`, {
1337
1924
  method: "DELETE",
1338
- });
1339
- return { success: true };
1925
+ }, params.company_id);
1926
+ return { success: true, company_id: params.company_id };
1340
1927
  },
1341
1928
  // ─────────────────────────────────────────────────────────────────────────
1342
1929
  // Views
@@ -1392,6 +1979,516 @@ const toolHandlers = {
1392
1979
  : `❌ ${response.errors?.length || 0} errors, ${response.warnings?.length || 0} warnings`,
1393
1980
  };
1394
1981
  },
1982
+ async gufi_view_create(params) {
1983
+ // Generate safe function name
1984
+ const safeName = params.name.replace(/[^a-zA-Z0-9]/g, "");
1985
+ // Template generators
1986
+ const getBlankTemplate = (name, fnName) => ({
1987
+ index: `// ${name}
1988
+ import React from 'react';
1989
+ import { Package } from 'lucide-react';
1990
+ import { type FeatureProps } from '@/sdk';
1991
+
1992
+ export { featureConfig } from './core/dataProvider';
1993
+
1994
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
1995
+ const viewSpec = gufi?.context?.viewSpec || {};
1996
+
1997
+ return (
1998
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
1999
+ <header className="mb-6">
2000
+ <h1 className="text-xl font-bold flex items-center gap-2">
2001
+ <Package className="w-5 h-5 text-violet-600" />
2002
+ ${name}
2003
+ </h1>
2004
+ </header>
2005
+
2006
+ <div className="bg-white rounded-xl border shadow-sm p-6">
2007
+ <p className="text-gray-600">Start building your view here!</p>
2008
+ <p className="text-sm text-gray-400 mt-2">
2009
+ viewSpec.mainTable: {viewSpec.mainTable || 'Not configured'}
2010
+ </p>
2011
+ </div>
2012
+ </div>
2013
+ );
2014
+ }
2015
+ `,
2016
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
2017
+
2018
+ export const featureConfig: FeatureConfig = {
2019
+ dataSources: {
2020
+ mainTable: {
2021
+ type: 'table',
2022
+ module: 'default',
2023
+ entity: 'items',
2024
+ label: 'Main Table'
2025
+ }
2026
+ },
2027
+ inputs: {},
2028
+ seedData: {
2029
+ description: { es: 'Vista', en: 'View' },
2030
+ data: {},
2031
+ order: []
2032
+ }
2033
+ };
2034
+ `,
2035
+ });
2036
+ const getDashboardTemplate = (name, fnName) => ({
2037
+ index: `// ${name}
2038
+ import React, { useState, useEffect } from 'react';
2039
+ import { Package, TrendingUp, Users, DollarSign } from 'lucide-react';
2040
+ import { cn, toastError, type FeatureProps } from '@/sdk';
2041
+ import { Button, Card, CardContent, CardHeader, CardTitle } from '@/sdk';
2042
+
2043
+ export { featureConfig } from './core/dataProvider';
2044
+
2045
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
2046
+ const dataProvider = gufi?.dataProvider;
2047
+ const viewSpec = gufi?.context?.viewSpec || {};
2048
+ const isPreview = gufi?.context?.isPreview;
2049
+ const seedData = gufi?.seedData;
2050
+
2051
+ const [data, setData] = useState<any[]>([]);
2052
+ const [loading, setLoading] = useState(true);
2053
+ const [stats, setStats] = useState({ total: 0, active: 0, revenue: 0 });
2054
+
2055
+ useEffect(() => {
2056
+ const load = async () => {
2057
+ setLoading(true);
2058
+
2059
+ if (isPreview && seedData?.data) {
2060
+ const items = seedData.data.items || [];
2061
+ setData(items);
2062
+ setStats({
2063
+ total: items.length,
2064
+ active: items.filter((i: any) => i.status === 'active').length,
2065
+ revenue: items.reduce((acc: number, i: any) => acc + (i.amount || 0), 0)
2066
+ });
2067
+ setLoading(false);
2068
+ return;
2069
+ }
2070
+
2071
+ if (!dataProvider || !viewSpec.mainTable) {
2072
+ setLoading(false);
2073
+ return;
2074
+ }
2075
+
2076
+ try {
2077
+ const res = await dataProvider.getList({
2078
+ resource: viewSpec.mainTable,
2079
+ pagination: { current: 1, pageSize: 100 },
2080
+ });
2081
+ setData(res.data || []);
2082
+ } catch (err) {
2083
+ toastError('Error loading data');
2084
+ }
2085
+ setLoading(false);
2086
+ };
2087
+ load();
2088
+ }, [dataProvider, viewSpec.mainTable, seedData, isPreview]);
2089
+
2090
+ if (loading) {
2091
+ return (
2092
+ <div className="p-6 space-y-4">
2093
+ {[1,2,3].map(i => (
2094
+ <div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" />
2095
+ ))}
2096
+ </div>
2097
+ );
2098
+ }
2099
+
2100
+ return (
2101
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
2102
+ <header className="mb-6">
2103
+ <h1 className="text-2xl font-bold flex items-center gap-2">
2104
+ <Package className="w-6 h-6 text-violet-600" />
2105
+ ${name}
2106
+ </h1>
2107
+ </header>
2108
+
2109
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
2110
+ <Card>
2111
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
2112
+ <CardTitle className="text-sm font-medium text-gray-500">Total</CardTitle>
2113
+ <Users className="w-4 h-4 text-violet-500" />
2114
+ </CardHeader>
2115
+ <CardContent>
2116
+ <div className="text-2xl font-bold">{stats.total}</div>
2117
+ </CardContent>
2118
+ </Card>
2119
+ <Card>
2120
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
2121
+ <CardTitle className="text-sm font-medium text-gray-500">Active</CardTitle>
2122
+ <TrendingUp className="w-4 h-4 text-green-500" />
2123
+ </CardHeader>
2124
+ <CardContent>
2125
+ <div className="text-2xl font-bold text-green-600">{stats.active}</div>
2126
+ </CardContent>
2127
+ </Card>
2128
+ <Card>
2129
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
2130
+ <CardTitle className="text-sm font-medium text-gray-500">Revenue</CardTitle>
2131
+ <DollarSign className="w-4 h-4 text-violet-500" />
2132
+ </CardHeader>
2133
+ <CardContent>
2134
+ <div className="text-2xl font-bold">\${stats.revenue.toLocaleString()}</div>
2135
+ </CardContent>
2136
+ </Card>
2137
+ </div>
2138
+
2139
+ <Card className="flex-1">
2140
+ <CardHeader>
2141
+ <CardTitle>Records</CardTitle>
2142
+ </CardHeader>
2143
+ <CardContent>
2144
+ <div className="space-y-2">
2145
+ {data.slice(0, 10).map((item, i) => (
2146
+ <div key={item.id || i} className="p-3 bg-gray-50 rounded-lg flex justify-between">
2147
+ <span>{item.name || item.nombre || 'Item ' + (i + 1)}</span>
2148
+ <span className="text-gray-500">{item.status || '-'}</span>
2149
+ </div>
2150
+ ))}
2151
+ {data.length === 0 && (
2152
+ <div className="text-center text-gray-500 py-8">No data available</div>
2153
+ )}
2154
+ </div>
2155
+ </CardContent>
2156
+ </Card>
2157
+ </div>
2158
+ );
2159
+ }
2160
+ `,
2161
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
2162
+
2163
+ export const featureConfig: FeatureConfig = {
2164
+ dataSources: {
2165
+ mainTable: {
2166
+ type: 'table',
2167
+ module: 'default',
2168
+ entity: 'items',
2169
+ label: 'Main Data'
2170
+ }
2171
+ },
2172
+ inputs: {
2173
+ title: {
2174
+ type: 'text',
2175
+ label: 'Dashboard Title',
2176
+ default: '${name}'
2177
+ }
2178
+ },
2179
+ seedData: {
2180
+ description: {
2181
+ es: 'Dashboard de ejemplo',
2182
+ en: 'Example dashboard'
2183
+ },
2184
+ data: {
2185
+ items: [
2186
+ { id: 1, name: 'Item 1', status: 'active', amount: 1500 },
2187
+ { id: 2, name: 'Item 2', status: 'active', amount: 2300 },
2188
+ { id: 3, name: 'Item 3', status: 'pending', amount: 800 },
2189
+ ]
2190
+ },
2191
+ order: ['items']
2192
+ }
2193
+ };
2194
+ `,
2195
+ });
2196
+ const getTableTemplate = (name, fnName) => ({
2197
+ index: `// ${name}
2198
+ import React, { useState, useEffect } from 'react';
2199
+ import { TableIcon, Search } from 'lucide-react';
2200
+ import { cn, toastError, type FeatureProps } from '@/sdk';
2201
+ import { Input, Button } from '@/sdk';
2202
+
2203
+ export { featureConfig } from './core/dataProvider';
2204
+
2205
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
2206
+ const dataProvider = gufi?.dataProvider;
2207
+ const viewSpec = gufi?.context?.viewSpec || {};
2208
+ const isPreview = gufi?.context?.isPreview;
2209
+ const seedData = gufi?.seedData;
2210
+
2211
+ const [data, setData] = useState<any[]>([]);
2212
+ const [loading, setLoading] = useState(true);
2213
+ const [search, setSearch] = useState('');
2214
+
2215
+ useEffect(() => {
2216
+ const load = async () => {
2217
+ setLoading(true);
2218
+ if (isPreview && seedData?.data) {
2219
+ setData(seedData.data.items || []);
2220
+ setLoading(false);
2221
+ return;
2222
+ }
2223
+ if (!dataProvider || !viewSpec.mainTable) {
2224
+ setLoading(false);
2225
+ return;
2226
+ }
2227
+ try {
2228
+ const res = await dataProvider.getList({
2229
+ resource: viewSpec.mainTable,
2230
+ pagination: { current: 1, pageSize: 50 },
2231
+ });
2232
+ setData(res.data || []);
2233
+ } catch (err) {
2234
+ toastError('Error loading data');
2235
+ }
2236
+ setLoading(false);
2237
+ };
2238
+ load();
2239
+ }, [dataProvider, viewSpec.mainTable, seedData, isPreview]);
2240
+
2241
+ const filtered = data.filter(item =>
2242
+ !search || JSON.stringify(item).toLowerCase().includes(search.toLowerCase())
2243
+ );
2244
+
2245
+ if (loading) {
2246
+ return (
2247
+ <div className="p-6">
2248
+ <div className="h-10 bg-gray-100 rounded animate-pulse mb-4" />
2249
+ {[1,2,3,4,5].map(i => (
2250
+ <div key={i} className="h-12 bg-gray-50 rounded animate-pulse mb-2" />
2251
+ ))}
2252
+ </div>
2253
+ );
2254
+ }
2255
+
2256
+ return (
2257
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
2258
+ <header className="mb-4 flex items-center justify-between">
2259
+ <h1 className="text-xl font-bold flex items-center gap-2">
2260
+ <TableIcon className="w-5 h-5 text-violet-600" />
2261
+ ${name}
2262
+ </h1>
2263
+ <div className="relative">
2264
+ <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
2265
+ <Input
2266
+ className="pl-9 w-64"
2267
+ placeholder="Search..."
2268
+ value={search}
2269
+ onChange={(e) => setSearch(e.target.value)}
2270
+ />
2271
+ </div>
2272
+ </header>
2273
+
2274
+ <div className="bg-white rounded-xl border shadow-sm overflow-hidden">
2275
+ <table className="w-full">
2276
+ <thead className="bg-gray-50 border-b">
2277
+ <tr>
2278
+ <th className="text-left p-3 font-medium text-gray-600">ID</th>
2279
+ <th className="text-left p-3 font-medium text-gray-600">Name</th>
2280
+ <th className="text-left p-3 font-medium text-gray-600">Status</th>
2281
+ </tr>
2282
+ </thead>
2283
+ <tbody>
2284
+ {filtered.map((item, i) => (
2285
+ <tr key={item.id || i} className="border-b hover:bg-gray-50">
2286
+ <td className="p-3">{item.id}</td>
2287
+ <td className="p-3">{item.name || item.nombre || '-'}</td>
2288
+ <td className="p-3">{item.status || '-'}</td>
2289
+ </tr>
2290
+ ))}
2291
+ {filtered.length === 0 && (
2292
+ <tr>
2293
+ <td colSpan={3} className="p-8 text-center text-gray-500">No data found</td>
2294
+ </tr>
2295
+ )}
2296
+ </tbody>
2297
+ </table>
2298
+ </div>
2299
+ </div>
2300
+ );
2301
+ }
2302
+ `,
2303
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
2304
+
2305
+ export const featureConfig: FeatureConfig = {
2306
+ dataSources: {
2307
+ mainTable: {
2308
+ type: 'table',
2309
+ module: 'default',
2310
+ entity: 'items',
2311
+ label: 'Data Table'
2312
+ }
2313
+ },
2314
+ inputs: {},
2315
+ seedData: {
2316
+ description: { es: 'Tabla de datos', en: 'Data table' },
2317
+ data: {
2318
+ items: [
2319
+ { id: 1, name: 'Record 1', status: 'active' },
2320
+ { id: 2, name: 'Record 2', status: 'pending' },
2321
+ { id: 3, name: 'Record 3', status: 'completed' },
2322
+ ]
2323
+ },
2324
+ order: ['items']
2325
+ }
2326
+ };
2327
+ `,
2328
+ });
2329
+ const getFormTemplate = (name, fnName) => ({
2330
+ index: `// ${name}
2331
+ import React, { useState } from 'react';
2332
+ import { FileText, Save } from 'lucide-react';
2333
+ import { toastSuccess, toastError, type FeatureProps } from '@/sdk';
2334
+ import { Button, Input, Label } from '@/sdk';
2335
+
2336
+ export { featureConfig } from './core/dataProvider';
2337
+
2338
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
2339
+ const dataProvider = gufi?.dataProvider;
2340
+ const viewSpec = gufi?.context?.viewSpec || {};
2341
+ const CB = gufi?.CB;
2342
+
2343
+ const [formData, setFormData] = useState({ name: '', email: '', notes: '' });
2344
+ const [saving, setSaving] = useState(false);
2345
+
2346
+ const handleSubmit = async (e: React.FormEvent) => {
2347
+ e.preventDefault();
2348
+ if (!dataProvider || !viewSpec.mainTable || !CB) {
2349
+ toastError('Configuration error');
2350
+ return;
2351
+ }
2352
+ setSaving(true);
2353
+ try {
2354
+ await dataProvider.create({
2355
+ resource: viewSpec.mainTable,
2356
+ variables: {
2357
+ name: CB.text(formData.name),
2358
+ email: CB.text(formData.email),
2359
+ notes: CB.text(formData.notes),
2360
+ }
2361
+ });
2362
+ toastSuccess('Saved successfully!');
2363
+ setFormData({ name: '', email: '', notes: '' });
2364
+ } catch (err) {
2365
+ toastError('Error saving');
2366
+ }
2367
+ setSaving(false);
2368
+ };
2369
+
2370
+ return (
2371
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
2372
+ <header className="mb-6">
2373
+ <h1 className="text-xl font-bold flex items-center gap-2">
2374
+ <FileText className="w-5 h-5 text-violet-600" />
2375
+ ${name}
2376
+ </h1>
2377
+ </header>
2378
+
2379
+ <form onSubmit={handleSubmit} className="bg-white rounded-xl border shadow-sm p-6 max-w-lg space-y-4">
2380
+ <div>
2381
+ <Label htmlFor="name">Name</Label>
2382
+ <Input
2383
+ id="name"
2384
+ value={formData.name}
2385
+ onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
2386
+ placeholder="Enter name..."
2387
+ required
2388
+ />
2389
+ </div>
2390
+ <div>
2391
+ <Label htmlFor="email">Email</Label>
2392
+ <Input
2393
+ id="email"
2394
+ type="email"
2395
+ value={formData.email}
2396
+ onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
2397
+ placeholder="Enter email..."
2398
+ />
2399
+ </div>
2400
+ <div>
2401
+ <Label htmlFor="notes">Notes</Label>
2402
+ <Input
2403
+ id="notes"
2404
+ value={formData.notes}
2405
+ onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
2406
+ placeholder="Additional notes..."
2407
+ />
2408
+ </div>
2409
+ <Button type="submit" disabled={saving} className="w-full">
2410
+ <Save className="w-4 h-4 mr-2" />
2411
+ {saving ? 'Saving...' : 'Save'}
2412
+ </Button>
2413
+ </form>
2414
+ </div>
2415
+ );
2416
+ }
2417
+ `,
2418
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
2419
+
2420
+ export const featureConfig: FeatureConfig = {
2421
+ dataSources: {
2422
+ mainTable: {
2423
+ type: 'table',
2424
+ module: 'default',
2425
+ entity: 'submissions',
2426
+ label: 'Form Submissions'
2427
+ }
2428
+ },
2429
+ inputs: {},
2430
+ seedData: {
2431
+ description: { es: 'Formulario', en: 'Form' },
2432
+ data: {},
2433
+ order: []
2434
+ }
2435
+ };
2436
+ `,
2437
+ });
2438
+ // Get the right template
2439
+ let template;
2440
+ switch (params.template) {
2441
+ case "dashboard":
2442
+ template = getDashboardTemplate(params.name, safeName);
2443
+ break;
2444
+ case "table":
2445
+ template = getTableTemplate(params.name, safeName);
2446
+ break;
2447
+ case "form":
2448
+ template = getFormTemplate(params.name, safeName);
2449
+ break;
2450
+ default:
2451
+ template = getBlankTemplate(params.name, safeName);
2452
+ }
2453
+ // Need package_id to create a view
2454
+ if (!params.package_id) {
2455
+ return {
2456
+ error: "package_id is required to create a view",
2457
+ hint: "First create a package with gufi_package_create, then pass its ID",
2458
+ available_templates: ["dashboard", "table", "form", "blank"],
2459
+ };
2460
+ }
2461
+ // Create the view via API
2462
+ const response = await apiRequest(`/api/marketplace/packages/${params.package_id}/views`, {
2463
+ method: "POST",
2464
+ body: JSON.stringify({
2465
+ name: params.name,
2466
+ description: params.description || `${params.template} view created via MCP`,
2467
+ view_type: "custom",
2468
+ }),
2469
+ });
2470
+ const viewId = response.pk_id;
2471
+ // Now update with template files
2472
+ const files = [
2473
+ { file_path: "index.tsx", content: template.index, language: "typescript" },
2474
+ { file_path: "core/dataProvider.ts", content: template.dataProvider, language: "typescript" },
2475
+ ];
2476
+ await saveViewFiles(viewId, files);
2477
+ return {
2478
+ success: true,
2479
+ view_id: viewId,
2480
+ name: params.name,
2481
+ template: params.template,
2482
+ package_id: params.package_id,
2483
+ files_created: files.map(f => f.file_path),
2484
+ next_steps: [
2485
+ `1. Download locally: gufi view:pull ${viewId}`,
2486
+ `2. Edit files in: ~/gufi-dev/view_${viewId}/`,
2487
+ `3. Upload changes: gufi view:push`,
2488
+ `4. Publish: gufi package:sync ${params.package_id}`,
2489
+ ],
2490
+ };
2491
+ },
1395
2492
  // ─────────────────────────────────────────────────────────────────────────
1396
2493
  // Packages
1397
2494
  // ─────────────────────────────────────────────────────────────────────────
@@ -1544,6 +2641,31 @@ const toolHandlers = {
1544
2641
  const response = await developerRequest(`/packages/${params.package_id}/entities`);
1545
2642
  return { entities: response.data || [] };
1546
2643
  },
2644
+ async gufi_link_view(params) {
2645
+ const entity = {
2646
+ kind: "marketplace_view",
2647
+ marketplace_view_id: params.view_id,
2648
+ label: params.label,
2649
+ icon: params.icon || "Sparkles",
2650
+ };
2651
+ const response = await developerRequest(`/packages/${params.package_id}/modules/${params.module_id}/add-entity`, {
2652
+ method: "POST",
2653
+ body: JSON.stringify({
2654
+ submodule_name: params.submodule_name,
2655
+ entity,
2656
+ }),
2657
+ });
2658
+ // Invalidate schema cache on main backend
2659
+ try {
2660
+ await apiRequest("/api/company/schema/invalidate", { method: "POST" });
2661
+ }
2662
+ catch { }
2663
+ return {
2664
+ success: true,
2665
+ message: `View "${params.label}" linked to ${params.submodule_name}`,
2666
+ entity: response.data?.entity,
2667
+ };
2668
+ },
1547
2669
  };
1548
2670
  // ════════════════════════════════════════════════════════════════════════════
1549
2671
  // Context Generation Helpers