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 CHANGED
@@ -55,9 +55,9 @@ gufi config:prod
55
55
  }
56
56
  ```
57
57
 
58
- ## MCP Server - 12 Tools
58
+ ## MCP Server - 13 Tools
59
59
 
60
- El servidor MCP está en `src/mcp.ts` y expone **12 tools** para que Claude interactúe con Gufi.
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 atomicas (add/update/remove) |
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
- await dataProvider.runClickAutomation?.({
177
- company_id, module_id, table_id: entityId,
178
- function_name: "my_automation",
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,6 @@
1
+ /**
2
+ * gufi setup-claude - Configure Claude Code with Gufi MCP
3
+ *
4
+ * Creates workspace and configures MCP server
5
+ */
6
+ export declare function setupClaudeCommand(): 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,6 +1,6 @@
1
1
  {
2
2
  "name": "gufi-cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "CLI for developing Gufi Marketplace views locally with Claude Code",
5
5
  "bin": {
6
6
  "gufi": "./bin/gufi.js"
@@ -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
- }