gufi-cli 0.1.37 โ†’ 0.1.39

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.
@@ -0,0 +1,22 @@
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 {};
@@ -0,0 +1,377 @@
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
+ }
package/dist/index.js CHANGED
@@ -82,11 +82,14 @@ import { doctorCommand } from "./commands/doctor.js";
82
82
  import { docsCommand } from "./commands/docs.js";
83
83
  import { startMcpServer } from "./mcp.js";
84
84
  import { claudeCommand } from "./commands/claude.js";
85
+ import { createRequire } from "module";
86
+ const require = createRequire(import.meta.url);
87
+ const pkg = require("../package.json");
85
88
  const program = new Command();
86
89
  program
87
90
  .name("gufi")
88
91
  .description("๐ŸŸฃ Gufi CLI - Desarrolla mรณdulos, vistas y automations")
89
- .version("0.1.8");
92
+ .version(pkg.version);
90
93
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
91
94
  // ๐Ÿ” Auth
92
95
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
package/dist/mcp.js CHANGED
@@ -37,19 +37,36 @@ const DEFAULT_ENV = "prod";
37
37
  /**
38
38
  * Get API URL for the specified environment.
39
39
  * @param env - 'prod' (default) or 'dev' (localhost:3000 โ†’ Cloud SQL Dev)
40
+ *
41
+ * Uses resolveEnv() which is STRICT - invalid values throw errors.
40
42
  */
41
43
  function getApiUrl(env) {
42
- const resolvedEnv = (env === "dev" || env === "local" || env === "prod") ? env : DEFAULT_ENV;
44
+ const resolvedEnv = resolveEnv(env);
43
45
  return ENV_URLS[resolvedEnv];
44
46
  }
45
47
  /**
46
48
  * Resolve env parameter to canonical form ('prod' or 'dev')
47
49
  * 'local' is treated as alias for 'dev'
50
+ *
51
+ * IMPORTANT: This function is STRICT to prevent accidental prod modifications.
52
+ * - undefined/null โ†’ defaults to 'prod' (backwards compatible)
53
+ * - 'prod', 'dev', 'local' โ†’ valid values
54
+ * - Any other value โ†’ THROWS ERROR (prevents typos like 'dve' from hitting prod)
48
55
  */
49
56
  function resolveEnv(env) {
57
+ // Undefined/null โ†’ default to prod (backwards compatible)
58
+ if (env === undefined || env === null || env === "") {
59
+ return "prod";
60
+ }
61
+ // Valid values
50
62
  if (env === "dev" || env === "local")
51
63
  return "dev";
52
- return "prod";
64
+ if (env === "prod")
65
+ return "prod";
66
+ // STRICT: Unknown value = ERROR, not silent fallback to prod
67
+ // This prevents typos like 'dve' or 'Dev' from accidentally modifying prod
68
+ throw new Error(`Invalid environment: '${env}'. Valid values are 'prod', 'dev', or 'local'. ` +
69
+ `This error prevents accidental production modifications.`);
53
70
  }
54
71
  // Keep for backwards compatibility (some internal functions may use this)
55
72
  function getSessionApiUrl() {
@@ -871,23 +888,24 @@ const toolHandlers = {
871
888
  async gufi_context(params) {
872
889
  // Generate intelligent context based on what's requested
873
890
  // Always returns FULL context (no more "detail" parameter)
891
+ const env = params.env;
874
892
  // Module-specific context
875
893
  if (params.module_id && params.company_id) {
876
- return getClaudeOptimizedContext(params.company_id, params.module_id, undefined, true);
894
+ return getClaudeOptimizedContext(params.company_id, params.module_id, undefined, true, env);
877
895
  }
878
896
  // Entity-specific context
879
897
  if (params.entity_id && params.company_id) {
880
- return getClaudeOptimizedContext(params.company_id, undefined, params.entity_id, true);
898
+ return getClaudeOptimizedContext(params.company_id, undefined, params.entity_id, true, env);
881
899
  }
882
900
  if (params.view_id) {
883
- return generateViewContextMcp(parseInt(params.view_id), true);
901
+ return generateViewContextMcp(parseInt(params.view_id), true, env);
884
902
  }
885
903
  else if (params.package_id) {
886
- return generatePackageContextMcp(parseInt(params.package_id), true);
904
+ return generatePackageContextMcp(parseInt(params.package_id), true, env);
887
905
  }
888
906
  else if (params.company_id) {
889
907
  // Company context - always full
890
- return getClaudeOptimizedContext(params.company_id, undefined, undefined, true);
908
+ return getClaudeOptimizedContext(params.company_id, undefined, undefined, true, env);
891
909
  }
892
910
  else {
893
911
  // Auto-detect context from current directory
@@ -922,10 +940,10 @@ const toolHandlers = {
922
940
  // 2. Check if we're in a view directory (.gufi-view.json)
923
941
  const viewMeta = loadViewMetaFromCwd();
924
942
  if (viewMeta) {
925
- return generateViewContextMcp(viewMeta.viewId, true);
943
+ return generateViewContextMcp(viewMeta.viewId, true, env);
926
944
  }
927
945
  // 3. Fallback: Return packages overview
928
- const response = await developerRequest("/my-packages");
946
+ const response = await developerRequest("/my-packages", {}, true, env);
929
947
  const packages = response.data || [];
930
948
  return {
931
949
  type: "packages_overview",
@@ -983,7 +1001,7 @@ const toolHandlers = {
983
1001
  preview: params.preview || false,
984
1002
  skip_existing: params.skip_existing || false,
985
1003
  }),
986
- }, params.company_id);
1004
+ }, params.company_id, true, params.env);
987
1005
  if (params.preview) {
988
1006
  return {
989
1007
  preview: true,
@@ -1261,7 +1279,7 @@ const toolHandlers = {
1261
1279
  }
1262
1280
  const data = await apiRequest(endpoint, {
1263
1281
  headers: { "X-Company-ID": params.company_id },
1264
- });
1282
+ }, params.company_id, true, params.env);
1265
1283
  return { executions: data.data || data || [] };
1266
1284
  },
1267
1285
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -1387,10 +1405,10 @@ const toolHandlers = {
1387
1405
  // Environment Variables
1388
1406
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1389
1407
  async gufi_env(params) {
1390
- const { action, company_id, key, value } = params;
1408
+ const { action, company_id, key, value, env } = params;
1391
1409
  switch (action) {
1392
1410
  case "list": {
1393
- const data = await apiRequest("/api/cli/env", {}, company_id);
1411
+ const data = await apiRequest("/api/cli/env", {}, company_id, true, env);
1394
1412
  return { variables: data || [] };
1395
1413
  }
1396
1414
  case "set": {
@@ -1399,7 +1417,7 @@ const toolHandlers = {
1399
1417
  await apiRequest("/api/cli/env", {
1400
1418
  method: "POST",
1401
1419
  body: JSON.stringify({ key, value }),
1402
- }, company_id);
1420
+ }, company_id, true, env);
1403
1421
  return { success: true, key };
1404
1422
  }
1405
1423
  case "delete": {
@@ -1407,7 +1425,7 @@ const toolHandlers = {
1407
1425
  throw new Error("key required for action 'delete'");
1408
1426
  await apiRequest(`/api/cli/env/${encodeURIComponent(key)}`, {
1409
1427
  method: "DELETE",
1410
- }, company_id);
1428
+ }, company_id, true, env);
1411
1429
  return { success: true, key };
1412
1430
  }
1413
1431
  default:
@@ -1420,8 +1438,9 @@ const toolHandlers = {
1420
1438
  async gufi_view_pull(params) {
1421
1439
  const viewId = params.view_id;
1422
1440
  const companyId = params.company_id;
1441
+ const env = params.env;
1423
1442
  // Get view info for package_id (pass company_id for access check)
1424
- const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, companyId);
1443
+ const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, companyId, true, env);
1425
1444
  const view = viewResponse.data || viewResponse;
1426
1445
  // ๐Ÿ’œ Backend returns pk_id, not id
1427
1446
  if (!view || !view.pk_id) {
@@ -1444,6 +1463,7 @@ const toolHandlers = {
1444
1463
  async gufi_view_push(params) {
1445
1464
  let viewDir;
1446
1465
  let viewId = params.view_id;
1466
+ const env = params.env;
1447
1467
  // If view_id provided, get dir from it
1448
1468
  if (viewId) {
1449
1469
  viewDir = getViewDir(`view_${viewId}`);
@@ -1459,7 +1479,7 @@ const toolHandlers = {
1459
1479
  // Get view info for package
1460
1480
  let packageInfo = null;
1461
1481
  if (viewId) {
1462
- const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`);
1482
+ const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, undefined, true, env);
1463
1483
  const view = viewResponse.data || viewResponse;
1464
1484
  if (view.package_id) {
1465
1485
  packageInfo = { id: view.package_id, publish_cmd: `gufi package:publish ${view.package_id}` };
@@ -1483,10 +1503,10 @@ const toolHandlers = {
1483
1503
  // Packages
1484
1504
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1485
1505
  async gufi_package(params) {
1486
- const { action, id, name, description, module_id, company_id } = params;
1506
+ const { action, id, name, description, module_id, company_id, env } = params;
1487
1507
  switch (action) {
1488
1508
  case "list": {
1489
- const response = await developerRequest("/my-packages");
1509
+ const response = await developerRequest("/my-packages", {}, true, env);
1490
1510
  const packages = response.data || [];
1491
1511
  return {
1492
1512
  packages: packages.map((p) => ({
@@ -1502,11 +1522,11 @@ const toolHandlers = {
1502
1522
  case "get": {
1503
1523
  if (!id)
1504
1524
  throw new Error("id required for action 'get'");
1505
- const response = await developerRequest(`/packages/${id}`);
1525
+ const response = await developerRequest(`/packages/${id}`, {}, true, env);
1506
1526
  const pkg = response.data;
1507
1527
  let views = [];
1508
1528
  try {
1509
- const viewsResponse = await developerRequest(`/packages/${id}/views`);
1529
+ const viewsResponse = await developerRequest(`/packages/${id}/views`, {}, true, env);
1510
1530
  views = viewsResponse.data || [];
1511
1531
  }
1512
1532
  catch { }
@@ -1543,13 +1563,13 @@ const toolHandlers = {
1543
1563
  version: "1.0.0",
1544
1564
  status: "draft",
1545
1565
  }),
1546
- });
1566
+ }, true, env);
1547
1567
  return { success: true, package: response.data };
1548
1568
  }
1549
1569
  case "delete": {
1550
1570
  if (!id)
1551
1571
  throw new Error("id required for action 'delete'");
1552
- await developerRequest(`/packages/${id}`, { method: "DELETE" });
1572
+ await developerRequest(`/packages/${id}`, { method: "DELETE" }, true, env);
1553
1573
  return { success: true };
1554
1574
  }
1555
1575
  case "add_module": {
@@ -1561,7 +1581,7 @@ const toolHandlers = {
1561
1581
  companyId: parseInt(company_id),
1562
1582
  moduleId: parseInt(module_id),
1563
1583
  }),
1564
- });
1584
+ }, true, env);
1565
1585
  return { success: true, module: response.data };
1566
1586
  }
1567
1587
  case "remove_module": {
@@ -1569,7 +1589,7 @@ const toolHandlers = {
1569
1589
  throw new Error("id and module_id required for action 'remove_module'");
1570
1590
  await developerRequest(`/packages/${id}/modules/${module_id}`, {
1571
1591
  method: "DELETE",
1572
- });
1592
+ }, true, env);
1573
1593
  return { success: true };
1574
1594
  }
1575
1595
  case "publish": {
@@ -1577,7 +1597,7 @@ const toolHandlers = {
1577
1597
  throw new Error("id required for action 'publish'");
1578
1598
  const response = await developerRequest(`/packages/${id}/publish`, {
1579
1599
  method: "POST",
1580
- });
1600
+ }, true, env);
1581
1601
  return { success: true, status: response.data?.status, version: response.data?.version };
1582
1602
  }
1583
1603
  default:
@@ -1596,8 +1616,9 @@ const toolHandlers = {
1596
1616
  * @param moduleId - Optional module ID to filter by
1597
1617
  * @param entityId - Optional entity ID to filter by
1598
1618
  * @param fullText - If true, return as plain text. Otherwise return structured.
1619
+ * @param env - Environment: 'prod' (default) or 'dev'
1599
1620
  */
1600
- async function getClaudeOptimizedContext(companyId, moduleId, entityId, fullText = true) {
1621
+ async function getClaudeOptimizedContext(companyId, moduleId, entityId, fullText = true, env) {
1601
1622
  // Build query params
1602
1623
  const params = new URLSearchParams();
1603
1624
  if (moduleId)
@@ -1605,12 +1626,14 @@ async function getClaudeOptimizedContext(companyId, moduleId, entityId, fullText
1605
1626
  if (entityId)
1606
1627
  params.set("entity_id", entityId);
1607
1628
  const queryString = params.toString();
1608
- const url = `${getSessionApiUrl()}/api/schema/export-claude${queryString ? "?" + queryString : ""}`;
1609
- let token = getToken();
1629
+ const apiUrl = getApiUrl(env);
1630
+ const url = `${apiUrl}/api/schema/export-claude${queryString ? "?" + queryString : ""}`;
1631
+ const resolvedEnv = resolveEnv(env);
1632
+ let token = getTokenForEnv(resolvedEnv);
1610
1633
  if (!token) {
1611
- token = await autoLogin();
1634
+ token = await autoLoginWithEnv(env);
1612
1635
  if (!token)
1613
- throw new Error("Not logged in. Run: gufi login");
1636
+ throw new Error(`Not logged in for env '${resolvedEnv}'. Run: gufi login`);
1614
1637
  }
1615
1638
  const headers = {
1616
1639
  Authorization: `Bearer ${token}`,
@@ -1645,15 +1668,15 @@ async function getClaudeOptimizedContext(companyId, moduleId, entityId, fullText
1645
1668
  schema,
1646
1669
  };
1647
1670
  }
1648
- async function generateViewContextMcp(viewId, includeConcepts) {
1671
+ async function generateViewContextMcp(viewId, includeConcepts, env) {
1649
1672
  // Use apiRequest for view details
1650
- const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`);
1673
+ const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, undefined, true, env);
1651
1674
  const view = viewResponse.data || viewResponse;
1652
1675
  // Get package info if exists
1653
1676
  let pkg = null;
1654
1677
  if (view.package_id) {
1655
1678
  try {
1656
- const pkgResponse = await developerRequest(`/packages/${view.package_id}`);
1679
+ const pkgResponse = await developerRequest(`/packages/${view.package_id}`, {}, true, env);
1657
1680
  pkg = pkgResponse.data || pkgResponse;
1658
1681
  }
1659
1682
  catch { }
@@ -1681,17 +1704,17 @@ async function generateViewContextMcp(viewId, includeConcepts) {
1681
1704
  try {
1682
1705
  const modulesResponse = await apiRequest(`/api/company/schema`, {
1683
1706
  headers: { "X-Company-ID": String(viewCompanyId) },
1684
- }, String(viewCompanyId));
1707
+ }, String(viewCompanyId), true, env);
1685
1708
  modules = modulesResponse.modules || modulesResponse.data?.modules || [];
1686
1709
  }
1687
1710
  catch { }
1688
1711
  try {
1689
- const automationsResponse = await apiRequest(`/api/automation-scripts`, {}, String(viewCompanyId));
1712
+ const automationsResponse = await apiRequest(`/api/automation-scripts`, {}, String(viewCompanyId), true, env);
1690
1713
  automations = Array.isArray(automationsResponse) ? automationsResponse : automationsResponse.data || [];
1691
1714
  }
1692
1715
  catch { }
1693
1716
  try {
1694
- const envResponse = await apiRequest(`/api/cli/env`, {}, String(viewCompanyId));
1717
+ const envResponse = await apiRequest(`/api/cli/env`, {}, String(viewCompanyId), true, env);
1695
1718
  envVars = Array.isArray(envResponse) ? envResponse : envResponse.data || [];
1696
1719
  }
1697
1720
  catch { }
@@ -1756,14 +1779,14 @@ async function generateViewContextMcp(viewId, includeConcepts) {
1756
1779
  }
1757
1780
  return result;
1758
1781
  }
1759
- async function generatePackageContextMcp(packageId, includeConcepts) {
1782
+ async function generatePackageContextMcp(packageId, includeConcepts, env) {
1760
1783
  // Use developerRequest which has correct path /api/developer/...
1761
- const response = await developerRequest(`/packages/${packageId}`);
1784
+ const response = await developerRequest(`/packages/${packageId}`, {}, true, env);
1762
1785
  const pkg = response.data || response;
1763
1786
  // Get views
1764
1787
  let views = [];
1765
1788
  try {
1766
- const viewsResponse = await developerRequest(`/packages/${packageId}/views`);
1789
+ const viewsResponse = await developerRequest(`/packages/${packageId}/views`, {}, true, env);
1767
1790
  views = viewsResponse.data || [];
1768
1791
  }
1769
1792
  catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gufi-cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "CLI for developing Gufi Marketplace views locally with Claude Code",
5
5
  "bin": {
6
6
  "gufi": "./bin/gufi.js"
@@ -1,21 +0,0 @@
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>;
@@ -1,6 +0,0 @@
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>;
@@ -1,137 +0,0 @@
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
- }