gufi-cli 0.1.39 → 0.1.41
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 +4 -3
- package/README.md +5 -4
- package/dist/commands/install.d.ts +21 -0
- package/dist/commands/setup-claude.d.ts +6 -0
- package/dist/commands/setup-claude.js +137 -0
- package/dist/mcp.js +191 -0
- package/package.json +1 -1
- package/dist/commands/assistant.d.ts +0 -22
- package/dist/commands/assistant.js +0 -377
package/CLAUDE.md
CHANGED
|
@@ -55,9 +55,9 @@ gufi config:prod
|
|
|
55
55
|
}
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
## MCP Server -
|
|
58
|
+
## MCP Server - 13 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 **13 tools** para que Claude interactúe con Gufi.
|
|
61
61
|
|
|
62
62
|
### Tools organizados por categoría
|
|
63
63
|
|
|
@@ -66,7 +66,7 @@ El servidor MCP está en `src/mcp.ts` y expone **12 tools** para que Claude inte
|
|
|
66
66
|
| **Context** | `gufi_context` | Schema completo (company, view, package) |
|
|
67
67
|
| | `gufi_whoami` | Usuario, entorno, empresas |
|
|
68
68
|
| | `gufi_docs` | Documentación (`docs/mcp/`) |
|
|
69
|
-
| **Schema** | `gufi_schema_modify` | Operaciones
|
|
69
|
+
| **Schema** | `gufi_schema_modify` | Operaciones atómicas (add/update/remove) |
|
|
70
70
|
| **Automations** | `gufi_automation` | Scripts: list, get, create |
|
|
71
71
|
| | `gufi_triggers` | Ver/configurar triggers |
|
|
72
72
|
| | `gufi_executions` | Historial de ejecuciones |
|
|
@@ -74,6 +74,7 @@ El servidor MCP está en `src/mcp.ts` y expone **12 tools** para que Claude inte
|
|
|
74
74
|
| **Env** | `gufi_env` | Variables: list, set, delete |
|
|
75
75
|
| **Views** | `gufi_view_pull` | Descargar vista a local |
|
|
76
76
|
| | `gufi_view_push` | Subir cambios a draft |
|
|
77
|
+
| | `gufi_view_test` | Test en headless browser (errores, screenshot) |
|
|
77
78
|
| **Packages** | `gufi_package` | list, get, create, delete, add_module, remove_module, publish |
|
|
78
79
|
|
|
79
80
|
### Añadir un nuevo tool MCP
|
package/README.md
CHANGED
|
@@ -173,11 +173,12 @@ const { rows } = await api.query(...); // ERROR!
|
|
|
173
173
|
### Llamar automations desde vistas
|
|
174
174
|
|
|
175
175
|
```tsx
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
input: { record_id: 123 },
|
|
176
|
+
// 💜 gufi.automation() - Simple y directo
|
|
177
|
+
const result = await gufi.automation('my_automation', {
|
|
178
|
+
record_id: 123
|
|
180
179
|
});
|
|
180
|
+
|
|
181
|
+
// La automation tiene acceso a api.stripe.*, api.nayax.*, etc.
|
|
181
182
|
```
|
|
182
183
|
|
|
183
184
|
## Row CRUD (Datos)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gufi install - Instala Gufi Claude como servicio de sistema
|
|
3
|
+
*
|
|
4
|
+
* Mac: LaunchAgent que arranca con el sistema
|
|
5
|
+
* Linux: systemd user service
|
|
6
|
+
* Windows: Task Scheduler que arranca con login
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 💜 gufi install - Install as system service
|
|
10
|
+
*/
|
|
11
|
+
export declare function installCommand(options: {
|
|
12
|
+
port?: number;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* 💜 gufi uninstall - Remove system service
|
|
16
|
+
*/
|
|
17
|
+
export declare function uninstallCommand(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* 💜 gufi service:status - Check service status
|
|
20
|
+
*/
|
|
21
|
+
export declare function serviceStatusCommand(): Promise<void>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gufi setup-claude - Configure Claude Code with Gufi MCP
|
|
3
|
+
*
|
|
4
|
+
* Creates workspace and configures MCP server
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { isLoggedIn, loadConfig } from "../lib/config.js";
|
|
12
|
+
const WORKSPACE_DIR = path.join(os.homedir(), "gufi-workspace");
|
|
13
|
+
const CLAUDE_CONFIG_DIR = path.join(os.homedir(), ".claude");
|
|
14
|
+
// Claude Code CLI uses settings.json, NOT claude_desktop_config.json
|
|
15
|
+
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_CONFIG_DIR, "settings.json");
|
|
16
|
+
const CLAUDE_MD_CONTENT = `# Gufi Workspace
|
|
17
|
+
|
|
18
|
+
Este es tu espacio de trabajo para Gufi ERP.
|
|
19
|
+
|
|
20
|
+
## MCP Tools Disponibles
|
|
21
|
+
|
|
22
|
+
Tienes acceso a todas las herramientas de Gufi:
|
|
23
|
+
|
|
24
|
+
- \`gufi_context\` - Obtener contexto de empresa, módulos, entidades
|
|
25
|
+
- \`gufi_rows\` / \`gufi_row\` - Leer datos
|
|
26
|
+
- \`gufi_row_create\` / \`gufi_row_update\` - Crear/actualizar datos
|
|
27
|
+
- \`gufi_schema_modify\` - Modificar schema
|
|
28
|
+
- \`gufi_automations\` - Ver automatizaciones
|
|
29
|
+
- \`gufi_docs\` - Leer documentación
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
1. Ver tus empresas:
|
|
34
|
+
\`\`\`
|
|
35
|
+
Usa gufi_companies para ver las empresas disponibles
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
2. Obtener contexto de una empresa:
|
|
39
|
+
\`\`\`
|
|
40
|
+
Usa gufi_context con company_id para ver módulos y entidades
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
3. Consultar datos:
|
|
44
|
+
\`\`\`
|
|
45
|
+
Usa gufi_rows con el nombre de tabla (ej: m308_t4136)
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
## Documentación
|
|
49
|
+
|
|
50
|
+
Para más información, usa:
|
|
51
|
+
\`\`\`
|
|
52
|
+
gufi_docs({ topic: "overview" })
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
Configurado con: gufi setup-claude
|
|
57
|
+
`;
|
|
58
|
+
export async function setupClaudeCommand() {
|
|
59
|
+
console.log(chalk.magenta(`
|
|
60
|
+
╭──────────────────────────────────────────────────────╮
|
|
61
|
+
│ 🐿️ Gufi Claude Code Setup │
|
|
62
|
+
╰──────────────────────────────────────────────────────╯
|
|
63
|
+
`));
|
|
64
|
+
// Check login
|
|
65
|
+
if (!isLoggedIn()) {
|
|
66
|
+
console.log(chalk.yellow(" ⚠️ Primero debes iniciar sesión."));
|
|
67
|
+
console.log(chalk.gray(" Ejecuta: gufi login\n"));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
console.log(chalk.gray(` Usuario: ${config.email}\n`));
|
|
72
|
+
// Step 1: Create workspace directory
|
|
73
|
+
const spinnerWorkspace = ora("Creando workspace...").start();
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.existsSync(WORKSPACE_DIR)) {
|
|
76
|
+
fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
// Create CLAUDE.md
|
|
79
|
+
const claudeMdPath = path.join(WORKSPACE_DIR, "CLAUDE.md");
|
|
80
|
+
fs.writeFileSync(claudeMdPath, CLAUDE_MD_CONTENT);
|
|
81
|
+
spinnerWorkspace.succeed(chalk.green(`Workspace creado: ${WORKSPACE_DIR}`));
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
spinnerWorkspace.fail(chalk.red(`Error creando workspace: ${err.message}`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
// Step 2: Configure MCP in Claude Code
|
|
88
|
+
const spinnerMcp = ora("Configurando MCP en Claude Code...").start();
|
|
89
|
+
try {
|
|
90
|
+
// Ensure .claude directory exists
|
|
91
|
+
if (!fs.existsSync(CLAUDE_CONFIG_DIR)) {
|
|
92
|
+
fs.mkdirSync(CLAUDE_CONFIG_DIR, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
// Read existing config or create new
|
|
95
|
+
let claudeConfig = { mcpServers: {} };
|
|
96
|
+
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
97
|
+
try {
|
|
98
|
+
const existing = fs.readFileSync(CLAUDE_SETTINGS_FILE, "utf8");
|
|
99
|
+
claudeConfig = JSON.parse(existing);
|
|
100
|
+
if (!claudeConfig.mcpServers) {
|
|
101
|
+
claudeConfig.mcpServers = {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// If parse fails, start fresh
|
|
106
|
+
claudeConfig = { mcpServers: {} };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Add/update Gufi MCP server
|
|
110
|
+
claudeConfig.mcpServers.gufi = {
|
|
111
|
+
command: "gufi",
|
|
112
|
+
args: ["mcp"],
|
|
113
|
+
};
|
|
114
|
+
// Write config
|
|
115
|
+
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(claudeConfig, null, 2));
|
|
116
|
+
spinnerMcp.succeed(chalk.green("MCP de Gufi configurado en Claude Code"));
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
spinnerMcp.fail(chalk.red(`Error configurando MCP: ${err.message}`));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
// Success message
|
|
123
|
+
console.log(chalk.cyan(`
|
|
124
|
+
✅ Setup completado!
|
|
125
|
+
|
|
126
|
+
Tu workspace está en:
|
|
127
|
+
${chalk.white(WORKSPACE_DIR)}
|
|
128
|
+
|
|
129
|
+
Para usar Claude Code con Gufi:
|
|
130
|
+
${chalk.white("gufi claude")}
|
|
131
|
+
|
|
132
|
+
Esto abrirá Claude Code con acceso a:
|
|
133
|
+
• Todas las herramientas MCP de Gufi
|
|
134
|
+
• Tu sesión y empresas
|
|
135
|
+
• Documentación integrada
|
|
136
|
+
`));
|
|
137
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -552,6 +552,16 @@ const TOOLS = [
|
|
|
552
552
|
},
|
|
553
553
|
},
|
|
554
554
|
},
|
|
555
|
+
{
|
|
556
|
+
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.",
|
|
558
|
+
inputSchema: {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: {
|
|
561
|
+
name: { type: "string", description: "Integration name to get detailed info (optional)" },
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
555
565
|
// ─────────────────────────────────────────────────────────────────────────
|
|
556
566
|
// Automations (Business Logic)
|
|
557
567
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -713,6 +723,35 @@ Example: gufi_view_push({ view_id: 13, message: "Fixed bug in chart" })`,
|
|
|
713
723
|
required: [],
|
|
714
724
|
},
|
|
715
725
|
},
|
|
726
|
+
// 🧪 Test view in headless browser
|
|
727
|
+
{
|
|
728
|
+
name: "gufi_view_test",
|
|
729
|
+
description: getDesc("gufi_view_test"),
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: "object",
|
|
732
|
+
properties: {
|
|
733
|
+
view_id: { type: "number", description: "View ID to test" },
|
|
734
|
+
company_id: { type: "string", description: "Company ID for authentication context" },
|
|
735
|
+
timeout: { type: "number", description: "Navigation timeout in ms (default: 15000)" },
|
|
736
|
+
actions: {
|
|
737
|
+
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}]",
|
|
739
|
+
items: {
|
|
740
|
+
type: "object",
|
|
741
|
+
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" },
|
|
745
|
+
ms: { type: "number", description: "Milliseconds for delay action" },
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
capture_screenshot: { type: "boolean", description: "Include base64 screenshot (default: true)" },
|
|
750
|
+
env: ENV_PARAM,
|
|
751
|
+
},
|
|
752
|
+
required: ["view_id", "company_id"],
|
|
753
|
+
},
|
|
754
|
+
},
|
|
716
755
|
// ─────────────────────────────────────────────────────────────────────────
|
|
717
756
|
// Packages (Marketplace Distribution)
|
|
718
757
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1104,6 +1143,102 @@ const toolHandlers = {
|
|
|
1104
1143
|
hint: "Use gufi_docs({ topic: 'fields' }) to read about field types, or gufi_docs({ search: 'currency' }) to search.",
|
|
1105
1144
|
};
|
|
1106
1145
|
},
|
|
1146
|
+
async gufi_automation_integrations(params) {
|
|
1147
|
+
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
|
+
};
|
|
1199
|
+
if (name) {
|
|
1200
|
+
const integration = integrations[name];
|
|
1201
|
+
if (!integration) {
|
|
1202
|
+
return {
|
|
1203
|
+
error: `Integration '${name}' not found`,
|
|
1204
|
+
available: Object.keys(integrations),
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
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
|
+
}`,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
// List all integrations
|
|
1226
|
+
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),
|
|
1232
|
+
})),
|
|
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
|
+
});`,
|
|
1240
|
+
};
|
|
1241
|
+
},
|
|
1107
1242
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1108
1243
|
// Automations
|
|
1109
1244
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1498,6 +1633,62 @@ const toolHandlers = {
|
|
|
1498
1633
|
: "Changes saved (no package - goes directly to production)",
|
|
1499
1634
|
};
|
|
1500
1635
|
},
|
|
1636
|
+
async gufi_view_test(params) {
|
|
1637
|
+
const { view_id, company_id, timeout, actions, capture_screenshot, env } = params;
|
|
1638
|
+
const result = await apiRequest("/api/cli/view-test", {
|
|
1639
|
+
method: "POST",
|
|
1640
|
+
body: JSON.stringify({
|
|
1641
|
+
view_id,
|
|
1642
|
+
company_id,
|
|
1643
|
+
timeout: timeout || 15000,
|
|
1644
|
+
actions: actions || [],
|
|
1645
|
+
capture_screenshot: capture_screenshot !== false,
|
|
1646
|
+
}),
|
|
1647
|
+
}, company_id, true, env);
|
|
1648
|
+
// Format response for Claude readability
|
|
1649
|
+
const response = {
|
|
1650
|
+
success: result.success,
|
|
1651
|
+
view_id: result.view_id,
|
|
1652
|
+
url: result.url,
|
|
1653
|
+
duration_ms: result.duration_ms,
|
|
1654
|
+
};
|
|
1655
|
+
// Add console output if there are errors/warnings
|
|
1656
|
+
if (result.console?.errors?.length > 0) {
|
|
1657
|
+
response.console_errors = result.console.errors;
|
|
1658
|
+
}
|
|
1659
|
+
if (result.console?.warnings?.length > 0) {
|
|
1660
|
+
response.console_warnings = result.console.warnings;
|
|
1661
|
+
}
|
|
1662
|
+
if (result.console?.logs?.length > 0) {
|
|
1663
|
+
response.console_logs = result.console.logs;
|
|
1664
|
+
}
|
|
1665
|
+
// Add JS/network errors
|
|
1666
|
+
if (result.errors?.length > 0) {
|
|
1667
|
+
response.errors = result.errors;
|
|
1668
|
+
}
|
|
1669
|
+
// Add failed API calls
|
|
1670
|
+
const failedCalls = (result.api_calls || []).filter((c) => c.status >= 400);
|
|
1671
|
+
if (failedCalls.length > 0) {
|
|
1672
|
+
response.failed_api_calls = failedCalls;
|
|
1673
|
+
}
|
|
1674
|
+
// Add DOM info
|
|
1675
|
+
response.dom = result.dom;
|
|
1676
|
+
// Add screenshot (Claude can view base64 images)
|
|
1677
|
+
if (result.screenshot) {
|
|
1678
|
+
response.screenshot = result.screenshot;
|
|
1679
|
+
}
|
|
1680
|
+
// Add hint based on results
|
|
1681
|
+
if (!result.success) {
|
|
1682
|
+
response._hint = `View test failed: ${result.error}. Check console_errors and errors for details.`;
|
|
1683
|
+
}
|
|
1684
|
+
else if (result.errors?.length > 0 || result.console?.errors?.length > 0) {
|
|
1685
|
+
response._hint = "View loaded but has errors. Check console_errors and errors arrays.";
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
response._hint = "View loaded successfully. Check screenshot to verify visual appearance.";
|
|
1689
|
+
}
|
|
1690
|
+
return response;
|
|
1691
|
+
},
|
|
1501
1692
|
// gufi_view_create removed - views are created from UI, edited via pull/push
|
|
1502
1693
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1503
1694
|
// Packages
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* gufi assistant - WebSocket bridge between frontend UI and Claude Code
|
|
3
|
-
*
|
|
4
|
-
* 💜 Permite editar vistas desde el frontend usando Claude Code local + MCP
|
|
5
|
-
*
|
|
6
|
-
* Architecture:
|
|
7
|
-
* ┌─────────────┐ WebSocket ┌─────────────────┐
|
|
8
|
-
* │ Frontend │◄──────────────────►│ gufi assistant │
|
|
9
|
-
* │ (UI/Chat) │ localhost:3005 │ ↓ │
|
|
10
|
-
* └─────────────┘ │ Claude Code │
|
|
11
|
-
* │ + MCP Gufi │
|
|
12
|
-
* └─────────────────┘
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* gufi assistant # Start on default port 3005
|
|
16
|
-
* gufi assistant --port 3010 # Custom port
|
|
17
|
-
*/
|
|
18
|
-
interface AssistantFlags {
|
|
19
|
-
port?: string | number;
|
|
20
|
-
}
|
|
21
|
-
export declare function assistantCommand(flags?: AssistantFlags): Promise<void>;
|
|
22
|
-
export {};
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* gufi assistant - WebSocket bridge between frontend UI and Claude Code
|
|
3
|
-
*
|
|
4
|
-
* 💜 Permite editar vistas desde el frontend usando Claude Code local + MCP
|
|
5
|
-
*
|
|
6
|
-
* Architecture:
|
|
7
|
-
* ┌─────────────┐ WebSocket ┌─────────────────┐
|
|
8
|
-
* │ Frontend │◄──────────────────►│ gufi assistant │
|
|
9
|
-
* │ (UI/Chat) │ localhost:3005 │ ↓ │
|
|
10
|
-
* └─────────────┘ │ Claude Code │
|
|
11
|
-
* │ + MCP Gufi │
|
|
12
|
-
* └─────────────────┘
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* gufi assistant # Start on default port 3005
|
|
16
|
-
* gufi assistant --port 3010 # Custom port
|
|
17
|
-
*/
|
|
18
|
-
import chalk from "chalk";
|
|
19
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
20
|
-
import { spawn } from "child_process";
|
|
21
|
-
import { isLoggedIn, getCurrentEnv } from "../lib/config.js";
|
|
22
|
-
const DEFAULT_PORT = 4005;
|
|
23
|
-
const clients = new Map();
|
|
24
|
-
/**
|
|
25
|
-
* Parse JSON output - extract final result and session_id for memory
|
|
26
|
-
*/
|
|
27
|
-
function parseClaudeJsonLine(line, _requestId, _sendResponse, state) {
|
|
28
|
-
try {
|
|
29
|
-
const data = JSON.parse(line);
|
|
30
|
-
// Return the final result with session_id (check FIRST before generic session_id)
|
|
31
|
-
if (data.type === "result" && data.result) {
|
|
32
|
-
return { finalText: data.result, sessionId: data.session_id };
|
|
33
|
-
}
|
|
34
|
-
// Capture session_id from init message (not result)
|
|
35
|
-
if (data.type === "system" && data.session_id) {
|
|
36
|
-
return { sessionId: data.session_id };
|
|
37
|
-
}
|
|
38
|
-
// Log tool usage for terminal visibility
|
|
39
|
-
if (data.type === "assistant" && data.message?.content) {
|
|
40
|
-
for (const block of data.message.content) {
|
|
41
|
-
if (block.type === "tool_use") {
|
|
42
|
-
state.lastToolName = block.name;
|
|
43
|
-
console.log(chalk.blue(` 🔧 ${block.name}`));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (data.type === "user" && data.message?.content) {
|
|
48
|
-
for (const block of data.message.content) {
|
|
49
|
-
if (block.type === "tool_result") {
|
|
50
|
-
console.log(chalk.green(` ✓ completado`));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return {};
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Execute Claude Code with a prompt and stream the response
|
|
62
|
-
* Returns the session_id for memory/resume
|
|
63
|
-
*/
|
|
64
|
-
async function executeClaudeCode(prompt, viewId, viewName, requestId, sendResponse, existingSessionId = null) {
|
|
65
|
-
// Build the full prompt with view context
|
|
66
|
-
const fullPrompt = `
|
|
67
|
-
[Contexto: Estás editando la vista "${viewName}" de Gufi ERP]
|
|
68
|
-
|
|
69
|
-
REGLAS CRÍTICAS:
|
|
70
|
-
1. SIEMPRE usa los tools MCP para hacer cambios reales. NUNCA digas que hiciste algo sin llamar al tool.
|
|
71
|
-
2. Para modificar archivos: gufi_view_file_update({ view_id, file_path, content })
|
|
72
|
-
3. Para leer archivos: gufi_view_files({ view_id })
|
|
73
|
-
4. El view_id es ${viewId} - úsalo directamente
|
|
74
|
-
5. Responde en español y sé conciso
|
|
75
|
-
|
|
76
|
-
PROHIBIDO:
|
|
77
|
-
- Decir "He añadido..." sin haber llamado a gufi_view_file_update
|
|
78
|
-
- Inventar que hiciste cambios
|
|
79
|
-
- Responder sin ejecutar los tools necesarios
|
|
80
|
-
|
|
81
|
-
Instrucción del usuario: ${prompt}
|
|
82
|
-
`.trim();
|
|
83
|
-
console.log(chalk.cyan(`\n 📝 Request: "${prompt.substring(0, 50)}..."`));
|
|
84
|
-
console.log(chalk.gray(` View: "${viewName}" (ID hint: ${viewId})`));
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
// Build args - use --resume if we have a session
|
|
87
|
-
const args = [
|
|
88
|
-
"--dangerously-skip-permissions",
|
|
89
|
-
"-p", // print mode
|
|
90
|
-
"--output-format", "stream-json",
|
|
91
|
-
"--verbose",
|
|
92
|
-
];
|
|
93
|
-
if (existingSessionId) {
|
|
94
|
-
args.push("--resume", existingSessionId);
|
|
95
|
-
console.log(chalk.gray(` 🔧 Resumiendo sesión ${existingSessionId.substring(0, 8)}...`));
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
console.log(chalk.gray(` 🔧 Nueva sesión de Claude...`));
|
|
99
|
-
}
|
|
100
|
-
const claudeProcess = spawn("claude", args, {
|
|
101
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
102
|
-
env: { ...process.env },
|
|
103
|
-
});
|
|
104
|
-
// Write prompt to stdin and close it
|
|
105
|
-
claudeProcess.stdin?.write(fullPrompt);
|
|
106
|
-
claudeProcess.stdin?.end();
|
|
107
|
-
console.log(chalk.yellow(" ⏳ Claude Code ejecutando (PID: " + claudeProcess.pid + ")..."));
|
|
108
|
-
let lineBuffer = "";
|
|
109
|
-
let lastFinalText = "";
|
|
110
|
-
let capturedSessionId = existingSessionId;
|
|
111
|
-
const streamState = {
|
|
112
|
-
currentText: "",
|
|
113
|
-
currentToolName: null,
|
|
114
|
-
currentToolInput: "",
|
|
115
|
-
lastToolName: null,
|
|
116
|
-
};
|
|
117
|
-
claudeProcess.stdout?.on("data", (data) => {
|
|
118
|
-
const chunk = data.toString();
|
|
119
|
-
lineBuffer += chunk;
|
|
120
|
-
// Process complete lines (each JSON object is on its own line)
|
|
121
|
-
const lines = lineBuffer.split("\n");
|
|
122
|
-
lineBuffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
123
|
-
for (const line of lines) {
|
|
124
|
-
if (line.trim()) {
|
|
125
|
-
const result = parseClaudeJsonLine(line, requestId, sendResponse, streamState);
|
|
126
|
-
if (result.finalText) {
|
|
127
|
-
lastFinalText = result.finalText;
|
|
128
|
-
}
|
|
129
|
-
if (result.sessionId) {
|
|
130
|
-
capturedSessionId = result.sessionId;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
claudeProcess.stderr?.on("data", (data) => {
|
|
136
|
-
const text = data.toString();
|
|
137
|
-
// Log all stderr for debugging
|
|
138
|
-
console.log(chalk.gray(` 📋 stderr: ${text.substring(0, 100)}`));
|
|
139
|
-
if (text.toLowerCase().includes("error:") && !text.includes("is_error")) {
|
|
140
|
-
sendResponse({
|
|
141
|
-
type: "error",
|
|
142
|
-
id: requestId,
|
|
143
|
-
error: text,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
claudeProcess.on("close", (code) => {
|
|
148
|
-
// Process any remaining buffer
|
|
149
|
-
if (lineBuffer.trim()) {
|
|
150
|
-
const result = parseClaudeJsonLine(lineBuffer, requestId, sendResponse, streamState);
|
|
151
|
-
if (result.finalText) {
|
|
152
|
-
lastFinalText = result.finalText;
|
|
153
|
-
}
|
|
154
|
-
if (result.sessionId) {
|
|
155
|
-
capturedSessionId = result.sessionId;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
console.log(chalk.gray(` 🏁 Claude terminó con código: ${code}`));
|
|
159
|
-
if (capturedSessionId) {
|
|
160
|
-
console.log(chalk.gray(` 📝 Session: ${capturedSessionId.substring(0, 8)}...`));
|
|
161
|
-
}
|
|
162
|
-
if (code !== 0) {
|
|
163
|
-
console.log(chalk.red(` ❌ Error: código ${code}`));
|
|
164
|
-
sendResponse({
|
|
165
|
-
type: "error",
|
|
166
|
-
id: requestId,
|
|
167
|
-
error: `Claude process exited with code ${code}`,
|
|
168
|
-
});
|
|
169
|
-
reject(new Error(`Claude exited with code ${code}`));
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
console.log(chalk.green(` ✓ Completado`));
|
|
173
|
-
sendResponse({
|
|
174
|
-
type: "response",
|
|
175
|
-
id: requestId,
|
|
176
|
-
content: lastFinalText,
|
|
177
|
-
done: true,
|
|
178
|
-
});
|
|
179
|
-
resolve(capturedSessionId);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
claudeProcess.on("error", (err) => {
|
|
183
|
-
console.log(chalk.red(` ❌ Error spawn: ${err.message}`));
|
|
184
|
-
sendResponse({
|
|
185
|
-
type: "error",
|
|
186
|
-
id: requestId,
|
|
187
|
-
error: `Failed to start Claude: ${err.message}`,
|
|
188
|
-
});
|
|
189
|
-
reject(err);
|
|
190
|
-
});
|
|
191
|
-
// Store the process for potential cancellation
|
|
192
|
-
const clientState = [...clients.values()].find((c) => c.currentRequestId === requestId);
|
|
193
|
-
if (clientState) {
|
|
194
|
-
clientState.claudeProcess = claudeProcess;
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Handle incoming WebSocket messages
|
|
200
|
-
*/
|
|
201
|
-
async function handleMessage(ws, rawMessage) {
|
|
202
|
-
const state = clients.get(ws);
|
|
203
|
-
if (!state)
|
|
204
|
-
return;
|
|
205
|
-
const sendResponse = (response) => {
|
|
206
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
207
|
-
ws.send(JSON.stringify(response));
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
try {
|
|
211
|
-
const message = JSON.parse(rawMessage);
|
|
212
|
-
switch (message.type) {
|
|
213
|
-
case "ping":
|
|
214
|
-
sendResponse({ type: "pong", id: message.id });
|
|
215
|
-
break;
|
|
216
|
-
case "cancel":
|
|
217
|
-
if (state.claudeProcess) {
|
|
218
|
-
state.claudeProcess.kill("SIGTERM");
|
|
219
|
-
state.claudeProcess = null;
|
|
220
|
-
state.currentRequestId = null;
|
|
221
|
-
sendResponse({
|
|
222
|
-
type: "response",
|
|
223
|
-
id: message.id,
|
|
224
|
-
content: "Cancelado",
|
|
225
|
-
done: true,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
break;
|
|
229
|
-
case "clear":
|
|
230
|
-
// Reset session for new conversation
|
|
231
|
-
state.sessionId = null;
|
|
232
|
-
console.log(chalk.yellow(" 🗑️ Sesión limpiada"));
|
|
233
|
-
sendResponse({
|
|
234
|
-
type: "status",
|
|
235
|
-
id: message.id,
|
|
236
|
-
content: "cleared",
|
|
237
|
-
});
|
|
238
|
-
break;
|
|
239
|
-
case "chat":
|
|
240
|
-
if (!message.viewId || !message.message) {
|
|
241
|
-
sendResponse({
|
|
242
|
-
type: "error",
|
|
243
|
-
id: message.id,
|
|
244
|
-
error: "viewId y message son requeridos",
|
|
245
|
-
});
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
state.currentRequestId = message.id;
|
|
249
|
-
try {
|
|
250
|
-
// Pass existing sessionId for memory, get back new/updated sessionId
|
|
251
|
-
const newSessionId = await executeClaudeCode(message.message, message.viewId, message.viewName || "Unknown", message.id, sendResponse, state.sessionId);
|
|
252
|
-
// Save session for next message
|
|
253
|
-
if (newSessionId) {
|
|
254
|
-
state.sessionId = newSessionId;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
catch (error) {
|
|
258
|
-
sendResponse({
|
|
259
|
-
type: "error",
|
|
260
|
-
id: message.id,
|
|
261
|
-
error: error.message || "Error desconocido",
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
finally {
|
|
265
|
-
state.claudeProcess = null;
|
|
266
|
-
state.currentRequestId = null;
|
|
267
|
-
}
|
|
268
|
-
break;
|
|
269
|
-
default:
|
|
270
|
-
sendResponse({
|
|
271
|
-
type: "error",
|
|
272
|
-
id: message.id || "unknown",
|
|
273
|
-
error: `Tipo de mensaje desconocido: ${message.type}`,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
catch (error) {
|
|
278
|
-
sendResponse({
|
|
279
|
-
type: "error",
|
|
280
|
-
id: "parse-error",
|
|
281
|
-
error: `Error parseando mensaje: ${error.message}`,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
export async function assistantCommand(flags = {}) {
|
|
286
|
-
// Check login
|
|
287
|
-
if (!isLoggedIn()) {
|
|
288
|
-
console.log(chalk.red("\n ✗ No estás logueado. Usa: gufi login\n"));
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
// Check Claude Code is installed
|
|
292
|
-
try {
|
|
293
|
-
const { execSync } = await import("child_process");
|
|
294
|
-
execSync("claude --version", { stdio: "ignore" });
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
console.log(chalk.red("\n ✗ Claude Code no encontrado."));
|
|
298
|
-
console.log(chalk.gray(" Instala con: npm install -g @anthropic-ai/claude-code\n"));
|
|
299
|
-
process.exit(1);
|
|
300
|
-
}
|
|
301
|
-
const port = typeof flags.port === "string" ? parseInt(flags.port, 10) : flags.port || DEFAULT_PORT;
|
|
302
|
-
const env = getCurrentEnv();
|
|
303
|
-
console.log(chalk.magenta("\n 🟣 Gufi Assistant\n"));
|
|
304
|
-
console.log(chalk.gray(` Entorno: ${env === "prod" ? "Producción" : "Local"}`));
|
|
305
|
-
console.log(chalk.gray(` Puerto WebSocket: ${port}`));
|
|
306
|
-
console.log();
|
|
307
|
-
// Create WebSocket server
|
|
308
|
-
const wss = new WebSocketServer({ port });
|
|
309
|
-
wss.on("listening", () => {
|
|
310
|
-
console.log(chalk.green(` ✓ WebSocket server escuchando en ws://localhost:${port}`));
|
|
311
|
-
console.log();
|
|
312
|
-
console.log(chalk.cyan(" Conecta desde el frontend:"));
|
|
313
|
-
console.log(chalk.gray(` 1. Abre /developer/views/<id> en Gufi`));
|
|
314
|
-
console.log(chalk.gray(` 2. Click en "Edit with AI"`));
|
|
315
|
-
console.log(chalk.gray(` 3. El chat se conecta automáticamente aquí`));
|
|
316
|
-
console.log();
|
|
317
|
-
console.log(chalk.yellow(" Ctrl+C para detener\n"));
|
|
318
|
-
});
|
|
319
|
-
wss.on("connection", (ws) => {
|
|
320
|
-
console.log(chalk.green(" → Cliente conectado"));
|
|
321
|
-
// Initialize client state
|
|
322
|
-
clients.set(ws, {
|
|
323
|
-
ws,
|
|
324
|
-
claudeProcess: null,
|
|
325
|
-
currentRequestId: null,
|
|
326
|
-
sessionId: null,
|
|
327
|
-
});
|
|
328
|
-
// Send status message
|
|
329
|
-
ws.send(JSON.stringify({
|
|
330
|
-
type: "status",
|
|
331
|
-
id: "init",
|
|
332
|
-
content: "connected",
|
|
333
|
-
}));
|
|
334
|
-
ws.on("message", (data) => {
|
|
335
|
-
handleMessage(ws, data.toString());
|
|
336
|
-
});
|
|
337
|
-
ws.on("close", () => {
|
|
338
|
-
console.log(chalk.yellow(" ← Cliente desconectado"));
|
|
339
|
-
// Don't kill Claude process on disconnect - let it finish
|
|
340
|
-
// The client might reconnect (hot reload, etc.)
|
|
341
|
-
clients.delete(ws);
|
|
342
|
-
});
|
|
343
|
-
ws.on("error", (error) => {
|
|
344
|
-
console.log(chalk.red(` ✗ Error WebSocket: ${error.message}`));
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
wss.on("error", (error) => {
|
|
348
|
-
if (error.code === "EADDRINUSE") {
|
|
349
|
-
console.log(chalk.red(`\n ✗ Puerto ${port} ya está en uso.`));
|
|
350
|
-
console.log(chalk.gray(` Usa: gufi assistant --port <otro-puerto>\n`));
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
console.log(chalk.red(`\n ✗ Error: ${error.message}\n`));
|
|
354
|
-
}
|
|
355
|
-
process.exit(1);
|
|
356
|
-
});
|
|
357
|
-
// Handle graceful shutdown
|
|
358
|
-
process.on("SIGINT", () => {
|
|
359
|
-
console.log(chalk.yellow("\n Cerrando servidor..."));
|
|
360
|
-
// Kill all Claude processes with SIGKILL (force)
|
|
361
|
-
for (const state of clients.values()) {
|
|
362
|
-
if (state.claudeProcess) {
|
|
363
|
-
state.claudeProcess.kill("SIGKILL");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
// Close all WebSocket connections
|
|
367
|
-
for (const client of wss.clients) {
|
|
368
|
-
client.terminate();
|
|
369
|
-
}
|
|
370
|
-
// Force exit after 1 second max
|
|
371
|
-
setTimeout(() => {
|
|
372
|
-
console.log(chalk.green(" ✓ Servidor cerrado\n"));
|
|
373
|
-
process.exit(0);
|
|
374
|
-
}, 500);
|
|
375
|
-
wss.close();
|
|
376
|
-
});
|
|
377
|
-
}
|