gufi-cli 0.1.41 → 0.1.44

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.
package/CLAUDE.md CHANGED
@@ -10,7 +10,7 @@
10
10
  tools/cli/
11
11
  ├── src/
12
12
  │ ├── index.ts # Entry point CLI
13
- │ ├── mcp.ts # MCP Server (12 tools)
13
+ │ ├── mcp.ts # MCP Server (15 tools)
14
14
  │ ├── commands/ # Comandos CLI
15
15
  │ │ ├── context.ts # gufi context
16
16
  │ │ ├── pull.ts # gufi view:pull
@@ -55,9 +55,9 @@ gufi config:prod
55
55
  }
56
56
  ```
57
57
 
58
- ## MCP Server - 13 Tools
58
+ ## MCP Server - 15 Tools
59
59
 
60
- El servidor MCP está en `src/mcp.ts` y expone **13 tools** para que Claude interactúe con Gufi.
60
+ El servidor MCP está en `src/mcp.ts` y expone **15 tools** para que Claude interactúe con Gufi.
61
61
 
62
62
  ### Tools organizados por categoría
63
63
 
@@ -67,10 +67,12 @@ El servidor MCP está en `src/mcp.ts` y expone **13 tools** para que Claude inte
67
67
  | | `gufi_whoami` | Usuario, entorno, empresas |
68
68
  | | `gufi_docs` | Documentación (`docs/mcp/`) |
69
69
  | **Schema** | `gufi_schema_modify` | Operaciones atómicas (add/update/remove) |
70
- | **Automations** | `gufi_automation` | Scripts: list, get, create |
71
- | | `gufi_triggers` | Ver/configurar triggers |
72
- | | `gufi_executions` | Historial de ejecuciones |
73
- | **Data** | `gufi_data` | CRUD: list, get, create, update, delete |
70
+ | **Automations** | `gufi_automation_scripts` | Scripts: list, get, create, update, delete |
71
+ | | `gufi_automation_script_test` | Testear script manualmente |
72
+ | | `gufi_automation_meta` | Ver/configurar triggers por entidad |
73
+ | | `gufi_automation_integrations` | Integraciones built-in (Stripe, Nayax, etc.) |
74
+ | | `gufi_automation_executions` | Historial de ejecuciones |
75
+ | **Data** | `gufi_data` | CRUD: list, get, create, update, delete, aggregate |
74
76
  | **Env** | `gufi_env` | Variables: list, set, delete |
75
77
  | **Views** | `gufi_view_pull` | Descargar vista a local |
76
78
  | | `gufi_view_push` | Subir cambios a draft |
@@ -165,12 +167,14 @@ export async function miComando(args: string[], flags: Flags) {
165
167
  ```
166
168
  gufi view:pull <id> → ~/gufi-dev/view_<id>/
167
169
  # Editar archivos locales
168
- gufi view:push → Sync completo
170
+ gufi view:push → Sync completo + registra automations
169
171
  gufi package:publish <id> → Publicar
170
172
  ```
171
173
 
172
174
  **Push = sync completo**: Sube TODOS los archivos, servidor borra los que no vengan.
173
175
 
176
+ **Automations**: Si existe `core/automations.ts`, push detecta las automations declaradas y las registra en `__automation_meta__` con `trigger_event='CUSTOM'`.
177
+
174
178
  ## Config (~/.gufi/)
175
179
 
176
180
  ```
package/README.md CHANGED
@@ -509,12 +509,11 @@ El MCP expone todas las funcionalidades del CLI como tools que Claude puede usar
509
509
  | `gufi_module_update` | Actualizar módulo desde JSON |
510
510
  | `gufi_module_create` | Crear módulo nuevo |
511
511
  | **Automations** | |
512
- | `gufi_automations` | Listar scripts de automation |
513
- | `gufi_automation` | Ver código de un automation |
514
- | `gufi_automation_create` | Crear/actualizar automation |
515
- | `gufi_entity_automations` | Ver triggers de una entidad |
516
- | `gufi_entity_automations_update` | Actualizar triggers |
517
- | `gufi_automations_executions` | Ver historial de ejecuciones |
512
+ | `gufi_automation_scripts` | CRUD de scripts (list, get, create, update, delete) |
513
+ | `gufi_automation_meta` | Ver/configurar triggers por entidad |
514
+ | `gufi_automation_integrations` | Ver integraciones built-in (Stripe, Nayax, etc.) |
515
+ | `gufi_automation_script_test` | Testear un script manualmente |
516
+ | `gufi_automation_executions` | Ver historial de ejecuciones |
518
517
  | **Datos** | |
519
518
  | `gufi_rows` | Listar registros de una tabla |
520
519
  | `gufi_row` | Ver un registro específico |
@@ -546,7 +545,7 @@ Una vez configurado, puedes hablar naturalmente con Claude:
546
545
  → Claude usa gufi_module para leer, modifica, y usa gufi_module_update
547
546
 
548
547
  "Crea un automation que envíe email cuando un pedido pase a 'enviado'"
549
- → Claude usa gufi_automations para ver existentes, crea código, usa gufi_automation_create
548
+ → Claude usa gufi_automation_scripts para ver existentes, crea código, configura con gufi_automation_meta
550
549
 
551
550
  "¿Cuántos registros hay en la tabla de clientes?"
552
551
  → Claude usa gufi_rows para consultar
@@ -1496,9 +1496,9 @@ export async function automationExecuteCommand(automationIdOrName, options) {
1496
1496
  process.exit(1);
1497
1497
  }
1498
1498
  }
1499
- // 💜 Execute automation via triggerClick endpoint
1499
+ // 💜 Execute automation
1500
1500
  console.log(chalk.yellow("\n ⏳ Ejecutando..."));
1501
- const result = await apiRequest("/api/automation-scripts/triggerClick", {
1501
+ const result = await apiRequest("/api/automation-scripts/run", {
1502
1502
  method: "POST",
1503
1503
  headers: { "X-Company-ID": companyId },
1504
1504
  body: JSON.stringify({
@@ -367,12 +367,10 @@ function generateRelevantConcepts(dataSources, inputs, automations) {
367
367
  // automations concept
368
368
  if (automations.length > 0) {
369
369
  md += `### Llamar Automations\n`;
370
- md += `Puedes ejecutar automations de tipo \`click\` desde la vista:\n`;
370
+ md += `Puedes ejecutar automations desde la vista con \`gufi.automation()\`:\n`;
371
371
  md += "```typescript\n";
372
- md += `await gufi.dataProvider.runClickAutomation?.({\n`;
373
- md += ` company_id: companyId,\n`;
374
- md += ` function_name: "${automations[0]?.function_name || "mi_automation"}",\n`;
375
- md += ` input: { /* parámetros */ },\n`;
372
+ md += `const result = await gufi.automation("${automations[0]?.function_name || "mi_automation"}", {\n`;
373
+ md += ` // input parameters\n`;
376
374
  md += `});\n`;
377
375
  md += "```\n\n";
378
376
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * gufi integrations - List available integrations and their methods
3
+ *
4
+ * Usage:
5
+ * gufi integrations # List all integrations with methods
6
+ */
7
+ /**
8
+ * List all integrations with their methods
9
+ */
10
+ export declare function integrationsCommand(): Promise<void>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * gufi integrations - List available integrations and their methods
3
+ *
4
+ * Usage:
5
+ * gufi integrations # List all integrations with methods
6
+ */
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import { fileURLToPath } from "url";
10
+ import chalk from "chalk";
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ // Path to integrations folder
14
+ const INTEGRATIONS_PATH = path.resolve(__dirname, "../../../../backend/src/features/automations/core/api/integrations");
15
+ /**
16
+ * List all integrations with their methods
17
+ */
18
+ export async function integrationsCommand() {
19
+ const integrations = await loadIntegrations();
20
+ console.log(chalk.bold.magenta("\n🔌 Gufi Integrations\n"));
21
+ for (const [name, integration] of Object.entries(integrations)) {
22
+ const methodCount = Object.keys(integration.methods).length;
23
+ console.log(` ${chalk.cyan(name.padEnd(12))} ${chalk.gray(integration.meta.description)} ${chalk.yellow(`(${methodCount} methods)`)}`);
24
+ for (const [methodName, method] of Object.entries(integration.methods)) {
25
+ console.log(` ${chalk.white("•")} ${methodName} - ${chalk.gray(method.description)}`);
26
+ }
27
+ console.log();
28
+ }
29
+ }
30
+ async function loadIntegrations() {
31
+ const integrations = {};
32
+ const dirs = fs.readdirSync(INTEGRATIONS_PATH, { withFileTypes: true });
33
+ for (const dir of dirs) {
34
+ if (!dir.isDirectory())
35
+ continue;
36
+ const integrationPath = path.join(INTEGRATIONS_PATH, dir.name, "integration.js");
37
+ if (!fs.existsSync(integrationPath))
38
+ continue;
39
+ try {
40
+ const module = await import(`file://${integrationPath}`);
41
+ if (module.meta && module.methods) {
42
+ integrations[module.meta.name] = {
43
+ meta: module.meta,
44
+ methods: module.methods,
45
+ };
46
+ }
47
+ }
48
+ catch (err) {
49
+ console.error(chalk.yellow(`Warning: Could not load ${dir.name}: ${err.message}`));
50
+ }
51
+ }
52
+ return integrations;
53
+ }
package/dist/index.js CHANGED
@@ -80,6 +80,7 @@ import { configCommand, configLocalCommand, configProdCommand, configSetCommand,
80
80
  import { contextCommand } from "./commands/context.js";
81
81
  import { doctorCommand } from "./commands/doctor.js";
82
82
  import { docsCommand } from "./commands/docs.js";
83
+ import { integrationsCommand } from "./commands/integrations.js";
83
84
  import { startMcpServer } from "./mcp.js";
84
85
  import { claudeCommand } from "./commands/claude.js";
85
86
  import { createRequire } from "module";
@@ -163,6 +164,13 @@ program
163
164
  docsCommand(args);
164
165
  });
165
166
  // ════════════════════════════════════════════════════════════════════
167
+ // 🔌 Integrations
168
+ // ════════════════════════════════════════════════════════════════════
169
+ program
170
+ .command("integrations")
171
+ .description("Listar integraciones disponibles con sus métodos")
172
+ .action(integrationsCommand);
173
+ // ════════════════════════════════════════════════════════════════════
166
174
  // 🏢 Companies & Modules
167
175
  // ════════════════════════════════════════════════════════════════════
168
176
  program
package/dist/lib/sync.js CHANGED
@@ -8,6 +8,7 @@ import os from "os";
8
8
  import crypto from "crypto";
9
9
  import { getViewFiles, saveViewFiles } from "./api.js";
10
10
  import { setCurrentView, getCurrentView } from "./config.js";
11
+ // 💜 Views go to ~/gufi-dev/ for local development
11
12
  const GUFI_DEV_DIR = path.join(os.homedir(), "gufi-dev");
12
13
  const META_FILE = ".gufi-view.json";
13
14
  function ensureDir(dir) {
package/dist/mcp.js CHANGED
@@ -13,14 +13,102 @@
13
13
  import * as readline from "readline";
14
14
  import * as fs from "fs";
15
15
  import * as path from "path";
16
+ import * as os from "os";
17
+ import * as crypto from "crypto";
16
18
  import { fileURLToPath } from "url";
17
- import { getToken, getRefreshToken, loadConfig, isLoggedIn, getTokenForEnv, getRefreshTokenForEnv, setTokenForEnv, getCredentialsForEnv } from "./lib/config.js";
18
- import { pullView, pushView, getViewDir, loadViewMeta } from "./lib/sync.js";
19
+ // 💜 Local storage for CLI: ~/gufi-dev/
20
+ const LOCAL_VIEWS_DIR = path.join(os.homedir(), "gufi-dev");
21
+ // 💜 Auto-detect if we should use local filesystem or workspace BD
22
+ // In server (Docker), use GUFI_USE_WORKSPACE=true to force workspace BD
23
+ function canWriteLocal() {
24
+ // Server mode: force workspace BD instead of local filesystem
25
+ if (process.env.GUFI_USE_WORKSPACE === "true") {
26
+ return false;
27
+ }
28
+ try {
29
+ fs.mkdirSync(LOCAL_VIEWS_DIR, { recursive: true });
30
+ const testFile = path.join(LOCAL_VIEWS_DIR, ".write-test");
31
+ fs.writeFileSync(testFile, "test");
32
+ fs.unlinkSync(testFile);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ import { getToken, getRefreshToken, loadConfig, isLoggedIn, getTokenForEnv, getRefreshTokenForEnv, setTokenForEnv, getCredentialsForEnv, getCurrentEnv } from "./lib/config.js";
19
40
  // For ES modules __dirname equivalent
20
41
  const __filename = fileURLToPath(import.meta.url);
21
42
  const __dirname = path.dirname(__filename);
22
43
  // docs/mcp path relative to CLI
23
44
  const DOCS_MCP_PATH = path.resolve(__dirname, "../../../docs/mcp");
45
+ // docs/dev-guide path for integration docs
46
+ const DOCS_DEV_GUIDE_PATH = path.resolve(__dirname, "../../../docs/dev-guide");
47
+ /**
48
+ * Parse frontmatter from markdown content
49
+ */
50
+ function parseFrontmatter(content) {
51
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
52
+ const match = content.match(frontmatterRegex);
53
+ if (!match) {
54
+ return { frontmatter: {}, body: content };
55
+ }
56
+ const [, frontmatterStr, body] = match;
57
+ const frontmatter = {};
58
+ frontmatterStr.split('\n').forEach(line => {
59
+ const colonIndex = line.indexOf(':');
60
+ if (colonIndex > 0) {
61
+ const key = line.slice(0, colonIndex).trim();
62
+ let value = line.slice(colonIndex + 1).trim();
63
+ // Remove quotes
64
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
65
+ value = value.slice(1, -1);
66
+ }
67
+ frontmatter[key] = value;
68
+ }
69
+ });
70
+ return { frontmatter, body: body.trim() };
71
+ }
72
+ /**
73
+ * Get all integration docs from docs/dev-guide/
74
+ * Returns docs that have group: integrations in frontmatter
75
+ */
76
+ function getIntegrationDocs() {
77
+ try {
78
+ if (!fs.existsSync(DOCS_DEV_GUIDE_PATH)) {
79
+ return [];
80
+ }
81
+ const files = fs.readdirSync(DOCS_DEV_GUIDE_PATH).filter(f => f.endsWith('.md'));
82
+ const integrations = [];
83
+ for (const file of files) {
84
+ const filePath = path.join(DOCS_DEV_GUIDE_PATH, file);
85
+ const content = fs.readFileSync(filePath, 'utf-8');
86
+ const { frontmatter, body } = parseFrontmatter(content);
87
+ // Only include docs with group: integrations
88
+ if (frontmatter.group === 'integrations') {
89
+ integrations.push({
90
+ id: frontmatter.id || file.replace('.md', ''),
91
+ title: frontmatter.title || '',
92
+ description: frontmatter.description || '',
93
+ content: body,
94
+ });
95
+ }
96
+ }
97
+ // Sort by filename order (2-06, 2-07, etc.)
98
+ return integrations;
99
+ }
100
+ catch (err) {
101
+ console.error('[MCP] Error reading integration docs:', err);
102
+ return [];
103
+ }
104
+ }
105
+ /**
106
+ * Get a specific integration doc by name/id
107
+ */
108
+ function getIntegrationDoc(name) {
109
+ const docs = getIntegrationDocs();
110
+ return docs.find(d => d.id === name || d.id === name.toLowerCase()) || null;
111
+ }
24
112
  // ════════════════════════════════════════════════════════════════════════════
25
113
  // Environment Configuration (stateless - passed per request)
26
114
  // ════════════════════════════════════════════════════════════════════════════
@@ -32,11 +120,19 @@ const ENV_URLS = {
32
120
  dev: "http://localhost:3000",
33
121
  local: "http://localhost:3000", // Alias for backwards compatibility
34
122
  };
35
- // Default environment - always prod for safety
36
- const DEFAULT_ENV = "prod";
123
+ // 💜 Default environment - read from config file (set by Gufi AI based on gufiEnv)
124
+ function getDefaultEnv() {
125
+ try {
126
+ const configEnv = getCurrentEnv();
127
+ return configEnv === "local" ? "dev" : "prod";
128
+ }
129
+ catch {
130
+ return "prod"; // Fallback if config not available
131
+ }
132
+ }
37
133
  /**
38
134
  * Get API URL for the specified environment.
39
- * @param env - 'prod' (default) or 'dev' (localhost:3000 Cloud SQL Dev)
135
+ * @param env - 'prod' or 'dev' (localhost:3000). If not specified, uses config's currentEnv.
40
136
  *
41
137
  * Uses resolveEnv() which is STRICT - invalid values throw errors.
42
138
  */
@@ -49,14 +145,14 @@ function getApiUrl(env) {
49
145
  * 'local' is treated as alias for 'dev'
50
146
  *
51
147
  * IMPORTANT: This function is STRICT to prevent accidental prod modifications.
52
- * - undefined/null → defaults to 'prod' (backwards compatible)
148
+ * - undefined/null → defaults to config's currentEnv (set by Gufi AI)
53
149
  * - 'prod', 'dev', 'local' → valid values
54
150
  * - Any other value → THROWS ERROR (prevents typos like 'dve' from hitting prod)
55
151
  */
56
152
  function resolveEnv(env) {
57
- // Undefined/null → default to prod (backwards compatible)
153
+ // Undefined/null → default to config's currentEnv
58
154
  if (env === undefined || env === null || env === "") {
59
- return "prod";
155
+ return getDefaultEnv();
60
156
  }
61
157
  // Valid values
62
158
  if (env === "dev" || env === "local")
@@ -70,7 +166,7 @@ function resolveEnv(env) {
70
166
  }
71
167
  // Keep for backwards compatibility (some internal functions may use this)
72
168
  function getSessionApiUrl() {
73
- return ENV_URLS[DEFAULT_ENV];
169
+ return ENV_URLS[getDefaultEnv()];
74
170
  }
75
171
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
76
172
  const viewFilesCache = new Map();
@@ -183,7 +279,7 @@ async function trackAnalytics(data) {
183
279
  // API Client (reused from CLI)
184
280
  // ════════════════════════════════════════════════════════════════════════════
185
281
  async function autoLogin() {
186
- return autoLoginWithEnv(DEFAULT_ENV);
282
+ return autoLoginWithEnv(getDefaultEnv());
187
283
  }
188
284
  async function autoLoginWithEnv(env) {
189
285
  const resolvedEnv = resolveEnv(env);
@@ -210,7 +306,7 @@ async function autoLoginWithEnv(env) {
210
306
  }
211
307
  }
212
308
  async function refreshOrLogin() {
213
- return refreshOrLoginWithEnv(DEFAULT_ENV);
309
+ return refreshOrLoginWithEnv(getDefaultEnv());
214
310
  }
215
311
  async function refreshOrLoginWithEnv(env) {
216
312
  const resolvedEnv = resolveEnv(env);
@@ -552,22 +648,23 @@ const TOOLS = [
552
648
  },
553
649
  },
554
650
  },
651
+ // ─────────────────────────────────────────────────────────────────────────
652
+ // Automations (Business Logic)
653
+ // ─────────────────────────────────────────────────────────────────────────
555
654
  {
556
655
  name: "gufi_automation_integrations",
557
- description: "List Gufi's built-in integrations (Stripe, Nayax, etc.) and their api.* methods. These are pre-built by Gufi - users can also create custom integrations directly in their automations using api.http() for any external service. Credentials are passed using env.* variables.",
656
+ description: getDesc("gufi_automation_integrations"),
558
657
  inputSchema: {
559
658
  type: "object",
560
659
  properties: {
561
660
  name: { type: "string", description: "Integration name to get detailed info (optional)" },
661
+ env: ENV_PARAM,
562
662
  },
563
663
  },
564
664
  },
565
- // ─────────────────────────────────────────────────────────────────────────
566
- // Automations (Business Logic)
567
- // ─────────────────────────────────────────────────────────────────────────
568
665
  {
569
- name: "gufi_automation",
570
- description: getDesc("gufi_automation"),
666
+ name: "gufi_automation_scripts",
667
+ description: getDesc("gufi_automation_scripts"),
571
668
  inputSchema: {
572
669
  type: "object",
573
670
  properties: {
@@ -583,33 +680,54 @@ const TOOLS = [
583
680
  },
584
681
  },
585
682
  {
586
- name: "gufi_automation_test",
587
- description: "Ejecutar/testear un automation script manualmente.\n\nEjemplo:\ngufi_automation_test({\n company_id: '116',\n script_name: 'send_email',\n entity: 'ventas.facturas',\n trigger_event: 'on_click',\n row: { id: 123, total: 150 },\n input: { email_to: 'test@example.com' } // Para on_click\n})",
683
+ name: "gufi_automation_script_test",
684
+ description: `Ejecutar/testear un automation script manualmente.
685
+
686
+ Ejemplos:
687
+ # Con entidad (la mayoría de casos)
688
+ gufi_automation_script_test({
689
+ company_id: '116',
690
+ script_name: 'send_email',
691
+ entity: 'ventas.facturas', // nombre lógico, se resuelve automáticamente
692
+ trigger_event: 'on_click',
693
+ row: { id: 123, total: 150 },
694
+ input: { email_to: 'test@example.com' }
695
+ })
696
+
697
+ # Script standalone (sin entidad vinculada)
698
+ gufi_automation_script_test({
699
+ company_id: '116',
700
+ script_name: 'daily_report',
701
+ trigger_event: 'scheduled',
702
+ input: { date: '2024-01-15' }
703
+ })
704
+
705
+ IMPORTANTE: Usa nombres lógicos (module.entity) NO IDs físicos.`,
588
706
  inputSchema: {
589
707
  type: "object",
590
708
  properties: {
591
709
  company_id: { type: "string", description: "Company ID" },
592
710
  script_name: { type: "string", description: "Script name to execute" },
593
- entity: { type: "string", description: "Entity name (module.entity format like 'ventas.facturas')" },
594
- trigger_event: { type: "string", description: "Trigger event: on_create, on_update, on_delete, on_click" },
595
- row: { type: "object", description: "Row data to pass to the script (must include id)" },
596
- input: { type: "object", description: "Input data for on_click triggers (goes to context.input)" },
711
+ entity: { type: "string", description: "Entity name (module.entity format). Optional for standalone scripts." },
712
+ trigger_event: { type: "string", description: "Trigger event: on_create, on_update, on_delete, on_click, scheduled" },
713
+ row: { type: "object", description: "Row data (goes to context.row). Include 'id' field." },
714
+ input: { type: "object", description: "Input data (goes to context.input). For on_click or standalone scripts." },
597
715
  env: ENV_PARAM,
598
716
  },
599
- required: ["company_id", "script_name", "entity", "trigger_event", "row"],
717
+ required: ["company_id", "script_name", "trigger_event"],
600
718
  },
601
719
  },
602
720
  {
603
- name: "gufi_triggers",
604
- description: getDesc("gufi_triggers"),
721
+ name: "gufi_automation_meta",
722
+ description: getDesc("gufi_automation_meta"),
605
723
  inputSchema: {
606
724
  type: "object",
607
725
  properties: {
608
726
  entity_id: { type: "string", description: "Entity ID" },
609
727
  company_id: { type: "string", description: "Company ID" },
610
- config: {
611
- type: "object",
612
- description: "Trigger config to SET (omit to GET current config). Format: { on_create: [...], on_update: [...], on_delete: [...], on_click: [...] }",
728
+ automations: {
729
+ type: "array",
730
+ description: "Array of automations to SET (omit to GET current). Each: { trigger: 'insert'|'update'|'delete'|'click'|'scheduled'|'custom', function_name: 'script_name' }. NOTA: CUSTOM se registra automáticamente al hacer view_push con core/automations.ts",
613
731
  },
614
732
  env: ENV_PARAM,
615
733
  },
@@ -617,8 +735,8 @@ const TOOLS = [
617
735
  },
618
736
  },
619
737
  {
620
- name: "gufi_executions",
621
- description: getDesc("gufi_executions"),
738
+ name: "gufi_automation_executions",
739
+ description: getDesc("gufi_automation_executions"),
622
740
  inputSchema: {
623
741
  type: "object",
624
742
  properties: {
@@ -710,6 +828,9 @@ Example: gufi_view_pull({ view_id: 13 })`,
710
828
  Pushes changed files from ~/gufi-dev/view_<id>/ to Gufi draft.
711
829
  Creates a snapshot for version history (last 10 kept).
712
830
 
831
+ ⚠️ IMPORTANT: If core/automations.ts exists, declared automations are registered in __automation_meta__.
832
+ Only automations declared in core/automations.ts can be called via gufi.automation().
833
+
713
834
  After pushing, use: gufi package:publish <package_id> to publish.
714
835
 
715
836
  Example: gufi_view_push({ view_id: 13, message: "Fixed bug in chart" })`,
@@ -732,21 +853,35 @@ Example: gufi_view_push({ view_id: 13, message: "Fixed bug in chart" })`,
732
853
  properties: {
733
854
  view_id: { type: "number", description: "View ID to test" },
734
855
  company_id: { type: "string", description: "Company ID for authentication context" },
735
- timeout: { type: "number", description: "Navigation timeout in ms (default: 15000)" },
856
+ timeout: { type: "number", description: "Navigation timeout in ms (default: 25000)" },
736
857
  actions: {
737
858
  type: "array",
738
- description: "Optional actions to perform after page load: [{type:'click',selector:'.btn'}, {type:'fill',selector:'input',value:'text'}, {type:'wait',selector:'.loaded'}, {type:'delay',ms:1000}]",
859
+ description: `Actions to perform. IMPORTANT workflow:
860
+ 1. First use 'explore' to list all interactive elements
861
+ 2. Then use 'click' with text selector: {type:'click', selector:'text:Button Text'}
862
+ 3. Use 'closeModals' to dismiss popups before interacting
863
+
864
+ Examples:
865
+ - Explore: {type:'explore'}
866
+ - Click by text: {type:'click', selector:'text:Submit'}
867
+ - Click by CSS: {type:'click', selector:'.my-button'}
868
+ - Close modals: {type:'closeModals'}
869
+ - Wait: {type:'delay', ms:1000}`,
739
870
  items: {
740
871
  type: "object",
741
872
  properties: {
742
- type: { type: "string", description: "Action type: click, fill, wait, delay" },
743
- selector: { type: "string", description: "CSS selector for click/fill/wait" },
744
- value: { type: "string", description: "Value for fill action" },
873
+ type: { type: "string", description: "Action type: explore (list elements), click, closeModals, fill, clear, select, wait, waitForText, delay, scroll, hover, screenshot, getText, eval" },
874
+ selector: { type: "string", description: "CSS selector OR 'text:Button Text' for click by visible text" },
875
+ value: { type: "string", description: "Value for fill/select action" },
876
+ text: { type: "string", description: "Text to wait for (waitForText)" },
745
877
  ms: { type: "number", description: "Milliseconds for delay action" },
878
+ y: { type: "number", description: "Pixels to scroll (scroll without selector)" },
879
+ timeout: { type: "number", description: "Timeout for wait actions (default 5000)" },
880
+ code: { type: "string", description: "JavaScript code to evaluate (eval action)" },
746
881
  },
747
882
  },
748
883
  },
749
- capture_screenshot: { type: "boolean", description: "Include base64 screenshot (default: true)" },
884
+ capture_screenshot: { type: "boolean", description: "Include base64 screenshot (default: false)" },
750
885
  env: ENV_PARAM,
751
886
  },
752
887
  required: ["view_id", "company_id"],
@@ -899,7 +1034,7 @@ const toolHandlers = {
899
1034
  // Context & Info
900
1035
  // ─────────────────────────────────────────────────────────────────────────
901
1036
  async gufi_whoami(params) {
902
- const env = params.env || DEFAULT_ENV;
1037
+ const env = params.env || getDefaultEnv();
903
1038
  const config = loadConfig();
904
1039
  const loggedIn = isLoggedIn();
905
1040
  let companies = [];
@@ -1143,106 +1278,70 @@ const toolHandlers = {
1143
1278
  hint: "Use gufi_docs({ topic: 'fields' }) to read about field types, or gufi_docs({ search: 'currency' }) to search.",
1144
1279
  };
1145
1280
  },
1281
+ // ─────────────────────────────────────────────────────────────────────────
1282
+ // Automations
1283
+ // ─────────────────────────────────────────────────────────────────────────
1146
1284
  async gufi_automation_integrations(params) {
1147
1285
  const { name } = params;
1148
- // Static list of available integrations and their methods
1149
- // Credentials are passed from automations using env.* - no configuration status to check
1150
- const integrations = {
1151
- stripe: {
1152
- name: "stripe",
1153
- description: "Stripe payment processing - Create checkout sessions, handle payments",
1154
- category: "payments",
1155
- docs: "https://stripe.com/docs/api",
1156
- methods: [
1157
- { name: "createCheckoutSession", description: "Create a Stripe Checkout Session for payment", params: ["secretKey", "lineItems", "customerEmail?", "successUrl?", "cancelUrl?"] },
1158
- ],
1159
- },
1160
- nayax: {
1161
- name: "nayax",
1162
- description: "Nayax vending machine telemetry - Sync machines, products, sales, alerts",
1163
- category: "vending",
1164
- docs: "https://developers.nayax.com/",
1165
- methods: [
1166
- { name: "sync_machines", description: "Sync all machines from Nayax", params: ["token", "tableName"] },
1167
- { name: "sync_products", description: "Sync products from Nayax", params: ["token", "tableName"] },
1168
- { name: "sync_last_sales", description: "Sync recent sales", params: ["token", "tableName", "machinesTable", "productsTable"] },
1169
- { name: "sync_alerts", description: "Sync machine alerts", params: ["token", "tableName", "machinesTable", "productsTable"] },
1170
- { name: "sync_machine_products", description: "Sync planograms", params: ["token", "tableName", "machinesTable", "productsTable"] },
1171
- { name: "sync_pick_list", description: "Sync low stock items", params: ["token", "tableName", "machinesTable", "productsTable"] },
1172
- { name: "sync_single_machine_capacity", description: "Sync and calculate capacity for one machine", params: ["machineId", "token", "machineProductsTable", "machinesTable", "productsTable"] },
1173
- ],
1174
- },
1175
- ourvend: {
1176
- name: "ourvend",
1177
- description: "OurVend (RedPanda) vending system - Sync groups, machines, products, sales",
1178
- category: "vending",
1179
- docs: "https://ourvend.com/api",
1180
- methods: [
1181
- { name: "sync_groups", description: "Sync machine groups", params: ["user", "password", "tableName"] },
1182
- { name: "sync_machines", description: "Sync machines", params: ["user", "password", "tableName", "groupsTable"] },
1183
- { name: "sync_products", description: "Sync products", params: ["user", "password", "tableName"] },
1184
- { name: "sync_sales", description: "Sync sales", params: ["user", "password", "tableName", "machinesTable", "productsTable", "groupsTable", "dateFrom?", "dateTo?"] },
1185
- ],
1186
- },
1187
- tns: {
1188
- name: "tns",
1189
- description: "TNS Colombian accounting - Sync products and categories",
1190
- category: "accounting",
1191
- docs: "https://tns.com.co/api",
1192
- methods: [
1193
- { name: "sync_products", description: "Sync products from TNS", params: ["productsTable", "categoriesTable", "env?"] },
1194
- { name: "sync_products_from_sales", description: "Sync products that appear in sales", params: ["productsTable", "categoriesTable", "salesTable", "env?"] },
1195
- { name: "sync_all_products", description: "Sync all products from catalog", params: ["productsTable", "categoriesTable", "env?"] },
1196
- ],
1197
- },
1198
- };
1286
+ // Read integration docs from centralized docs/dev-guide/
1287
+ const allDocs = getIntegrationDocs();
1199
1288
  if (name) {
1200
- const integration = integrations[name];
1201
- if (!integration) {
1289
+ // Get specific integration doc
1290
+ const doc = getIntegrationDoc(name);
1291
+ if (!doc) {
1292
+ // List available integrations on error
1293
+ const available = allDocs
1294
+ .filter(d => d.id !== 'integrations-overview')
1295
+ .map(d => d.id);
1202
1296
  return {
1203
1297
  error: `Integration '${name}' not found`,
1204
- available: Object.keys(integrations),
1298
+ available_integrations: available,
1299
+ hint: "Use one of the available integration names, or use gufi_automation_integrations() to see all",
1205
1300
  };
1206
1301
  }
1207
1302
  return {
1208
- ...integration,
1209
- methods: integration.methods.map((m) => ({
1210
- ...m,
1211
- usage: `api.${name}.${m.name}({ ${m.params.map(p => p.replace("?", "")).join(", ")} })`,
1212
- })),
1213
- usage_note: "Credentials (tokens, passwords, keys) are passed from automations using env.* variables. Name your env vars however you prefer.",
1214
- example: `
1215
- // In automation:
1216
- async function my_automation(context, api, logger) {
1217
- const { env } = context;
1218
- const result = await api.${name}.${integration.methods[0].name}({
1219
- ${integration.methods[0].params.filter(p => !p.endsWith("?")).map(p => `${p}: env.MY_${p.toUpperCase()}`).join(",\n ")}
1220
- });
1221
- return result;
1222
- }`,
1303
+ name: doc.id,
1304
+ title: doc.title,
1305
+ description: doc.description,
1306
+ documentation: doc.content,
1307
+ usage: {
1308
+ step1: "Set up credentials in Environment Variables",
1309
+ step2: `In automation script: api.${doc.id}.methodName({ params })`,
1310
+ step3: "See documentation above for methods and examples",
1311
+ },
1223
1312
  };
1224
1313
  }
1225
1314
  // List all integrations
1315
+ const integrations = allDocs.filter(d => d.id !== 'integrations-overview');
1316
+ const overview = allDocs.find(d => d.id === 'integrations-overview');
1226
1317
  return {
1227
- integrations: Object.values(integrations).map((i) => ({
1228
- name: i.name,
1229
- description: i.description,
1230
- category: i.category,
1231
- methods: i.methods.map((m) => m.name),
1318
+ overview: overview ? {
1319
+ title: overview.title,
1320
+ description: overview.description,
1321
+ content: overview.content,
1322
+ } : {
1323
+ description: "Connect automations to external services",
1324
+ usage: "api.{integration}.{method}() or api.http() for custom",
1325
+ credentials: "Use env.* variables for API keys",
1326
+ },
1327
+ integrations: integrations.map(d => ({
1328
+ name: d.id,
1329
+ title: d.title,
1330
+ description: d.description,
1232
1331
  })),
1233
- usage_note: "Credentials are passed from automations using env.* variables. Name your env vars however you prefer.",
1234
- example: `
1235
- // In automation, use api.{integration}.{method}():
1236
- const result = await api.stripe.createCheckoutSession({
1237
- secretKey: env.MY_STRIPE_KEY, // Your env var name
1238
- lineItems: [{ name: 'Product', amount: 1999, quantity: 1 }],
1239
- });`,
1332
+ custom_http: {
1333
+ description: "For APIs without built-in integration, use api.http()",
1334
+ example: `await api.http({
1335
+ url: 'https://api.example.com/endpoint',
1336
+ method: 'POST',
1337
+ headers: { Authorization: 'Bearer ' + env.API_KEY },
1338
+ body: { data: 'value' }
1339
+ })`,
1340
+ },
1341
+ hint: "Use gufi_automation_integrations({ name: 'stripe' }) for complete documentation of an integration",
1240
1342
  };
1241
1343
  },
1242
- // ─────────────────────────────────────────────────────────────────────────
1243
- // Automations
1244
- // ─────────────────────────────────────────────────────────────────────────
1245
- async gufi_automation(params) {
1344
+ async gufi_automation_scripts(params) {
1246
1345
  const { action, company_id, id, name, code, description, env } = params;
1247
1346
  switch (action) {
1248
1347
  case "list": {
@@ -1322,45 +1421,51 @@ const result = await api.stripe.createCheckoutSession({
1322
1421
  throw new Error(`Unknown action: ${action}. Use: list, get, create, update, delete`);
1323
1422
  }
1324
1423
  },
1325
- async gufi_automation_test(params) {
1424
+ async gufi_automation_script_test(params) {
1326
1425
  const { company_id, script_name, entity, trigger_event, row, input, env } = params;
1327
- // Resolve entity to get module_id and table_id
1328
- const schema = await getCompanySchema(company_id, env);
1329
- if (!schema?.modules) {
1330
- throw new Error("Cannot get schema. Make sure company_id is correct.");
1426
+ // Validate: need at least row or input
1427
+ if (!row && !input) {
1428
+ throw new Error("Must provide 'row' or 'input' (or both)");
1331
1429
  }
1332
- // Parse entity (module.entity format)
1333
- const parts = entity.split(".");
1334
- if (parts.length !== 2) {
1335
- throw new Error(`Invalid entity format: "${entity}". Use module.entity (e.g., 'ventas.facturas')`);
1336
- }
1337
- const [moduleName, entityName] = parts;
1338
- // Find module and entity IDs
1339
1430
  let moduleId = null;
1340
1431
  let tableId = null;
1341
1432
  let tableName = null;
1342
- for (const mod of schema.modules) {
1343
- const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
1344
- mod.label?.toLowerCase() === moduleName.toLowerCase();
1345
- if (!modMatch)
1346
- continue;
1347
- moduleId = mod.id;
1348
- for (const ent of mod.entities || []) {
1349
- const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
1350
- ent.label?.toLowerCase() === entityName.toLowerCase();
1351
- if (entMatch) {
1352
- tableId = ent.id;
1353
- tableName = `m${moduleId}_t${tableId}`;
1354
- break;
1433
+ // Resolve entity if provided (optional for standalone scripts)
1434
+ if (entity) {
1435
+ const schema = await getCompanySchema(company_id, env);
1436
+ if (!schema?.modules) {
1437
+ throw new Error("Cannot get schema. Make sure company_id is correct.");
1438
+ }
1439
+ // Parse entity (module.entity format)
1440
+ const parts = entity.split(".");
1441
+ if (parts.length !== 2) {
1442
+ throw new Error(`Invalid entity format: "${entity}". Use module.entity (e.g., 'ventas.facturas')`);
1443
+ }
1444
+ const [moduleName, entityName] = parts;
1445
+ // Find module and entity IDs
1446
+ for (const mod of schema.modules) {
1447
+ const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
1448
+ mod.label?.toLowerCase() === moduleName.toLowerCase();
1449
+ if (!modMatch)
1450
+ continue;
1451
+ moduleId = mod.id;
1452
+ for (const ent of mod.entities || []) {
1453
+ const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
1454
+ ent.label?.toLowerCase() === entityName.toLowerCase();
1455
+ if (entMatch) {
1456
+ tableId = ent.id;
1457
+ tableName = `m${moduleId}_t${tableId}`;
1458
+ break;
1459
+ }
1355
1460
  }
1461
+ if (tableId)
1462
+ break;
1463
+ }
1464
+ if (!moduleId || !tableId) {
1465
+ throw new Error(`Entity "${entity}" not found in schema`);
1356
1466
  }
1357
- if (tableId)
1358
- break;
1359
- }
1360
- if (!moduleId || !tableId) {
1361
- throw new Error(`Entity "${entity}" not found in schema`);
1362
1467
  }
1363
- // Call test endpoint (same as backend: accepts row and/or input)
1468
+ // Call test endpoint
1364
1469
  const response = await apiRequest("/api/automation-scripts/test", {
1365
1470
  method: "POST",
1366
1471
  body: JSON.stringify({
@@ -1370,29 +1475,37 @@ const result = await api.stripe.createCheckoutSession({
1370
1475
  trigger_event,
1371
1476
  table_id: tableId,
1372
1477
  table_name: tableName,
1373
- row,
1478
+ row: row || null,
1374
1479
  input: input || null,
1375
1480
  }),
1376
1481
  headers: { "X-Company-ID": company_id },
1377
1482
  }, company_id, true, env);
1378
- return {
1483
+ const result = {
1379
1484
  success: true,
1380
1485
  script_name,
1381
- entity,
1382
1486
  trigger_event,
1383
- result: response.data || response,
1384
1487
  };
1488
+ if (entity) {
1489
+ result.entity = entity;
1490
+ result.resolved_table = tableName;
1491
+ }
1492
+ // Include script output if available
1493
+ const scriptOutput = response.data || response;
1494
+ if (scriptOutput && Object.keys(scriptOutput).length > 0) {
1495
+ result.output = scriptOutput;
1496
+ }
1497
+ return result;
1385
1498
  },
1386
- async gufi_triggers(params) {
1387
- const { entity_id, company_id, config, env } = params;
1388
- if (config) {
1389
- // SET triggers
1499
+ async gufi_automation_meta(params) {
1500
+ const { entity_id, company_id, automations, env } = params;
1501
+ if (automations) {
1502
+ // SET triggers - backend expects array: [{ trigger: 'insert', function_name: 'my_script' }]
1390
1503
  await apiRequest(`/api/entities/${entity_id}/automations`, {
1391
1504
  method: "PUT",
1392
- body: JSON.stringify(config),
1505
+ body: JSON.stringify(automations),
1393
1506
  headers: { "X-Company-ID": company_id },
1394
1507
  }, company_id, true, env);
1395
- return { success: true, entity_id, config };
1508
+ return { success: true, entity_id, automations };
1396
1509
  }
1397
1510
  else {
1398
1511
  // GET triggers
@@ -1407,7 +1520,7 @@ const result = await api.stripe.createCheckoutSession({
1407
1520
  };
1408
1521
  }
1409
1522
  },
1410
- async gufi_executions(params) {
1523
+ async gufi_automation_executions(params) {
1411
1524
  let endpoint = `/api/automation-executions?limit=${params.limit || 20}`;
1412
1525
  if (params.script_name) {
1413
1526
  endpoint += `&script=${encodeURIComponent(params.script_name)}`;
@@ -1574,58 +1687,136 @@ const result = await api.stripe.createCheckoutSession({
1574
1687
  const viewId = params.view_id;
1575
1688
  const companyId = params.company_id;
1576
1689
  const env = params.env;
1577
- // Get view info for package_id (pass company_id for access check)
1690
+ const useLocal = canWriteLocal();
1691
+ // Get view info
1578
1692
  const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, companyId, true, env);
1579
1693
  const view = viewResponse.data || viewResponse;
1580
- // 💜 Backend returns pk_id, not id
1581
1694
  if (!view || !view.pk_id) {
1582
1695
  return { error: `View ${viewId} not found` };
1583
1696
  }
1584
- const viewKey = `view_${viewId}`;
1585
- const packageId = view.package_id || 0;
1586
- // Pull using sync.ts
1587
- const result = await pullView(viewId, viewKey, packageId);
1588
- return {
1589
- success: true,
1590
- view_id: viewId,
1591
- name: view.name,
1592
- package_id: packageId || null,
1593
- local_path: result.dir,
1594
- files_count: result.fileCount,
1595
- _hint: `📥 View downloaded to ${result.dir}. Use Read/Edit tools to modify files. Then gufi_view_push({ view_id: ${viewId} }) to upload.`,
1596
- };
1697
+ // Get view files
1698
+ const filesResponse = await apiRequest(`/api/marketplace/views/${viewId}/files`, {}, companyId, true, env);
1699
+ const files = Array.isArray(filesResponse) ? filesResponse : (filesResponse.data || []);
1700
+ if (useLocal) {
1701
+ // 💜 CLI: Save to ~/gufi-dev/view_<id>/
1702
+ const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
1703
+ fs.mkdirSync(viewDir, { recursive: true });
1704
+ const fileMeta = {};
1705
+ for (const file of files) {
1706
+ const filePath = path.join(viewDir, file.file_path.replace(/^\//, ""));
1707
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1708
+ fs.writeFileSync(filePath, file.content);
1709
+ fileMeta[file.file_path] = {
1710
+ hash: crypto.createHash("sha256").update(file.content, "utf-8").digest("hex")
1711
+ };
1712
+ }
1713
+ // Save metadata
1714
+ const meta = {
1715
+ viewId,
1716
+ viewName: `view_${viewId}`,
1717
+ packageId: view.package_id || 0,
1718
+ lastSync: new Date().toISOString(),
1719
+ files: fileMeta,
1720
+ };
1721
+ fs.writeFileSync(path.join(viewDir, ".gufi-view.json"), JSON.stringify(meta, null, 2));
1722
+ return {
1723
+ success: true,
1724
+ view_id: viewId,
1725
+ name: view.name,
1726
+ package_id: view.package_id || null,
1727
+ local_path: viewDir,
1728
+ storage: "local",
1729
+ files_count: files.length,
1730
+ _hint: `📥 View downloaded to ${viewDir}/. Use Read/Edit tools to modify files. Then gufi_view_push({ view_id: ${viewId}, env: '${env || 'prod'}' }) to upload.`,
1731
+ };
1732
+ }
1733
+ else {
1734
+ // 💜 Web: Save to Claude Workspace BD
1735
+ const saveResponse = await apiRequest(`/api/claude/workspace/view`, {
1736
+ method: "POST",
1737
+ body: JSON.stringify({ view_id: viewId, files }),
1738
+ }, companyId, true, env);
1739
+ const saveResult = saveResponse.data || saveResponse;
1740
+ const viewFolder = saveResult?.folder || `views/view_${viewId}`;
1741
+ return {
1742
+ success: true,
1743
+ view_id: viewId,
1744
+ name: view.name,
1745
+ package_id: view.package_id || null,
1746
+ local_path: `~/workspace/${viewFolder}`,
1747
+ storage: "workspace",
1748
+ files_count: files.length,
1749
+ _hint: `📥 View downloaded to Claude Workspace. Use Read/Edit tools to modify files. Then gufi_view_push({ view_id: ${viewId} }) to upload.`,
1750
+ };
1751
+ }
1597
1752
  },
1598
1753
  async gufi_view_push(params) {
1599
- let viewDir;
1600
- let viewId = params.view_id;
1754
+ const viewId = params.view_id;
1601
1755
  const env = params.env;
1602
- // If view_id provided, get dir from it
1603
- if (viewId) {
1604
- viewDir = getViewDir(`view_${viewId}`);
1756
+ const useLocal = canWriteLocal();
1757
+ if (!viewId) {
1758
+ throw new Error("view_id is required");
1759
+ }
1760
+ let files = [];
1761
+ if (useLocal) {
1762
+ // 💜 CLI: Read from ~/gufi-dev/view_<id>/
1763
+ const viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
1764
+ if (!fs.existsSync(viewDir)) {
1765
+ throw new Error(`View directory not found: ${viewDir}. Run gufi_view_pull first.`);
1766
+ }
1767
+ // Get all files recursively
1768
+ const getFiles = (dir, prefix = "") => {
1769
+ const result = [];
1770
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1771
+ if (entry.name.startsWith("."))
1772
+ continue;
1773
+ const fullPath = path.join(prefix, entry.name);
1774
+ if (entry.isDirectory()) {
1775
+ result.push(...getFiles(path.join(dir, entry.name), fullPath));
1776
+ }
1777
+ else {
1778
+ result.push(fullPath);
1779
+ }
1780
+ }
1781
+ return result;
1782
+ };
1783
+ const localFiles = getFiles(viewDir);
1784
+ const langMap = {
1785
+ ".ts": "typescript", ".tsx": "typescript",
1786
+ ".js": "javascript", ".jsx": "javascript",
1787
+ ".css": "css", ".json": "json", ".md": "markdown",
1788
+ };
1789
+ files = localFiles.map(file => ({
1790
+ file_path: "/" + file,
1791
+ content: fs.readFileSync(path.join(viewDir, file), "utf-8"),
1792
+ language: langMap[path.extname(file).toLowerCase()] || "text",
1793
+ is_entry_point: file === "index.tsx",
1794
+ }));
1605
1795
  }
1606
- // Try to load meta to get view_id if not provided
1607
- if (!viewId && viewDir) {
1608
- const meta = loadViewMeta(viewDir);
1609
- if (meta)
1610
- viewId = meta.viewId;
1796
+ else {
1797
+ // 💜 Web: Read from Claude Workspace BD
1798
+ const workspaceResponse = await apiRequest(`/api/claude/workspace/view/${viewId}/files`, {}, undefined, true, env);
1799
+ files = workspaceResponse.data || workspaceResponse || [];
1611
1800
  }
1612
- // Push using sync.ts (with optional message for snapshot)
1613
- const result = await pushView(viewDir, params.message);
1801
+ // Push to server using apiRequest (supports env)
1802
+ const result = await apiRequest(`/api/marketplace/views/${viewId}/files/bulk`, {
1803
+ method: "POST",
1804
+ body: JSON.stringify({ files, message: params.message, sync: true }),
1805
+ }, undefined, true, env);
1614
1806
  // Get view info for package
1615
1807
  let packageInfo = null;
1616
- if (viewId) {
1617
- const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, undefined, true, env);
1618
- const view = viewResponse.data || viewResponse;
1619
- if (view.package_id) {
1620
- packageInfo = { id: view.package_id, publish_cmd: `gufi package:publish ${view.package_id}` };
1621
- }
1808
+ const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, undefined, true, env);
1809
+ const view = viewResponse.data || viewResponse;
1810
+ if (view?.package_id) {
1811
+ packageInfo = { id: view.package_id, publish_cmd: `gufi package:publish ${view.package_id}` };
1622
1812
  }
1623
1813
  return {
1624
1814
  success: true,
1625
- pushed_files: result.pushed,
1815
+ pushed_files: files.length,
1626
1816
  snapshot: result.snapshot || null,
1627
- status_message: result.pushed > 0
1628
- ? `📤 Pushed ${result.pushed} file(s) to draft (snapshot #${result.snapshot})`
1817
+ storage: useLocal ? "local" : "workspace",
1818
+ status_message: files.length > 0
1819
+ ? `📤 Pushed ${files.length} file(s) to draft (snapshot #${result.snapshot})`
1629
1820
  : "No changes to push",
1630
1821
  package: packageInfo,
1631
1822
  _hint: packageInfo
@@ -1640,9 +1831,9 @@ const result = await api.stripe.createCheckoutSession({
1640
1831
  body: JSON.stringify({
1641
1832
  view_id,
1642
1833
  company_id,
1643
- timeout: timeout || 15000,
1834
+ timeout: timeout || 25000,
1644
1835
  actions: actions || [],
1645
- capture_screenshot: capture_screenshot !== false,
1836
+ capture_screenshot: capture_screenshot === true,
1646
1837
  }),
1647
1838
  }, company_id, true, env);
1648
1839
  // Format response for Claude readability
@@ -1673,6 +1864,10 @@ const result = await api.stripe.createCheckoutSession({
1673
1864
  }
1674
1865
  // Add DOM info
1675
1866
  response.dom = result.dom;
1867
+ // Add action results (eval results, getText, etc.)
1868
+ if (result.actions?.length > 0) {
1869
+ response.actions = result.actions;
1870
+ }
1676
1871
  // Add screenshot (Claude can view base64 images)
1677
1872
  if (result.screenshot) {
1678
1873
  response.screenshot = result.screenshot;
@@ -1937,7 +2132,7 @@ async function generateViewContextMcp(viewId, includeConcepts, env) {
1937
2132
  }
1938
2133
  // If has automations, suggest viewing them
1939
2134
  if (autoCount > 0 && viewCompanyId) {
1940
- nextActions.push(`See automations: gufi_automations({ company_id: "${viewCompanyId}" })`);
2135
+ nextActions.push(`See automations: gufi_automation_scripts({ action: "list", company_id: "${viewCompanyId}" })`);
1941
2136
  }
1942
2137
  // Always: docs for field types
1943
2138
  nextActions.push(`Field types reference: gufi_docs({ topic: "fields" })`);
@@ -2040,89 +2235,25 @@ async function generatePackageContextMcp(packageId, includeConcepts, env) {
2040
2235
  return result;
2041
2236
  }
2042
2237
  async function generateCompanyContextMcp(companyId, includeConcepts) {
2043
- // Use /api/company/schema which has full entity info
2044
- const schemaResponse = await apiRequest(`/api/company/schema`, {
2045
- headers: { "X-Company-ID": String(companyId) },
2046
- }, String(companyId));
2047
- const modulesRaw = schemaResponse.modules || schemaResponse.data?.modules || [];
2238
+ // 💜 Use centralized /api/schema/export-claude endpoint
2239
+ // This returns the same complete text as "Copy All for Claude" in the frontend
2240
+ const exportText = await apiRequest(`/api/schema/export-claude`, {}, String(companyId));
2241
+ // Get automation scripts for summary
2048
2242
  let automations = [];
2049
2243
  try {
2050
2244
  const automationsResponse = await apiRequest(`/api/automation-scripts`, {}, String(companyId));
2051
2245
  automations = Array.isArray(automationsResponse) ? automationsResponse : automationsResponse.data || [];
2052
2246
  }
2053
2247
  catch { }
2054
- const modulesInfo = [];
2055
- for (const mod of modulesRaw) {
2056
- // Find moduleId from entities
2057
- let moduleId;
2058
- for (const sub of mod.submodules || []) {
2059
- for (const ent of sub.entities || []) {
2060
- if (ent.ui?.moduleId) {
2061
- moduleId = ent.ui.moduleId;
2062
- break;
2063
- }
2064
- }
2065
- if (moduleId)
2066
- break;
2067
- }
2068
- const entities = [];
2069
- for (const sub of mod.submodules || []) {
2070
- for (const ent of sub.entities || []) {
2071
- const entityId = ent.ui?.entityId || ent.pk_id || ent.id;
2072
- entities.push({
2073
- name: ent.name,
2074
- label: ent.label,
2075
- table: moduleId && entityId ? `m${moduleId}_t${entityId}` : null,
2076
- fieldCount: ent.fields?.length || 0,
2077
- fields: (ent.fields || []).slice(0, 10).map((f) => ({
2078
- name: f.name,
2079
- type: f.type,
2080
- required: f.required || false,
2081
- })),
2082
- });
2083
- }
2084
- }
2085
- modulesInfo.push({
2086
- id: moduleId,
2087
- name: mod.name || mod.displayName,
2088
- displayName: mod.displayName || mod.label,
2089
- entities,
2090
- });
2091
- }
2092
- // Build summary and next_actions
2093
- const totalEntities = modulesInfo.reduce((acc, m) => acc + (m.entities?.length || 0), 0);
2094
- const summary = [
2095
- `Company ID: ${companyId}`,
2096
- `${modulesInfo.length} module(s)`,
2097
- `${totalEntities} table(s)`,
2098
- `${automations.length} automation(s)`,
2099
- ].join(" | ");
2100
- const nextActions = [];
2101
- // Suggest viewing data from first table
2102
- if (modulesInfo.length > 0 && modulesInfo[0].entities?.length > 0) {
2103
- const firstTable = modulesInfo[0].entities[0].table;
2104
- if (firstTable) {
2105
- nextActions.push(`View data: gufi_rows({ table: "${firstTable}", limit: 5 })`);
2106
- }
2107
- }
2108
- // If has modules, suggest editing one
2109
- if (modulesInfo.length > 0 && modulesInfo[0].id) {
2110
- nextActions.push(`Edit module structure: gufi_module({ module_id: "${modulesInfo[0].id}" })`);
2111
- }
2112
- // If has automations, suggest viewing them
2113
- if (automations.length > 0) {
2114
- nextActions.push(`View automation code: gufi_automation({ automation_id: "${automations[0].id}" })`);
2115
- }
2116
- nextActions.push(`Field types reference: gufi_docs({ topic: "fields" })`);
2117
2248
  const result = {
2118
2249
  type: "company",
2119
- summary,
2120
- next_actions: nextActions,
2121
2250
  company_id: companyId,
2122
- modules: modulesInfo,
2123
- automations: automations.map((a) => ({
2251
+ // The full schema export with ALL details (fields, automations, permissions, etc.)
2252
+ schema: exportText,
2253
+ // Also provide structured data for programmatic access
2254
+ automation_scripts: automations.map((a) => ({
2124
2255
  id: a.id,
2125
- name: a.name,
2256
+ name: a.name || a.function_name,
2126
2257
  description: a.description,
2127
2258
  })),
2128
2259
  commands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gufi-cli",
3
- "version": "0.1.41",
3
+ "version": "0.1.44",
4
4
  "description": "CLI for developing Gufi Marketplace views locally with Claude Code",
5
5
  "bin": {
6
6
  "gufi": "./bin/gufi.js"