gufi-cli 0.1.25 → 0.1.26

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 +1033 -133
  2. package/package.json +1 -1
package/dist/mcp.js CHANGED
@@ -172,10 +172,37 @@ const TOOLS = [
172
172
  type: "object",
173
173
  properties: {
174
174
  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" },
175
+ format: { type: "string", description: "Output format: 'flat' (text, default) or 'json' (structured)" },
176
176
  },
177
177
  },
178
178
  },
179
+ {
180
+ name: "gufi_schema_modify",
181
+ description: getDesc("gufi_schema_modify"),
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ company_id: { type: "string", description: "Company ID (optional if user has only one company)" },
186
+ preview: { type: "boolean", description: "If true, show what would be done without executing (dry run)" },
187
+ operations: {
188
+ type: "array",
189
+ description: "Array of operations to execute",
190
+ items: {
191
+ type: "object",
192
+ properties: {
193
+ op: { type: "string", description: "Operation: add_field, update_field, remove_field, add_entity, update_entity, remove_entity" },
194
+ entity: { type: "string", description: "Entity reference: id, name, or 'module.entity'" },
195
+ module: { type: "string", description: "Module reference (for add_entity)" },
196
+ field: { type: "object", description: "Field definition (for add_field)" },
197
+ field_name: { type: "string", description: "Field name (for update_field, remove_field)" },
198
+ changes: { type: "object", description: "Changes to apply (for update_*)" },
199
+ },
200
+ },
201
+ },
202
+ },
203
+ required: ["operations"],
204
+ },
205
+ },
179
206
  {
180
207
  name: "gufi_docs",
181
208
  description: getDesc("gufi_docs"),
@@ -226,33 +253,47 @@ const TOOLS = [
226
253
  inputSchema: {
227
254
  type: "object",
228
255
  properties: {
229
- module_id: { type: "string", description: "Module ID (auto-detects company)" },
256
+ module_id: { type: "string", description: "Module ID" },
257
+ company_id: { type: "string", description: "Company ID (required - module IDs are local per company)" },
230
258
  },
231
- required: ["module_id"],
259
+ required: ["module_id", "company_id"],
232
260
  },
233
261
  },
234
262
  {
235
- name: "gufi_module_update",
236
- description: getDesc("gufi_module_update"),
263
+ name: "gufi_entity",
264
+ description: getDesc("gufi_entity"),
237
265
  inputSchema: {
238
266
  type: "object",
239
267
  properties: {
240
- module_id: { type: "string", description: "Module ID" },
241
- definition: { type: "object", description: "Full module JSON definition with name, displayName, icon, submodules array" },
268
+ entity_id: { type: "string", description: "Entity ID" },
269
+ company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
242
270
  },
243
- required: ["module_id", "definition"],
271
+ required: ["entity_id", "company_id"],
244
272
  },
245
273
  },
246
274
  {
247
- name: "gufi_module_create",
248
- description: getDesc("gufi_module_create"),
275
+ name: "gufi_field",
276
+ description: getDesc("gufi_field"),
249
277
  inputSchema: {
250
278
  type: "object",
251
279
  properties: {
252
- company_id: { type: "string", description: "Company ID" },
253
- definition: { type: "object", description: "Module JSON definition" },
280
+ entity: { type: "string", description: "Entity reference: id, name, or 'module.entity'" },
281
+ field: { type: "string", description: "Field name" },
282
+ company_id: { type: "string", description: "Company ID (required)" },
254
283
  },
255
- required: ["company_id", "definition"],
284
+ required: ["entity", "field", "company_id"],
285
+ },
286
+ },
287
+ {
288
+ name: "gufi_relations",
289
+ description: getDesc("gufi_relations"),
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ module_id: { type: "string", description: "Module ID (optional - if omitted, shows all relations in company)" },
294
+ company_id: { type: "string", description: "Company ID (required)" },
295
+ },
296
+ required: ["company_id"],
256
297
  },
257
298
  },
258
299
  // ─────────────────────────────────────────────────────────────────────────
@@ -276,8 +317,9 @@ const TOOLS = [
276
317
  type: "object",
277
318
  properties: {
278
319
  automation_id: { type: "string", description: "Automation script ID" },
320
+ company_id: { type: "string", description: "Company ID (required - automation IDs are local per company)" },
279
321
  },
280
- required: ["automation_id"],
322
+ required: ["automation_id", "company_id"],
281
323
  },
282
324
  },
283
325
  {
@@ -301,8 +343,9 @@ const TOOLS = [
301
343
  type: "object",
302
344
  properties: {
303
345
  entity_id: { type: "string", description: "Entity ID (from module schema)" },
346
+ company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
304
347
  },
305
- required: ["entity_id"],
348
+ required: ["entity_id", "company_id"],
306
349
  },
307
350
  },
308
351
  {
@@ -312,12 +355,13 @@ const TOOLS = [
312
355
  type: "object",
313
356
  properties: {
314
357
  entity_id: { type: "string", description: "Entity ID" },
358
+ company_id: { type: "string", description: "Company ID (required - entity IDs are local per company)" },
315
359
  automations: {
316
360
  type: "object",
317
361
  description: "Automation configuration with on_create, on_update, on_delete, on_click arrays",
318
362
  },
319
363
  },
320
- required: ["entity_id", "automations"],
364
+ required: ["entity_id", "company_id", "automations"],
321
365
  },
322
366
  },
323
367
  {
@@ -336,21 +380,41 @@ const TOOLS = [
336
380
  // ─────────────────────────────────────────────────────────────────────────
337
381
  // Data (CRUD)
338
382
  // ─────────────────────────────────────────────────────────────────────────
383
+ {
384
+ name: "gufi_data",
385
+ description: getDesc("gufi_data"),
386
+ inputSchema: {
387
+ type: "object",
388
+ properties: {
389
+ action: { type: "string", description: "Action: list, get, create, update, delete" },
390
+ table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
391
+ company_id: { type: "string", description: "Company ID (required)" },
392
+ id: { type: "number", description: "Row ID (for get, update, delete)" },
393
+ data: { type: "object", description: "Row data (for create, update)" },
394
+ limit: { type: "number", description: "Number of rows for list (default 20)" },
395
+ offset: { type: "number", description: "Offset for pagination (default 0)" },
396
+ sort: { type: "string", description: "Field to sort by (default: id)" },
397
+ order: { type: "string", description: "ASC or DESC (default: DESC)" },
398
+ filter: { type: "string", description: "Filter expression (field=value)" },
399
+ },
400
+ required: ["action", "table", "company_id"],
401
+ },
402
+ },
339
403
  {
340
404
  name: "gufi_rows",
341
405
  description: getDesc("gufi_rows"),
342
406
  inputSchema: {
343
407
  type: "object",
344
408
  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)" },
409
+ table: { type: "string", description: "Table name (physical ID like m360_t16192)" },
410
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
347
411
  limit: { type: "number", description: "Number of rows (default 20)" },
348
412
  offset: { type: "number", description: "Offset for pagination (default 0)" },
349
413
  sort: { type: "string", description: "Field to sort by (default: id)" },
350
414
  order: { type: "string", description: "ASC or DESC (default: DESC)" },
351
415
  filter: { type: "string", description: "Filter expression (field=value)" },
352
416
  },
353
- required: ["table"],
417
+ required: ["table", "company_id"],
354
418
  },
355
419
  },
356
420
  {
@@ -360,10 +424,10 @@ const TOOLS = [
360
424
  type: "object",
361
425
  properties: {
362
426
  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)" },
427
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
364
428
  id: { type: "number", description: "Row ID" },
365
429
  },
366
- required: ["table", "id"],
430
+ required: ["table", "company_id", "id"],
367
431
  },
368
432
  },
369
433
  {
@@ -373,10 +437,10 @@ const TOOLS = [
373
437
  type: "object",
374
438
  properties: {
375
439
  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)" },
440
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
377
441
  data: { type: "object", description: "Row data as key-value pairs" },
378
442
  },
379
- required: ["table", "data"],
443
+ required: ["table", "company_id", "data"],
380
444
  },
381
445
  },
382
446
  {
@@ -386,11 +450,11 @@ const TOOLS = [
386
450
  type: "object",
387
451
  properties: {
388
452
  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)" },
453
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
390
454
  id: { type: "number", description: "Row ID" },
391
455
  data: { type: "object", description: "Fields to update" },
392
456
  },
393
- required: ["table", "id", "data"],
457
+ required: ["table", "company_id", "id", "data"],
394
458
  },
395
459
  },
396
460
  {
@@ -400,10 +464,10 @@ const TOOLS = [
400
464
  type: "object",
401
465
  properties: {
402
466
  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)" },
467
+ company_id: { type: "string", description: "Company ID (required - table names may collide between companies)" },
404
468
  id: { type: "number", description: "Row ID" },
405
469
  },
406
- required: ["table", "id"],
470
+ required: ["table", "company_id", "id"],
407
471
  },
408
472
  },
409
473
  // ─────────────────────────────────────────────────────────────────────────
@@ -427,8 +491,9 @@ const TOOLS = [
427
491
  properties: {
428
492
  key: { type: "string", description: "Variable name (uppercase recommended)" },
429
493
  value: { type: "string", description: "Variable value" },
494
+ company_id: { type: "string", description: "Company ID (required)" },
430
495
  },
431
- required: ["key", "value"],
496
+ required: ["key", "value", "company_id"],
432
497
  },
433
498
  },
434
499
  {
@@ -438,8 +503,9 @@ const TOOLS = [
438
503
  type: "object",
439
504
  properties: {
440
505
  key: { type: "string", description: "Variable name to delete" },
506
+ company_id: { type: "string", description: "Company ID (required)" },
441
507
  },
442
- required: ["key"],
508
+ required: ["key", "company_id"],
443
509
  },
444
510
  },
445
511
  // ─────────────────────────────────────────────────────────────────────────
@@ -503,6 +569,20 @@ const TOOLS = [
503
569
  required: ["view_id"],
504
570
  },
505
571
  },
572
+ {
573
+ name: "gufi_view_create",
574
+ description: getDesc("gufi_view_create"),
575
+ inputSchema: {
576
+ type: "object",
577
+ properties: {
578
+ name: { type: "string", description: "View name (e.g., 'Sales Dashboard')" },
579
+ template: { type: "string", description: "Template: 'dashboard', 'table', 'form', or 'blank'" },
580
+ package_id: { type: "string", description: "Package ID to add the view to (optional)" },
581
+ description: { type: "string", description: "View description (optional)" },
582
+ },
583
+ required: ["name", "template"],
584
+ },
585
+ },
506
586
  // ─────────────────────────────────────────────────────────────────────────
507
587
  // Packages (Marketplace Distribution)
508
588
  // ─────────────────────────────────────────────────────────────────────────
@@ -691,6 +771,22 @@ const TOOLS = [
691
771
  required: ["package_id"],
692
772
  },
693
773
  },
774
+ {
775
+ name: "gufi_link_view",
776
+ description: getDesc("gufi_link_view"),
777
+ inputSchema: {
778
+ type: "object",
779
+ properties: {
780
+ package_id: { type: "string", description: "Package ID" },
781
+ module_id: { type: "string", description: "Package module ID (from gufi_package)" },
782
+ submodule_name: { type: "string", description: "Submodule name to add the view to" },
783
+ view_id: { type: "number", description: "Marketplace view ID to link" },
784
+ label: { type: "string", description: "Display name for the view in the menu" },
785
+ icon: { type: "string", description: "Lucide icon name (e.g., 'BarChart3', 'Package')" },
786
+ },
787
+ required: ["package_id", "module_id", "submodule_name", "view_id", "label"],
788
+ },
789
+ },
694
790
  ];
695
791
  // ════════════════════════════════════════════════════════════════════════════
696
792
  // Tool Handlers
@@ -966,54 +1062,95 @@ const toolHandlers = {
966
1062
  }
967
1063
  },
968
1064
  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;
1065
+ // Use the new /api/schema/readable endpoint for FLAT format (default)
1066
+ // This returns human-readable text optimized for Claude comprehension
1067
+ if (params.format === "json") {
1068
+ // JSON format for programmatic use
1069
+ const data = await apiRequest("/api/company/schema", {
1070
+ headers: params.company_id ? { "X-Company-ID": params.company_id } : {},
1071
+ }, params.company_id);
1072
+ const modulesRaw = data.modules || data.data?.modules || [];
1073
+ const modules = [];
1074
+ for (const mod of modulesRaw) {
1075
+ let moduleId;
1076
+ const entities = [];
1077
+ for (const sub of mod.submodules || []) {
1078
+ for (const ent of sub.entities || []) {
1079
+ if (ent.ui?.moduleId)
1080
+ moduleId = ent.ui.moduleId;
1081
+ const entityId = ent.ui?.entityId || ent.pk_id || ent.id;
1082
+ entities.push({
1083
+ id: entityId,
1084
+ name: ent.name,
1085
+ label: ent.label,
1086
+ table: moduleId && entityId ? `m${moduleId}_t${entityId}` : null,
1087
+ fields: (ent.fields || []).map((f) => ({
1088
+ name: f.name,
1089
+ type: f.type,
1090
+ label: f.label,
1091
+ required: f.required,
1092
+ })),
1093
+ });
985
1094
  }
986
1095
  }
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;
1096
+ modules.push({
1097
+ id: moduleId,
1098
+ name: mod.name || mod.displayName,
1099
+ entities,
1100
+ });
1009
1101
  }
1010
- modules.push({
1011
- id: moduleId,
1012
- name: mod.name || mod.displayName,
1013
- entities,
1014
- });
1102
+ return { format: "json", modules };
1103
+ }
1104
+ // Default: FLAT text format optimized for Claude comprehension
1105
+ const url = `${getApiUrl()}/api/schema/readable`;
1106
+ let token = getToken();
1107
+ if (!token) {
1108
+ token = await autoLogin();
1109
+ if (!token)
1110
+ throw new Error("Not logged in. Run: gufi login");
1111
+ }
1112
+ const headers = {
1113
+ Authorization: `Bearer ${token}`,
1114
+ "X-Client": "mcp",
1115
+ };
1116
+ if (params.company_id) {
1117
+ headers["X-Company-ID"] = params.company_id;
1015
1118
  }
1016
- return { modules };
1119
+ const response = await fetch(url, { headers });
1120
+ if (!response.ok) {
1121
+ const text = await response.text();
1122
+ throw new Error(`API Error ${response.status}: ${text}`);
1123
+ }
1124
+ // Return as plain text for maximum readability
1125
+ const schema = await response.text();
1126
+ return { format: "flat", schema };
1127
+ },
1128
+ async gufi_schema_modify(params) {
1129
+ // Use the new /api/schema/operations endpoint for semantic operations
1130
+ const result = await apiRequest("/api/schema/operations", {
1131
+ method: "POST",
1132
+ body: JSON.stringify({
1133
+ operations: params.operations,
1134
+ preview: params.preview || false,
1135
+ }),
1136
+ }, params.company_id);
1137
+ if (params.preview) {
1138
+ return {
1139
+ preview: true,
1140
+ operations_count: params.operations.length,
1141
+ would_execute: result.preview || result.results?.map((r) => ({
1142
+ op: r.op,
1143
+ description: r.message,
1144
+ sql: r.sql || null,
1145
+ })),
1146
+ hint: "Set preview: false to execute these operations",
1147
+ };
1148
+ }
1149
+ return {
1150
+ success: result.success,
1151
+ results: result.results,
1152
+ summary: result.results?.map((r) => r.message).join("; "),
1153
+ };
1017
1154
  },
1018
1155
  async gufi_docs(params) {
1019
1156
  // Map topics to file names
@@ -1144,49 +1281,201 @@ const toolHandlers = {
1144
1281
  };
1145
1282
  },
1146
1283
  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`);
1284
+ // 💜 Use relational endpoint /api/schema/modules/:id
1285
+ const data = await apiRequest(`/api/schema/modules/${params.module_id}`, {
1286
+ headers: { "X-Company-ID": params.company_id },
1287
+ }, params.company_id);
1288
+ const { module, submodules, entities } = data;
1289
+ if (!module) {
1290
+ throw new Error(`Module ${params.module_id} not found in company ${params.company_id}`);
1150
1291
  }
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
- }));
1292
+ // Build submodules with entities (lightweight, no field definitions)
1293
+ const submodulesWithEntities = submodules.map((sub) => {
1294
+ const subEntities = entities.filter((e) => e.submodule_id === sub.id);
1295
+ return {
1296
+ id: sub.id,
1297
+ name: sub.name,
1298
+ label: sub.label,
1299
+ position: sub.position,
1300
+ entities: subEntities.map((e) => ({
1301
+ id: e.id,
1302
+ name: e.name,
1303
+ label: e.label,
1304
+ kind: e.kind,
1305
+ tableName: `m${module.id}_t${e.id}`,
1306
+ fieldCount: parseInt(e.field_count) || 0,
1307
+ // Include view_spec if present
1308
+ ...(e.view_spec && { view_spec: e.view_spec }),
1309
+ })),
1310
+ };
1311
+ });
1312
+ // Extract relations from entities for quick reference
1313
+ const relations = [];
1314
+ // Note: Full field details require gufi_entity call
1160
1315
  return {
1161
- module_id: module._moduleId,
1162
- company_id: companyId,
1163
- definition: {
1316
+ module_id: params.module_id,
1317
+ company_id: params.company_id,
1318
+ module: {
1319
+ id: module.id,
1164
1320
  name: module.name,
1165
- displayName: module.displayName || module.label,
1321
+ display_name: module.display_name,
1166
1322
  icon: module.icon,
1167
- submodules: cleanSubmodules,
1323
+ position: module.position,
1168
1324
  },
1325
+ submodules: submodulesWithEntities,
1326
+ entityCount: entities.length,
1327
+ hint: "Use gufi_entity({ entity_id, company_id }) to see fields and relations for a specific entity",
1169
1328
  };
1170
1329
  },
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`);
1330
+ async gufi_entity(params) {
1331
+ // 💜 Fetch entity with all fields from relational endpoint
1332
+ const data = await apiRequest(`/api/schema/entities/${params.entity_id}`, {
1333
+ headers: { "X-Company-ID": params.company_id },
1334
+ }, params.company_id);
1335
+ const { entity, fields } = data;
1336
+ if (!entity) {
1337
+ throw new Error(`Entity ${params.entity_id} not found in company ${params.company_id}`);
1175
1338
  }
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 },
1339
+ const tableName = `m${entity.module_id}_t${entity.id}`;
1340
+ // Extract relations for quick reference
1341
+ const relations = fields
1342
+ .filter((f) => f.type === "relation" && f.settings?.ref)
1343
+ .map((f) => ({
1344
+ field: f.name,
1345
+ ref: f.settings.ref,
1346
+ display: f.settings.display || "id",
1347
+ }));
1348
+ // Format fields for readability
1349
+ const formattedFields = fields.map((f) => {
1350
+ const constraints = [];
1351
+ if (f.required)
1352
+ constraints.push("required");
1353
+ if (f.settings?.unique)
1354
+ constraints.push("unique");
1355
+ if (f.settings?.ref)
1356
+ constraints.push(`→ ${f.settings.ref}`);
1357
+ if (f.settings?.options?.length > 0) {
1358
+ const opts = f.settings.options.slice(0, 5).map((o) => o.value || o);
1359
+ constraints.push(`[${opts.join("|")}${f.settings.options.length > 5 ? "..." : ""}]`);
1360
+ }
1361
+ return {
1362
+ name: f.name,
1363
+ type: f.type,
1364
+ label: f.label,
1365
+ ...(constraints.length > 0 && { constraints: constraints.join(", ") }),
1366
+ ...(f.settings && Object.keys(f.settings).length > 0 && { settings: f.settings }),
1367
+ };
1180
1368
  });
1181
- return { success: true, module_id: params.module_id };
1369
+ return {
1370
+ entity_id: params.entity_id,
1371
+ company_id: params.company_id,
1372
+ entity: {
1373
+ id: entity.id,
1374
+ name: entity.name,
1375
+ label: entity.label,
1376
+ kind: entity.kind,
1377
+ tableName,
1378
+ moduleName: entity.module_name,
1379
+ submoduleName: entity.submodule_name,
1380
+ ...(entity.view_spec && { view_spec: entity.view_spec }),
1381
+ },
1382
+ fields: formattedFields,
1383
+ relations,
1384
+ fieldCount: fields.length,
1385
+ hint: "Use gufi_schema_modify to add/update/remove fields",
1386
+ };
1182
1387
  },
1183
- async gufi_module_create(params) {
1184
- const response = await apiRequest("/api/company/modules", {
1185
- method: "POST",
1186
- body: JSON.stringify({ json: params.definition }),
1388
+ async gufi_field(params) {
1389
+ // 💜 Get a single field's full details
1390
+ // First resolve entity to ID if needed
1391
+ const schemaData = await apiRequest("/api/company/schema", {
1187
1392
  headers: { "X-Company-ID": params.company_id },
1188
- });
1189
- return { success: true, module: response.data || response };
1393
+ }, params.company_id);
1394
+ const modules = schemaData.modules || schemaData.data?.modules || [];
1395
+ let targetEntity = null;
1396
+ let targetField = null;
1397
+ // Search for entity by id, name, or module.entity path
1398
+ const entityRef = params.entity;
1399
+ const [modulePart, entityPart] = entityRef.includes(".") ? entityRef.split(".") : [null, entityRef];
1400
+ for (const mod of modules) {
1401
+ for (const sub of mod.submodules || []) {
1402
+ for (const ent of sub.entities || []) {
1403
+ const matchesId = String(ent.ui?.entityId || ent.id) === entityRef;
1404
+ const matchesName = ent.name === (entityPart || entityRef);
1405
+ const matchesPath = modulePart && mod.name === modulePart && ent.name === entityPart;
1406
+ if (matchesId || matchesName || matchesPath) {
1407
+ targetEntity = { ...ent, moduleName: mod.name, submoduleName: sub.name };
1408
+ targetField = (ent.fields || []).find((f) => f.name === params.field);
1409
+ break;
1410
+ }
1411
+ }
1412
+ if (targetEntity)
1413
+ break;
1414
+ }
1415
+ if (targetEntity)
1416
+ break;
1417
+ }
1418
+ if (!targetEntity) {
1419
+ throw new Error(`Entity "${params.entity}" not found in company ${params.company_id}`);
1420
+ }
1421
+ if (!targetField) {
1422
+ const availableFields = (targetEntity.fields || []).map((f) => f.name).slice(0, 10);
1423
+ throw new Error(`Field "${params.field}" not found in entity "${targetEntity.name}". Available: ${availableFields.join(", ")}${availableFields.length >= 10 ? "..." : ""}`);
1424
+ }
1425
+ return {
1426
+ entity: targetEntity.name,
1427
+ field: {
1428
+ name: targetField.name,
1429
+ type: targetField.type,
1430
+ label: targetField.label,
1431
+ required: targetField.required || false,
1432
+ settings: targetField.settings || {},
1433
+ },
1434
+ modify_hint: `gufi_schema_modify({ operations: [{ op: "update_field", entity: "${targetEntity.name}", field_name: "${params.field}", changes: { ... } }] })`,
1435
+ };
1436
+ },
1437
+ async gufi_relations(params) {
1438
+ // 💜 Get all relations in a module (or all modules if not specified)
1439
+ const schemaData = await apiRequest("/api/company/schema", {
1440
+ headers: { "X-Company-ID": params.company_id },
1441
+ }, params.company_id);
1442
+ const modules = schemaData.modules || schemaData.data?.modules || [];
1443
+ const relations = [];
1444
+ for (const mod of modules) {
1445
+ // Skip if module_id specified and doesn't match
1446
+ const moduleId = mod.submodules?.[0]?.entities?.[0]?.ui?.moduleId;
1447
+ if (params.module_id && String(moduleId) !== params.module_id)
1448
+ continue;
1449
+ for (const sub of mod.submodules || []) {
1450
+ for (const ent of sub.entities || []) {
1451
+ const entityPath = `${mod.name}.${ent.name}`;
1452
+ for (const field of ent.fields || []) {
1453
+ if (field.type === "relation" && field.settings?.ref) {
1454
+ relations.push({
1455
+ from: entityPath,
1456
+ field: field.name,
1457
+ to: field.settings.ref,
1458
+ display: field.settings.display || "id",
1459
+ });
1460
+ }
1461
+ }
1462
+ }
1463
+ }
1464
+ }
1465
+ // Group by source entity for readability
1466
+ const grouped = {};
1467
+ for (const rel of relations) {
1468
+ if (!grouped[rel.from])
1469
+ grouped[rel.from] = [];
1470
+ grouped[rel.from].push({ field: rel.field, to: rel.to, display: rel.display });
1471
+ }
1472
+ return {
1473
+ company_id: params.company_id,
1474
+ ...(params.module_id && { module_id: params.module_id }),
1475
+ total_relations: relations.length,
1476
+ relations_by_entity: grouped,
1477
+ flat_list: relations,
1478
+ };
1190
1479
  },
1191
1480
  // ─────────────────────────────────────────────────────────────────────────
1192
1481
  // Automations
@@ -1205,16 +1494,21 @@ const toolHandlers = {
1205
1494
  };
1206
1495
  },
1207
1496
  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`);
1497
+ // 💜 Fetch automation directly from specified company (IDs are local per company)
1498
+ const data = await apiRequest("/api/automation-scripts", {
1499
+ headers: { "X-Company-ID": params.company_id },
1500
+ });
1501
+ const automations = Array.isArray(data) ? data : data.data || [];
1502
+ const found = automations.find((a) => String(a.id) === params.automation_id);
1503
+ if (!found) {
1504
+ throw new Error(`Automation ${params.automation_id} not found in company ${params.company_id}`);
1211
1505
  }
1212
1506
  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 || "",
1507
+ id: found.id,
1508
+ name: found.name,
1509
+ description: found.description,
1510
+ company_id: params.company_id,
1511
+ code: found.code || "",
1218
1512
  };
1219
1513
  },
1220
1514
  async gufi_automation_create(params) {
@@ -1230,16 +1524,24 @@ const toolHandlers = {
1230
1524
  return { success: true, automation: response.data || response };
1231
1525
  },
1232
1526
  async gufi_entity_automations(params) {
1233
- const response = await apiRequest(`/api/entities/${params.entity_id}/automations`);
1527
+ const response = await apiRequest(`/api/entities/${params.entity_id}/automations`, {
1528
+ headers: { "X-Company-ID": params.company_id },
1529
+ }, params.company_id);
1234
1530
  const data = response.data || response;
1235
- return { automations: data.automations || data || [] };
1531
+ return {
1532
+ entity_id: params.entity_id,
1533
+ company_id: params.company_id,
1534
+ automations: data.automations || data || [],
1535
+ availableScripts: data.availableScripts || [],
1536
+ };
1236
1537
  },
1237
1538
  async gufi_entity_automations_update(params) {
1238
1539
  const response = await apiRequest(`/api/entities/${params.entity_id}/automations`, {
1239
1540
  method: "PUT",
1240
1541
  body: JSON.stringify(params.automations),
1241
- });
1242
- return { success: true };
1542
+ headers: { "X-Company-ID": params.company_id },
1543
+ }, params.company_id);
1544
+ return { success: true, entity_id: params.entity_id, company_id: params.company_id };
1243
1545
  },
1244
1546
  async gufi_automations_executions(params) {
1245
1547
  let endpoint = `/api/automation-executions?limit=${params.limit || 20}`;
@@ -1254,8 +1556,72 @@ const toolHandlers = {
1254
1556
  // ─────────────────────────────────────────────────────────────────────────
1255
1557
  // Data (CRUD)
1256
1558
  // ─────────────────────────────────────────────────────────────────────────
1559
+ async gufi_data(params) {
1560
+ // 💜 Unified data tool - all CRUD operations in one
1561
+ const { action, table, company_id } = params;
1562
+ switch (action) {
1563
+ case "list": {
1564
+ const limit = params.limit || 20;
1565
+ const offset = params.offset || 0;
1566
+ const body = {
1567
+ pagination: { current: Math.floor(offset / limit) + 1, pageSize: limit },
1568
+ sorters: [{ field: params.sort || "id", order: params.order || "desc" }],
1569
+ filters: [],
1570
+ };
1571
+ if (params.filter) {
1572
+ const [field, value] = params.filter.split("=");
1573
+ if (field && value)
1574
+ body.filters.push({ field, operator: "eq", value });
1575
+ }
1576
+ const result = await apiRequest(`/api/tables/${table}/list`, {
1577
+ method: "POST",
1578
+ body: JSON.stringify(body),
1579
+ }, company_id);
1580
+ return { action: "list", table, total: result.total, rows: result.data || [] };
1581
+ }
1582
+ case "get": {
1583
+ if (!params.id)
1584
+ throw new Error("id is required for 'get' action");
1585
+ const result = await apiRequest(`/api/tables/${table}/getOne`, {
1586
+ method: "POST",
1587
+ body: JSON.stringify({ id: params.id }),
1588
+ }, company_id);
1589
+ return { action: "get", table, id: params.id, row: result.data || result };
1590
+ }
1591
+ case "create": {
1592
+ if (!params.data)
1593
+ throw new Error("data is required for 'create' action");
1594
+ const result = await apiRequest(`/api/tables/${table}`, {
1595
+ method: "POST",
1596
+ body: JSON.stringify(params.data),
1597
+ }, company_id);
1598
+ return { action: "create", table, success: true, id: result.id || result.data?.id, row: result.data || result };
1599
+ }
1600
+ case "update": {
1601
+ if (!params.id)
1602
+ throw new Error("id is required for 'update' action");
1603
+ if (!params.data)
1604
+ throw new Error("data is required for 'update' action");
1605
+ const result = await apiRequest(`/api/tables/${table}/${params.id}`, {
1606
+ method: "PUT",
1607
+ body: JSON.stringify(params.data),
1608
+ }, company_id);
1609
+ return { action: "update", table, success: true, id: params.id, row: result.data || result };
1610
+ }
1611
+ case "delete": {
1612
+ if (!params.id)
1613
+ throw new Error("id is required for 'delete' action");
1614
+ await apiRequest(`/api/tables/${table}/${params.id}`, {
1615
+ method: "DELETE",
1616
+ }, company_id);
1617
+ return { action: "delete", table, success: true, id: params.id };
1618
+ }
1619
+ default:
1620
+ throw new Error(`Unknown action: ${action}. Use: list, get, create, update, delete`);
1621
+ }
1622
+ },
1257
1623
  async gufi_rows(params) {
1258
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1624
+ // 💜 company_id is now required - IDs are local per company
1259
1625
  const limit = params.limit || 20;
1260
1626
  const offset = params.offset || 0;
1261
1627
  const body = {
@@ -1272,51 +1638,50 @@ const toolHandlers = {
1272
1638
  const result = await apiRequest(`/api/tables/${params.table}/list`, {
1273
1639
  method: "POST",
1274
1640
  body: JSON.stringify(body),
1275
- }, companyId);
1641
+ }, params.company_id);
1276
1642
  return {
1277
1643
  table: params.table,
1644
+ company_id: params.company_id,
1278
1645
  total: result.total,
1279
1646
  rows: result.data || [],
1280
1647
  };
1281
1648
  },
1282
1649
  async gufi_row(params) {
1283
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1284
1650
  const result = await apiRequest(`/api/tables/${params.table}/getOne`, {
1285
1651
  method: "POST",
1286
1652
  body: JSON.stringify({ id: params.id }),
1287
- }, companyId);
1653
+ }, params.company_id);
1288
1654
  return {
1289
1655
  table: params.table,
1656
+ company_id: params.company_id,
1290
1657
  id: params.id,
1291
1658
  row: result.data || result,
1292
1659
  };
1293
1660
  },
1294
1661
  async gufi_row_create(params) {
1295
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1296
1662
  const result = await apiRequest(`/api/tables/${params.table}`, {
1297
1663
  method: "POST",
1298
1664
  body: JSON.stringify(params.data),
1299
- }, companyId);
1665
+ }, params.company_id);
1300
1666
  return {
1301
1667
  success: true,
1668
+ company_id: params.company_id,
1302
1669
  id: result.id || result.data?.id,
1303
1670
  row: result.data || result,
1304
1671
  };
1305
1672
  },
1306
1673
  async gufi_row_update(params) {
1307
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1308
1674
  const result = await apiRequest(`/api/tables/${params.table}/${params.id}`, {
1309
1675
  method: "PUT",
1310
1676
  body: JSON.stringify(params.data),
1311
- }, companyId);
1312
- return { success: true, row: result.data || result };
1677
+ }, params.company_id);
1678
+ return { success: true, company_id: params.company_id, row: result.data || result };
1313
1679
  },
1314
1680
  async gufi_row_delete(params) {
1315
- const companyId = params.company_id || await detectCompanyFromTable(params.table);
1316
1681
  await apiRequest(`/api/tables/${params.table}/${params.id}`, {
1317
1682
  method: "DELETE",
1318
- }, companyId);
1319
- return { success: true };
1683
+ }, params.company_id);
1684
+ return { success: true, company_id: params.company_id };
1320
1685
  },
1321
1686
  // ─────────────────────────────────────────────────────────────────────────
1322
1687
  // Environment Variables
@@ -1329,14 +1694,14 @@ const toolHandlers = {
1329
1694
  await apiRequest("/api/cli/env", {
1330
1695
  method: "POST",
1331
1696
  body: JSON.stringify({ key: params.key, value: params.value }),
1332
- });
1333
- return { success: true };
1697
+ }, params.company_id);
1698
+ return { success: true, company_id: params.company_id };
1334
1699
  },
1335
1700
  async gufi_env_delete(params) {
1336
1701
  await apiRequest(`/api/cli/env/${encodeURIComponent(params.key)}`, {
1337
1702
  method: "DELETE",
1338
- });
1339
- return { success: true };
1703
+ }, params.company_id);
1704
+ return { success: true, company_id: params.company_id };
1340
1705
  },
1341
1706
  // ─────────────────────────────────────────────────────────────────────────
1342
1707
  // Views
@@ -1392,6 +1757,516 @@ const toolHandlers = {
1392
1757
  : `❌ ${response.errors?.length || 0} errors, ${response.warnings?.length || 0} warnings`,
1393
1758
  };
1394
1759
  },
1760
+ async gufi_view_create(params) {
1761
+ // Generate safe function name
1762
+ const safeName = params.name.replace(/[^a-zA-Z0-9]/g, "");
1763
+ // Template generators
1764
+ const getBlankTemplate = (name, fnName) => ({
1765
+ index: `// ${name}
1766
+ import React from 'react';
1767
+ import { Package } from 'lucide-react';
1768
+ import { type FeatureProps } from '@/sdk';
1769
+
1770
+ export { featureConfig } from './core/dataProvider';
1771
+
1772
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
1773
+ const viewSpec = gufi?.context?.viewSpec || {};
1774
+
1775
+ return (
1776
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
1777
+ <header className="mb-6">
1778
+ <h1 className="text-xl font-bold flex items-center gap-2">
1779
+ <Package className="w-5 h-5 text-violet-600" />
1780
+ ${name}
1781
+ </h1>
1782
+ </header>
1783
+
1784
+ <div className="bg-white rounded-xl border shadow-sm p-6">
1785
+ <p className="text-gray-600">Start building your view here!</p>
1786
+ <p className="text-sm text-gray-400 mt-2">
1787
+ viewSpec.mainTable: {viewSpec.mainTable || 'Not configured'}
1788
+ </p>
1789
+ </div>
1790
+ </div>
1791
+ );
1792
+ }
1793
+ `,
1794
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
1795
+
1796
+ export const featureConfig: FeatureConfig = {
1797
+ dataSources: {
1798
+ mainTable: {
1799
+ type: 'table',
1800
+ module: 'default',
1801
+ entity: 'items',
1802
+ label: 'Main Table'
1803
+ }
1804
+ },
1805
+ inputs: {},
1806
+ seedData: {
1807
+ description: { es: 'Vista', en: 'View' },
1808
+ data: {},
1809
+ order: []
1810
+ }
1811
+ };
1812
+ `,
1813
+ });
1814
+ const getDashboardTemplate = (name, fnName) => ({
1815
+ index: `// ${name}
1816
+ import React, { useState, useEffect } from 'react';
1817
+ import { Package, TrendingUp, Users, DollarSign } from 'lucide-react';
1818
+ import { cn, toastError, type FeatureProps } from '@/sdk';
1819
+ import { Button, Card, CardContent, CardHeader, CardTitle } from '@/sdk';
1820
+
1821
+ export { featureConfig } from './core/dataProvider';
1822
+
1823
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
1824
+ const dataProvider = gufi?.dataProvider;
1825
+ const viewSpec = gufi?.context?.viewSpec || {};
1826
+ const isPreview = gufi?.context?.isPreview;
1827
+ const seedData = gufi?.seedData;
1828
+
1829
+ const [data, setData] = useState<any[]>([]);
1830
+ const [loading, setLoading] = useState(true);
1831
+ const [stats, setStats] = useState({ total: 0, active: 0, revenue: 0 });
1832
+
1833
+ useEffect(() => {
1834
+ const load = async () => {
1835
+ setLoading(true);
1836
+
1837
+ if (isPreview && seedData?.data) {
1838
+ const items = seedData.data.items || [];
1839
+ setData(items);
1840
+ setStats({
1841
+ total: items.length,
1842
+ active: items.filter((i: any) => i.status === 'active').length,
1843
+ revenue: items.reduce((acc: number, i: any) => acc + (i.amount || 0), 0)
1844
+ });
1845
+ setLoading(false);
1846
+ return;
1847
+ }
1848
+
1849
+ if (!dataProvider || !viewSpec.mainTable) {
1850
+ setLoading(false);
1851
+ return;
1852
+ }
1853
+
1854
+ try {
1855
+ const res = await dataProvider.getList({
1856
+ resource: viewSpec.mainTable,
1857
+ pagination: { current: 1, pageSize: 100 },
1858
+ });
1859
+ setData(res.data || []);
1860
+ } catch (err) {
1861
+ toastError('Error loading data');
1862
+ }
1863
+ setLoading(false);
1864
+ };
1865
+ load();
1866
+ }, [dataProvider, viewSpec.mainTable, seedData, isPreview]);
1867
+
1868
+ if (loading) {
1869
+ return (
1870
+ <div className="p-6 space-y-4">
1871
+ {[1,2,3].map(i => (
1872
+ <div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" />
1873
+ ))}
1874
+ </div>
1875
+ );
1876
+ }
1877
+
1878
+ return (
1879
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
1880
+ <header className="mb-6">
1881
+ <h1 className="text-2xl font-bold flex items-center gap-2">
1882
+ <Package className="w-6 h-6 text-violet-600" />
1883
+ ${name}
1884
+ </h1>
1885
+ </header>
1886
+
1887
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
1888
+ <Card>
1889
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
1890
+ <CardTitle className="text-sm font-medium text-gray-500">Total</CardTitle>
1891
+ <Users className="w-4 h-4 text-violet-500" />
1892
+ </CardHeader>
1893
+ <CardContent>
1894
+ <div className="text-2xl font-bold">{stats.total}</div>
1895
+ </CardContent>
1896
+ </Card>
1897
+ <Card>
1898
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
1899
+ <CardTitle className="text-sm font-medium text-gray-500">Active</CardTitle>
1900
+ <TrendingUp className="w-4 h-4 text-green-500" />
1901
+ </CardHeader>
1902
+ <CardContent>
1903
+ <div className="text-2xl font-bold text-green-600">{stats.active}</div>
1904
+ </CardContent>
1905
+ </Card>
1906
+ <Card>
1907
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
1908
+ <CardTitle className="text-sm font-medium text-gray-500">Revenue</CardTitle>
1909
+ <DollarSign className="w-4 h-4 text-violet-500" />
1910
+ </CardHeader>
1911
+ <CardContent>
1912
+ <div className="text-2xl font-bold">\${stats.revenue.toLocaleString()}</div>
1913
+ </CardContent>
1914
+ </Card>
1915
+ </div>
1916
+
1917
+ <Card className="flex-1">
1918
+ <CardHeader>
1919
+ <CardTitle>Records</CardTitle>
1920
+ </CardHeader>
1921
+ <CardContent>
1922
+ <div className="space-y-2">
1923
+ {data.slice(0, 10).map((item, i) => (
1924
+ <div key={item.id || i} className="p-3 bg-gray-50 rounded-lg flex justify-between">
1925
+ <span>{item.name || item.nombre || 'Item ' + (i + 1)}</span>
1926
+ <span className="text-gray-500">{item.status || '-'}</span>
1927
+ </div>
1928
+ ))}
1929
+ {data.length === 0 && (
1930
+ <div className="text-center text-gray-500 py-8">No data available</div>
1931
+ )}
1932
+ </div>
1933
+ </CardContent>
1934
+ </Card>
1935
+ </div>
1936
+ );
1937
+ }
1938
+ `,
1939
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
1940
+
1941
+ export const featureConfig: FeatureConfig = {
1942
+ dataSources: {
1943
+ mainTable: {
1944
+ type: 'table',
1945
+ module: 'default',
1946
+ entity: 'items',
1947
+ label: 'Main Data'
1948
+ }
1949
+ },
1950
+ inputs: {
1951
+ title: {
1952
+ type: 'text',
1953
+ label: 'Dashboard Title',
1954
+ default: '${name}'
1955
+ }
1956
+ },
1957
+ seedData: {
1958
+ description: {
1959
+ es: 'Dashboard de ejemplo',
1960
+ en: 'Example dashboard'
1961
+ },
1962
+ data: {
1963
+ items: [
1964
+ { id: 1, name: 'Item 1', status: 'active', amount: 1500 },
1965
+ { id: 2, name: 'Item 2', status: 'active', amount: 2300 },
1966
+ { id: 3, name: 'Item 3', status: 'pending', amount: 800 },
1967
+ ]
1968
+ },
1969
+ order: ['items']
1970
+ }
1971
+ };
1972
+ `,
1973
+ });
1974
+ const getTableTemplate = (name, fnName) => ({
1975
+ index: `// ${name}
1976
+ import React, { useState, useEffect } from 'react';
1977
+ import { TableIcon, Search } from 'lucide-react';
1978
+ import { cn, toastError, type FeatureProps } from '@/sdk';
1979
+ import { Input, Button } from '@/sdk';
1980
+
1981
+ export { featureConfig } from './core/dataProvider';
1982
+
1983
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
1984
+ const dataProvider = gufi?.dataProvider;
1985
+ const viewSpec = gufi?.context?.viewSpec || {};
1986
+ const isPreview = gufi?.context?.isPreview;
1987
+ const seedData = gufi?.seedData;
1988
+
1989
+ const [data, setData] = useState<any[]>([]);
1990
+ const [loading, setLoading] = useState(true);
1991
+ const [search, setSearch] = useState('');
1992
+
1993
+ useEffect(() => {
1994
+ const load = async () => {
1995
+ setLoading(true);
1996
+ if (isPreview && seedData?.data) {
1997
+ setData(seedData.data.items || []);
1998
+ setLoading(false);
1999
+ return;
2000
+ }
2001
+ if (!dataProvider || !viewSpec.mainTable) {
2002
+ setLoading(false);
2003
+ return;
2004
+ }
2005
+ try {
2006
+ const res = await dataProvider.getList({
2007
+ resource: viewSpec.mainTable,
2008
+ pagination: { current: 1, pageSize: 50 },
2009
+ });
2010
+ setData(res.data || []);
2011
+ } catch (err) {
2012
+ toastError('Error loading data');
2013
+ }
2014
+ setLoading(false);
2015
+ };
2016
+ load();
2017
+ }, [dataProvider, viewSpec.mainTable, seedData, isPreview]);
2018
+
2019
+ const filtered = data.filter(item =>
2020
+ !search || JSON.stringify(item).toLowerCase().includes(search.toLowerCase())
2021
+ );
2022
+
2023
+ if (loading) {
2024
+ return (
2025
+ <div className="p-6">
2026
+ <div className="h-10 bg-gray-100 rounded animate-pulse mb-4" />
2027
+ {[1,2,3,4,5].map(i => (
2028
+ <div key={i} className="h-12 bg-gray-50 rounded animate-pulse mb-2" />
2029
+ ))}
2030
+ </div>
2031
+ );
2032
+ }
2033
+
2034
+ return (
2035
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
2036
+ <header className="mb-4 flex items-center justify-between">
2037
+ <h1 className="text-xl font-bold flex items-center gap-2">
2038
+ <TableIcon className="w-5 h-5 text-violet-600" />
2039
+ ${name}
2040
+ </h1>
2041
+ <div className="relative">
2042
+ <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
2043
+ <Input
2044
+ className="pl-9 w-64"
2045
+ placeholder="Search..."
2046
+ value={search}
2047
+ onChange={(e) => setSearch(e.target.value)}
2048
+ />
2049
+ </div>
2050
+ </header>
2051
+
2052
+ <div className="bg-white rounded-xl border shadow-sm overflow-hidden">
2053
+ <table className="w-full">
2054
+ <thead className="bg-gray-50 border-b">
2055
+ <tr>
2056
+ <th className="text-left p-3 font-medium text-gray-600">ID</th>
2057
+ <th className="text-left p-3 font-medium text-gray-600">Name</th>
2058
+ <th className="text-left p-3 font-medium text-gray-600">Status</th>
2059
+ </tr>
2060
+ </thead>
2061
+ <tbody>
2062
+ {filtered.map((item, i) => (
2063
+ <tr key={item.id || i} className="border-b hover:bg-gray-50">
2064
+ <td className="p-3">{item.id}</td>
2065
+ <td className="p-3">{item.name || item.nombre || '-'}</td>
2066
+ <td className="p-3">{item.status || '-'}</td>
2067
+ </tr>
2068
+ ))}
2069
+ {filtered.length === 0 && (
2070
+ <tr>
2071
+ <td colSpan={3} className="p-8 text-center text-gray-500">No data found</td>
2072
+ </tr>
2073
+ )}
2074
+ </tbody>
2075
+ </table>
2076
+ </div>
2077
+ </div>
2078
+ );
2079
+ }
2080
+ `,
2081
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
2082
+
2083
+ export const featureConfig: FeatureConfig = {
2084
+ dataSources: {
2085
+ mainTable: {
2086
+ type: 'table',
2087
+ module: 'default',
2088
+ entity: 'items',
2089
+ label: 'Data Table'
2090
+ }
2091
+ },
2092
+ inputs: {},
2093
+ seedData: {
2094
+ description: { es: 'Tabla de datos', en: 'Data table' },
2095
+ data: {
2096
+ items: [
2097
+ { id: 1, name: 'Record 1', status: 'active' },
2098
+ { id: 2, name: 'Record 2', status: 'pending' },
2099
+ { id: 3, name: 'Record 3', status: 'completed' },
2100
+ ]
2101
+ },
2102
+ order: ['items']
2103
+ }
2104
+ };
2105
+ `,
2106
+ });
2107
+ const getFormTemplate = (name, fnName) => ({
2108
+ index: `// ${name}
2109
+ import React, { useState } from 'react';
2110
+ import { FileText, Save } from 'lucide-react';
2111
+ import { toastSuccess, toastError, type FeatureProps } from '@/sdk';
2112
+ import { Button, Input, Label } from '@/sdk';
2113
+
2114
+ export { featureConfig } from './core/dataProvider';
2115
+
2116
+ export default function ${fnName}View({ id, gufi }: FeatureProps) {
2117
+ const dataProvider = gufi?.dataProvider;
2118
+ const viewSpec = gufi?.context?.viewSpec || {};
2119
+ const CB = gufi?.CB;
2120
+
2121
+ const [formData, setFormData] = useState({ name: '', email: '', notes: '' });
2122
+ const [saving, setSaving] = useState(false);
2123
+
2124
+ const handleSubmit = async (e: React.FormEvent) => {
2125
+ e.preventDefault();
2126
+ if (!dataProvider || !viewSpec.mainTable || !CB) {
2127
+ toastError('Configuration error');
2128
+ return;
2129
+ }
2130
+ setSaving(true);
2131
+ try {
2132
+ await dataProvider.create({
2133
+ resource: viewSpec.mainTable,
2134
+ variables: {
2135
+ name: CB.text(formData.name),
2136
+ email: CB.text(formData.email),
2137
+ notes: CB.text(formData.notes),
2138
+ }
2139
+ });
2140
+ toastSuccess('Saved successfully!');
2141
+ setFormData({ name: '', email: '', notes: '' });
2142
+ } catch (err) {
2143
+ toastError('Error saving');
2144
+ }
2145
+ setSaving(false);
2146
+ };
2147
+
2148
+ return (
2149
+ <div className="flex flex-col min-h-screen bg-gradient-to-br from-violet-50/40 via-white to-purple-50/40 p-6">
2150
+ <header className="mb-6">
2151
+ <h1 className="text-xl font-bold flex items-center gap-2">
2152
+ <FileText className="w-5 h-5 text-violet-600" />
2153
+ ${name}
2154
+ </h1>
2155
+ </header>
2156
+
2157
+ <form onSubmit={handleSubmit} className="bg-white rounded-xl border shadow-sm p-6 max-w-lg space-y-4">
2158
+ <div>
2159
+ <Label htmlFor="name">Name</Label>
2160
+ <Input
2161
+ id="name"
2162
+ value={formData.name}
2163
+ onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
2164
+ placeholder="Enter name..."
2165
+ required
2166
+ />
2167
+ </div>
2168
+ <div>
2169
+ <Label htmlFor="email">Email</Label>
2170
+ <Input
2171
+ id="email"
2172
+ type="email"
2173
+ value={formData.email}
2174
+ onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
2175
+ placeholder="Enter email..."
2176
+ />
2177
+ </div>
2178
+ <div>
2179
+ <Label htmlFor="notes">Notes</Label>
2180
+ <Input
2181
+ id="notes"
2182
+ value={formData.notes}
2183
+ onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
2184
+ placeholder="Additional notes..."
2185
+ />
2186
+ </div>
2187
+ <Button type="submit" disabled={saving} className="w-full">
2188
+ <Save className="w-4 h-4 mr-2" />
2189
+ {saving ? 'Saving...' : 'Save'}
2190
+ </Button>
2191
+ </form>
2192
+ </div>
2193
+ );
2194
+ }
2195
+ `,
2196
+ dataProvider: `import type { FeatureConfig } from '@/sdk';
2197
+
2198
+ export const featureConfig: FeatureConfig = {
2199
+ dataSources: {
2200
+ mainTable: {
2201
+ type: 'table',
2202
+ module: 'default',
2203
+ entity: 'submissions',
2204
+ label: 'Form Submissions'
2205
+ }
2206
+ },
2207
+ inputs: {},
2208
+ seedData: {
2209
+ description: { es: 'Formulario', en: 'Form' },
2210
+ data: {},
2211
+ order: []
2212
+ }
2213
+ };
2214
+ `,
2215
+ });
2216
+ // Get the right template
2217
+ let template;
2218
+ switch (params.template) {
2219
+ case "dashboard":
2220
+ template = getDashboardTemplate(params.name, safeName);
2221
+ break;
2222
+ case "table":
2223
+ template = getTableTemplate(params.name, safeName);
2224
+ break;
2225
+ case "form":
2226
+ template = getFormTemplate(params.name, safeName);
2227
+ break;
2228
+ default:
2229
+ template = getBlankTemplate(params.name, safeName);
2230
+ }
2231
+ // Need package_id to create a view
2232
+ if (!params.package_id) {
2233
+ return {
2234
+ error: "package_id is required to create a view",
2235
+ hint: "First create a package with gufi_package_create, then pass its ID",
2236
+ available_templates: ["dashboard", "table", "form", "blank"],
2237
+ };
2238
+ }
2239
+ // Create the view via API
2240
+ const response = await apiRequest(`/api/marketplace/packages/${params.package_id}/views`, {
2241
+ method: "POST",
2242
+ body: JSON.stringify({
2243
+ name: params.name,
2244
+ description: params.description || `${params.template} view created via MCP`,
2245
+ view_type: "custom",
2246
+ }),
2247
+ });
2248
+ const viewId = response.pk_id;
2249
+ // Now update with template files
2250
+ const files = [
2251
+ { file_path: "index.tsx", content: template.index, language: "typescript" },
2252
+ { file_path: "core/dataProvider.ts", content: template.dataProvider, language: "typescript" },
2253
+ ];
2254
+ await saveViewFiles(viewId, files);
2255
+ return {
2256
+ success: true,
2257
+ view_id: viewId,
2258
+ name: params.name,
2259
+ template: params.template,
2260
+ package_id: params.package_id,
2261
+ files_created: files.map(f => f.file_path),
2262
+ next_steps: [
2263
+ `1. Download locally: gufi view:pull ${viewId}`,
2264
+ `2. Edit files in: ~/gufi-dev/view_${viewId}/`,
2265
+ `3. Upload changes: gufi view:push`,
2266
+ `4. Publish: gufi package:sync ${params.package_id}`,
2267
+ ],
2268
+ };
2269
+ },
1395
2270
  // ─────────────────────────────────────────────────────────────────────────
1396
2271
  // Packages
1397
2272
  // ─────────────────────────────────────────────────────────────────────────
@@ -1544,6 +2419,31 @@ const toolHandlers = {
1544
2419
  const response = await developerRequest(`/packages/${params.package_id}/entities`);
1545
2420
  return { entities: response.data || [] };
1546
2421
  },
2422
+ async gufi_link_view(params) {
2423
+ const entity = {
2424
+ kind: "marketplace_view",
2425
+ marketplace_view_id: params.view_id,
2426
+ label: params.label,
2427
+ icon: params.icon || "Sparkles",
2428
+ };
2429
+ const response = await developerRequest(`/packages/${params.package_id}/modules/${params.module_id}/add-entity`, {
2430
+ method: "POST",
2431
+ body: JSON.stringify({
2432
+ submodule_name: params.submodule_name,
2433
+ entity,
2434
+ }),
2435
+ });
2436
+ // Invalidate schema cache on main backend
2437
+ try {
2438
+ await apiRequest("/api/company/schema/invalidate", { method: "POST" });
2439
+ }
2440
+ catch { }
2441
+ return {
2442
+ success: true,
2443
+ message: `View "${params.label}" linked to ${params.submodule_name}`,
2444
+ entity: response.data?.entity,
2445
+ };
2446
+ },
1547
2447
  };
1548
2448
  // ════════════════════════════════════════════════════════════════════════════
1549
2449
  // Context Generation Helpers