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 +12 -8
- package/README.md +6 -7
- package/dist/commands/companies.js +2 -2
- package/dist/commands/context.js +3 -5
- package/dist/commands/integrations.d.ts +10 -0
- package/dist/commands/integrations.js +53 -0
- package/dist/index.js +8 -0
- package/dist/lib/sync.js +1 -0
- package/dist/mcp.js +408 -277
- package/package.json +1 -1
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 (
|
|
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 -
|
|
58
|
+
## MCP Server - 15 Tools
|
|
59
59
|
|
|
60
|
-
El servidor MCP está en `src/mcp.ts` y expone **
|
|
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** | `
|
|
71
|
-
| | `
|
|
72
|
-
| | `
|
|
73
|
-
|
|
|
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
|
-
| `
|
|
513
|
-
| `
|
|
514
|
-
| `
|
|
515
|
-
| `
|
|
516
|
-
| `
|
|
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
|
|
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
|
|
1499
|
+
// 💜 Execute automation
|
|
1500
1500
|
console.log(chalk.yellow("\n ⏳ Ejecutando..."));
|
|
1501
|
-
const result = await apiRequest("/api/automation-scripts/
|
|
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({
|
package/dist/commands/context.js
CHANGED
|
@@ -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
|
|
370
|
+
md += `Puedes ejecutar automations desde la vista con \`gufi.automation()\`:\n`;
|
|
371
371
|
md += "```typescript\n";
|
|
372
|
-
md += `await gufi.
|
|
373
|
-
md += `
|
|
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
|
-
|
|
18
|
-
|
|
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 -
|
|
36
|
-
|
|
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'
|
|
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 '
|
|
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
|
|
153
|
+
// Undefined/null → default to config's currentEnv
|
|
58
154
|
if (env === undefined || env === null || env === "") {
|
|
59
|
-
return
|
|
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[
|
|
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(
|
|
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(
|
|
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: "
|
|
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: "
|
|
570
|
-
description: getDesc("
|
|
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: "
|
|
587
|
-
description:
|
|
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
|
|
594
|
-
trigger_event: { type: "string", description: "Trigger event: on_create, on_update, on_delete, on_click" },
|
|
595
|
-
row: { type: "object", description: "Row data
|
|
596
|
-
input: { type: "object", description: "Input data
|
|
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", "
|
|
717
|
+
required: ["company_id", "script_name", "trigger_event"],
|
|
600
718
|
},
|
|
601
719
|
},
|
|
602
720
|
{
|
|
603
|
-
name: "
|
|
604
|
-
description: getDesc("
|
|
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
|
-
|
|
611
|
-
type: "
|
|
612
|
-
description: "
|
|
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: "
|
|
621
|
-
description: getDesc("
|
|
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:
|
|
856
|
+
timeout: { type: "number", description: "Navigation timeout in ms (default: 25000)" },
|
|
736
857
|
actions: {
|
|
737
858
|
type: "array",
|
|
738
|
-
description:
|
|
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
|
|
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:
|
|
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 ||
|
|
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
|
-
//
|
|
1149
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
description:
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
|
1424
|
+
async gufi_automation_script_test(params) {
|
|
1326
1425
|
const { company_id, script_name, entity, trigger_event, row, input, env } = params;
|
|
1327
|
-
//
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if (!
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1387
|
-
const { entity_id, company_id,
|
|
1388
|
-
if (
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
1600
|
-
let viewId = params.view_id;
|
|
1754
|
+
const viewId = params.view_id;
|
|
1601
1755
|
const env = params.env;
|
|
1602
|
-
|
|
1603
|
-
if (viewId) {
|
|
1604
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
1608
|
-
const
|
|
1609
|
-
|
|
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
|
|
1613
|
-
const result = await
|
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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:
|
|
1815
|
+
pushed_files: files.length,
|
|
1626
1816
|
snapshot: result.snapshot || null,
|
|
1627
|
-
|
|
1628
|
-
|
|
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 ||
|
|
1834
|
+
timeout: timeout || 25000,
|
|
1644
1835
|
actions: actions || [],
|
|
1645
|
-
capture_screenshot: capture_screenshot
|
|
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:
|
|
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/
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
-
|
|
2123
|
-
|
|
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: {
|