gufi-cli 0.1.35 → 0.1.36
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/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +37 -0
- package/dist/mcp.js +394 -70
- package/package.json +1 -1
package/dist/lib/config.d.ts
CHANGED
|
@@ -42,6 +42,13 @@ export declare function setCurrentEnv(env: "prod" | "local" | "custom"): void;
|
|
|
42
42
|
export declare function loadConfig(): GufiConfig;
|
|
43
43
|
export declare function saveConfig(config: GufiConfig): void;
|
|
44
44
|
export declare function getToken(): string | undefined;
|
|
45
|
+
export declare function getTokenForEnv(env: "prod" | "dev" | "local"): string | undefined;
|
|
46
|
+
export declare function getRefreshTokenForEnv(env: "prod" | "dev" | "local"): string | undefined;
|
|
47
|
+
export declare function setTokenForEnv(env: "prod" | "dev" | "local", token: string, email: string, refreshToken?: string): void;
|
|
48
|
+
export declare function getCredentialsForEnv(env: "prod" | "dev" | "local"): {
|
|
49
|
+
email?: string;
|
|
50
|
+
password?: string;
|
|
51
|
+
} | undefined;
|
|
45
52
|
export declare function setToken(token: string, email: string, refreshToken?: string): void;
|
|
46
53
|
export declare function getRefreshToken(): string | undefined;
|
|
47
54
|
export declare function setRefreshToken(refreshToken: string): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -117,6 +117,43 @@ export function saveConfig(config) {
|
|
|
117
117
|
export function getToken() {
|
|
118
118
|
return loadConfig().token;
|
|
119
119
|
}
|
|
120
|
+
// Get token for a specific environment
|
|
121
|
+
// Note: "dev" maps to "local" internally for backwards compatibility
|
|
122
|
+
export function getTokenForEnv(env) {
|
|
123
|
+
const file = loadConfigFile();
|
|
124
|
+
const key = env === "dev" ? "local" : env;
|
|
125
|
+
return file.environments[key]?.token;
|
|
126
|
+
}
|
|
127
|
+
// Get refresh token for a specific environment
|
|
128
|
+
export function getRefreshTokenForEnv(env) {
|
|
129
|
+
const file = loadConfigFile();
|
|
130
|
+
const key = env === "dev" ? "local" : env;
|
|
131
|
+
return file.environments[key]?.refreshToken;
|
|
132
|
+
}
|
|
133
|
+
// Set token for a specific environment
|
|
134
|
+
export function setTokenForEnv(env, token, email, refreshToken) {
|
|
135
|
+
const file = loadConfigFile();
|
|
136
|
+
const key = env === "dev" ? "local" : env;
|
|
137
|
+
if (!file.environments[key]) {
|
|
138
|
+
file.environments[key] = { apiUrl: key === "prod" ? "https://gogufi.com" : "http://localhost:3000" };
|
|
139
|
+
}
|
|
140
|
+
file.environments[key].token = token;
|
|
141
|
+
file.environments[key].email = email;
|
|
142
|
+
if (refreshToken) {
|
|
143
|
+
file.environments[key].refreshToken = refreshToken;
|
|
144
|
+
}
|
|
145
|
+
saveConfigFile(file);
|
|
146
|
+
}
|
|
147
|
+
// Get credentials for a specific environment (for auto-login)
|
|
148
|
+
export function getCredentialsForEnv(env) {
|
|
149
|
+
const file = loadConfigFile();
|
|
150
|
+
const key = env === "dev" ? "local" : env;
|
|
151
|
+
const envConfig = file.environments[key];
|
|
152
|
+
if (envConfig?.email && envConfig?.password) {
|
|
153
|
+
return { email: envConfig.email, password: envConfig.password };
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
120
157
|
export function setToken(token, email, refreshToken) {
|
|
121
158
|
const config = loadConfig();
|
|
122
159
|
config.token = token;
|
package/dist/mcp.js
CHANGED
|
@@ -14,13 +14,47 @@ import * as readline from "readline";
|
|
|
14
14
|
import * as fs from "fs";
|
|
15
15
|
import * as path from "path";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
|
-
import { getToken,
|
|
17
|
+
import { getToken, getRefreshToken, loadConfig, isLoggedIn, getTokenForEnv, getRefreshTokenForEnv, setTokenForEnv, getCredentialsForEnv } from "./lib/config.js";
|
|
18
18
|
import { pullView, pushView, getViewDir, loadViewMeta } from "./lib/sync.js";
|
|
19
19
|
// For ES modules __dirname equivalent
|
|
20
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
21
|
const __dirname = path.dirname(__filename);
|
|
22
22
|
// docs/mcp path relative to CLI
|
|
23
23
|
const DOCS_MCP_PATH = path.resolve(__dirname, "../../../docs/mcp");
|
|
24
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
// Environment Configuration (stateless - passed per request)
|
|
26
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
// IMPORTANT: No more global sessionEnv variable!
|
|
28
|
+
// Each tool call can specify env='local' or env='prod' (default: prod)
|
|
29
|
+
// This prevents cross-session conflicts when multiple Claude instances share the MCP process.
|
|
30
|
+
const ENV_URLS = {
|
|
31
|
+
prod: "https://gogufi.com",
|
|
32
|
+
dev: "http://localhost:3000",
|
|
33
|
+
local: "http://localhost:3000", // Alias for backwards compatibility
|
|
34
|
+
};
|
|
35
|
+
// Default environment - always prod for safety
|
|
36
|
+
const DEFAULT_ENV = "prod";
|
|
37
|
+
/**
|
|
38
|
+
* Get API URL for the specified environment.
|
|
39
|
+
* @param env - 'prod' (default) or 'dev' (localhost:3000 → Cloud SQL Dev)
|
|
40
|
+
*/
|
|
41
|
+
function getApiUrl(env) {
|
|
42
|
+
const resolvedEnv = (env === "dev" || env === "local" || env === "prod") ? env : DEFAULT_ENV;
|
|
43
|
+
return ENV_URLS[resolvedEnv];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve env parameter to canonical form ('prod' or 'dev')
|
|
47
|
+
* 'local' is treated as alias for 'dev'
|
|
48
|
+
*/
|
|
49
|
+
function resolveEnv(env) {
|
|
50
|
+
if (env === "dev" || env === "local")
|
|
51
|
+
return "dev";
|
|
52
|
+
return "prod";
|
|
53
|
+
}
|
|
54
|
+
// Keep for backwards compatibility (some internal functions may use this)
|
|
55
|
+
function getSessionApiUrl() {
|
|
56
|
+
return ENV_URLS[DEFAULT_ENV];
|
|
57
|
+
}
|
|
24
58
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
25
59
|
const viewFilesCache = new Map();
|
|
26
60
|
const viewContextCache = new Map();
|
|
@@ -49,6 +83,56 @@ function setCachedViewContext(viewId, context) {
|
|
|
49
83
|
viewContextCache.set(viewId, { data: context, timestamp: Date.now() });
|
|
50
84
|
}
|
|
51
85
|
// ════════════════════════════════════════════════════════════════════════════
|
|
86
|
+
// Automation Code Validation
|
|
87
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
88
|
+
/**
|
|
89
|
+
* 💜 Detecta patrones de IDs hardcodeados que rompen entre entornos
|
|
90
|
+
*
|
|
91
|
+
* Busca:
|
|
92
|
+
* - Tablas físicas: m310_t4146 (deben usar nombres lógicos: stock.productos)
|
|
93
|
+
* - entity_id/tableId hardcodeados: entity_id === 1234
|
|
94
|
+
*
|
|
95
|
+
* @param code - Código a validar
|
|
96
|
+
* @returns Array de warnings (vacío si no hay problemas)
|
|
97
|
+
*/
|
|
98
|
+
function checkForHardcodedIds(code) {
|
|
99
|
+
const warnings = [];
|
|
100
|
+
// Pattern 1: Physical table names (m123_t456)
|
|
101
|
+
const physicalTablePattern = /m\d+_t\d+/g;
|
|
102
|
+
const physicalMatches = code.match(physicalTablePattern);
|
|
103
|
+
if (physicalMatches) {
|
|
104
|
+
const uniqueTables = [...new Set(physicalMatches)];
|
|
105
|
+
warnings.push(`⚠️ HARDCODED TABLE IDS: Found physical table names: ${uniqueTables.join(', ')}. ` +
|
|
106
|
+
`Use logical names instead (e.g., 'stock.productos' not 'm310_t4146'). ` +
|
|
107
|
+
`Physical IDs change between DEV/PROD environments.`);
|
|
108
|
+
}
|
|
109
|
+
// Pattern 2: Hardcoded entity_id comparisons (entity_id === 1234 or entity_id == 1234)
|
|
110
|
+
const entityIdPattern = /entity_id\s*(?:===?|!==?)\s*(\d{3,})/g;
|
|
111
|
+
const entityMatches = [...code.matchAll(entityIdPattern)];
|
|
112
|
+
if (entityMatches.length > 0) {
|
|
113
|
+
const ids = entityMatches.map(m => m[1]);
|
|
114
|
+
warnings.push(`⚠️ HARDCODED ENTITY_ID: Found comparisons with literal IDs: ${ids.join(', ')}. ` +
|
|
115
|
+
`Use context.table instead (e.g., table.includes('supplier_orders') not entity_id === 7140).`);
|
|
116
|
+
}
|
|
117
|
+
// Pattern 3: Hardcoded tableId comparisons
|
|
118
|
+
const tableIdPattern = /tableId\s*(?:===?|!==?)\s*(\d{3,})/g;
|
|
119
|
+
const tableIdMatches = [...code.matchAll(tableIdPattern)];
|
|
120
|
+
if (tableIdMatches.length > 0) {
|
|
121
|
+
const ids = tableIdMatches.map(m => m[1]);
|
|
122
|
+
warnings.push(`⚠️ HARDCODED TABLE_ID: Found comparisons with literal IDs: ${ids.join(', ')}. ` +
|
|
123
|
+
`Use logical table names instead.`);
|
|
124
|
+
}
|
|
125
|
+
// Pattern 4: Hardcoded table_id comparisons (underscore version)
|
|
126
|
+
const table_idPattern = /table_id\s*(?:===?|!==?)\s*(\d{3,})/g;
|
|
127
|
+
const table_idMatches = [...code.matchAll(table_idPattern)];
|
|
128
|
+
if (table_idMatches.length > 0) {
|
|
129
|
+
const ids = table_idMatches.map(m => m[1]);
|
|
130
|
+
warnings.push(`⚠️ HARDCODED TABLE_ID: Found comparisons with literal IDs: ${ids.join(', ')}. ` +
|
|
131
|
+
`Use logical table names instead.`);
|
|
132
|
+
}
|
|
133
|
+
return warnings;
|
|
134
|
+
}
|
|
135
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
52
136
|
// Analytics - Track MCP tool calls
|
|
53
137
|
// ════════════════════════════════════════════════════════════════════════════
|
|
54
138
|
async function trackAnalytics(data) {
|
|
@@ -58,7 +142,8 @@ async function trackAnalytics(data) {
|
|
|
58
142
|
console.error("[MCP Analytics] No token, skipping");
|
|
59
143
|
return;
|
|
60
144
|
}
|
|
61
|
-
|
|
145
|
+
// Analytics always go to prod
|
|
146
|
+
const url = `${getApiUrl("prod")}/api/cli/mcp-analytics`;
|
|
62
147
|
console.error(`[MCP Analytics] Tracking ${data.tool_name} to ${url}`);
|
|
63
148
|
const res = await fetch(url, {
|
|
64
149
|
method: "POST",
|
|
@@ -81,19 +166,26 @@ async function trackAnalytics(data) {
|
|
|
81
166
|
// API Client (reused from CLI)
|
|
82
167
|
// ════════════════════════════════════════════════════════════════════════════
|
|
83
168
|
async function autoLogin() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
169
|
+
return autoLoginWithEnv(DEFAULT_ENV);
|
|
170
|
+
}
|
|
171
|
+
async function autoLoginWithEnv(env) {
|
|
172
|
+
const resolvedEnv = resolveEnv(env);
|
|
173
|
+
// ONLY use credentials for the specified env - NO fallback to other env
|
|
174
|
+
const creds = getCredentialsForEnv(resolvedEnv);
|
|
175
|
+
if (!creds?.email || !creds?.password) {
|
|
176
|
+
throw new Error(`No credentials for env '${resolvedEnv}'. Run: gufi config:${resolvedEnv === 'dev' ? 'local' : 'prod'} && gufi login`);
|
|
177
|
+
}
|
|
87
178
|
try {
|
|
88
|
-
const response = await fetch(`${getApiUrl()}/api/auth/login`, {
|
|
179
|
+
const response = await fetch(`${getApiUrl(env)}/api/auth/login`, {
|
|
89
180
|
method: "POST",
|
|
90
181
|
headers: { "Content-Type": "application/json", "X-Client": "mcp" },
|
|
91
|
-
body: JSON.stringify({ email:
|
|
182
|
+
body: JSON.stringify({ email: creds.email, password: creds.password }),
|
|
92
183
|
});
|
|
93
184
|
if (!response.ok)
|
|
94
185
|
return undefined;
|
|
95
186
|
const data = await response.json();
|
|
96
|
-
|
|
187
|
+
// Save token to the correct environment
|
|
188
|
+
setTokenForEnv(resolvedEnv, data.accessToken, creds.email, data.refreshToken);
|
|
97
189
|
return data.accessToken;
|
|
98
190
|
}
|
|
99
191
|
catch {
|
|
@@ -101,32 +193,43 @@ async function autoLogin() {
|
|
|
101
193
|
}
|
|
102
194
|
}
|
|
103
195
|
async function refreshOrLogin() {
|
|
104
|
-
|
|
196
|
+
return refreshOrLoginWithEnv(DEFAULT_ENV);
|
|
197
|
+
}
|
|
198
|
+
async function refreshOrLoginWithEnv(env) {
|
|
199
|
+
const resolvedEnv = resolveEnv(env);
|
|
200
|
+
const refreshToken = getRefreshTokenForEnv(resolvedEnv) || getRefreshToken();
|
|
105
201
|
if (refreshToken) {
|
|
106
202
|
try {
|
|
107
|
-
const response = await fetch(`${getApiUrl()}/api/auth/refresh`, {
|
|
203
|
+
const response = await fetch(`${getApiUrl(env)}/api/auth/refresh`, {
|
|
108
204
|
method: "POST",
|
|
109
205
|
headers: { "Content-Type": "application/json", "X-Client": "mcp", "X-Refresh-Token": refreshToken },
|
|
110
206
|
});
|
|
111
207
|
if (response.ok) {
|
|
112
208
|
const data = await response.json();
|
|
113
|
-
const
|
|
114
|
-
|
|
209
|
+
const creds = getCredentialsForEnv(resolvedEnv) || loadConfig();
|
|
210
|
+
setTokenForEnv(resolvedEnv, data.accessToken, creds.email || "", data.refreshToken);
|
|
115
211
|
return data.accessToken;
|
|
116
212
|
}
|
|
117
213
|
}
|
|
118
214
|
catch { }
|
|
119
215
|
}
|
|
120
|
-
return
|
|
216
|
+
return autoLoginWithEnv(env);
|
|
121
217
|
}
|
|
122
|
-
|
|
123
|
-
|
|
218
|
+
// Simple wrapper - just passes env to apiRequestWithEnv
|
|
219
|
+
async function apiRequest(endpoint, options = {}, companyId, retry = true, env) {
|
|
220
|
+
return apiRequestWithEnv(endpoint, options, companyId, env, retry);
|
|
221
|
+
}
|
|
222
|
+
async function apiRequestWithEnv(endpoint, options = {}, companyId, env, retry = true) {
|
|
223
|
+
const resolvedEnv = resolveEnv(env);
|
|
224
|
+
// ONLY use token for the specified env - NO fallback to prod token
|
|
225
|
+
let token = getTokenForEnv(resolvedEnv);
|
|
226
|
+
const apiUrl = getApiUrl(env);
|
|
124
227
|
if (!token) {
|
|
125
|
-
token = await
|
|
228
|
+
token = await autoLoginWithEnv(env);
|
|
126
229
|
if (!token)
|
|
127
230
|
throw new Error("Not logged in. Run: gufi login");
|
|
128
231
|
}
|
|
129
|
-
const url = `${
|
|
232
|
+
const url = `${apiUrl}${endpoint}`;
|
|
130
233
|
const headers = {
|
|
131
234
|
"Content-Type": "application/json",
|
|
132
235
|
Authorization: `Bearer ${token}`,
|
|
@@ -140,9 +243,9 @@ async function apiRequest(endpoint, options = {}, companyId, retry = true) {
|
|
|
140
243
|
headers: { ...headers, ...options.headers },
|
|
141
244
|
});
|
|
142
245
|
if ((response.status === 401 || response.status === 403) && retry) {
|
|
143
|
-
const newToken = await
|
|
246
|
+
const newToken = await refreshOrLoginWithEnv(env);
|
|
144
247
|
if (newToken)
|
|
145
|
-
return
|
|
248
|
+
return apiRequestWithEnv(endpoint, options, companyId, env, false);
|
|
146
249
|
}
|
|
147
250
|
if (!response.ok) {
|
|
148
251
|
const text = await response.text();
|
|
@@ -150,14 +253,17 @@ async function apiRequest(endpoint, options = {}, companyId, retry = true) {
|
|
|
150
253
|
}
|
|
151
254
|
return response.json();
|
|
152
255
|
}
|
|
153
|
-
async function developerRequest(path, options = {}, retry = true) {
|
|
154
|
-
|
|
256
|
+
async function developerRequest(path, options = {}, retry = true, env) {
|
|
257
|
+
const resolvedEnv = resolveEnv(env);
|
|
258
|
+
// ONLY use token for the specified env - NO fallback to prod token
|
|
259
|
+
let token = getTokenForEnv(resolvedEnv);
|
|
260
|
+
const apiUrl = getApiUrl(env);
|
|
155
261
|
if (!token) {
|
|
156
|
-
token = await
|
|
262
|
+
token = await autoLoginWithEnv(env);
|
|
157
263
|
if (!token)
|
|
158
264
|
throw new Error("Not logged in. Run: gufi login");
|
|
159
265
|
}
|
|
160
|
-
const res = await fetch(`${
|
|
266
|
+
const res = await fetch(`${apiUrl}/api/developer${path}`, {
|
|
161
267
|
...options,
|
|
162
268
|
headers: {
|
|
163
269
|
Authorization: `Bearer ${token}`,
|
|
@@ -167,9 +273,9 @@ async function developerRequest(path, options = {}, retry = true) {
|
|
|
167
273
|
},
|
|
168
274
|
});
|
|
169
275
|
if ((res.status === 401 || res.status === 403) && retry) {
|
|
170
|
-
const newToken = await
|
|
276
|
+
const newToken = await refreshOrLoginWithEnv(env);
|
|
171
277
|
if (newToken)
|
|
172
|
-
return developerRequest(path, options, false);
|
|
278
|
+
return developerRequest(path, options, false, env);
|
|
173
279
|
}
|
|
174
280
|
if (!res.ok) {
|
|
175
281
|
const error = await res.text();
|
|
@@ -223,12 +329,12 @@ function generateFileId() {
|
|
|
223
329
|
* Output: { id, url, name, mime, uploaded_at }
|
|
224
330
|
*/
|
|
225
331
|
function normalizeFileObject(file) {
|
|
226
|
-
// Validate required fields
|
|
227
|
-
if (!file.url || typeof file.url !== "string") {
|
|
228
|
-
throw new Error("File field error: 'url' is required and
|
|
332
|
+
// Validate required fields - url cannot be empty
|
|
333
|
+
if (!file.url || typeof file.url !== "string" || file.url.trim() === "") {
|
|
334
|
+
throw new Error("File field error: 'url' is required and cannot be empty");
|
|
229
335
|
}
|
|
230
|
-
if (!file.name || typeof file.name !== "string") {
|
|
231
|
-
throw new Error("File field error: 'name' is required and
|
|
336
|
+
if (!file.name || typeof file.name !== "string" || file.name.trim() === "") {
|
|
337
|
+
throw new Error("File field error: 'name' is required and cannot be empty");
|
|
232
338
|
}
|
|
233
339
|
// Validate url format (must be company_X/... path or full URL)
|
|
234
340
|
const isValidPath = file.url.startsWith("company_") || file.url.startsWith("http");
|
|
@@ -276,14 +382,16 @@ const PHYSICAL_TABLE_RE = /^m\d+_t\d+$/;
|
|
|
276
382
|
/**
|
|
277
383
|
* Get company schema (cached)
|
|
278
384
|
*/
|
|
279
|
-
async function getCompanySchema(companyId) {
|
|
280
|
-
|
|
385
|
+
async function getCompanySchema(companyId, env) {
|
|
386
|
+
// Include env in cache key to avoid mixing schemas from different environments
|
|
387
|
+
const cacheKey = `${companyId}:${env || 'prod'}`;
|
|
388
|
+
const cached = schemaCache.get(cacheKey);
|
|
281
389
|
if (cached && Date.now() - cached.timestamp < SCHEMA_CACHE_TTL) {
|
|
282
390
|
return cached.schema;
|
|
283
391
|
}
|
|
284
392
|
try {
|
|
285
|
-
const data = await apiRequest("/api/cli/schema", {}, companyId);
|
|
286
|
-
schemaCache.set(
|
|
393
|
+
const data = await apiRequest("/api/cli/schema", {}, companyId, true, env);
|
|
394
|
+
schemaCache.set(cacheKey, { schema: data, timestamp: Date.now() });
|
|
287
395
|
return data;
|
|
288
396
|
}
|
|
289
397
|
catch (err) {
|
|
@@ -298,7 +406,7 @@ async function getCompanySchema(companyId) {
|
|
|
298
406
|
* @param companyId - company ID for schema lookup
|
|
299
407
|
* @returns physical table name or throws error
|
|
300
408
|
*/
|
|
301
|
-
async function resolveTableName(tableName, companyId) {
|
|
409
|
+
async function resolveTableName(tableName, companyId, env) {
|
|
302
410
|
// If already physical, return as-is
|
|
303
411
|
if (PHYSICAL_TABLE_RE.test(tableName)) {
|
|
304
412
|
return tableName;
|
|
@@ -309,7 +417,7 @@ async function resolveTableName(tableName, companyId) {
|
|
|
309
417
|
throw new Error(`Invalid table name: "${tableName}". Use physical (m123_t456) or logical (module.entity) format`);
|
|
310
418
|
}
|
|
311
419
|
const [moduleName, entityName] = parts;
|
|
312
|
-
const schema = await getCompanySchema(companyId);
|
|
420
|
+
const schema = await getCompanySchema(companyId, env);
|
|
313
421
|
if (!schema?.modules) {
|
|
314
422
|
throw new Error(`Cannot resolve logical table name "${tableName}": schema not available. Use physical name (m123_t456) instead`);
|
|
315
423
|
}
|
|
@@ -353,6 +461,8 @@ catch (err) {
|
|
|
353
461
|
function getDesc(toolName, fallback = "") {
|
|
354
462
|
return toolDescriptions[toolName]?.description || fallback;
|
|
355
463
|
}
|
|
464
|
+
// Environment parameter description (reused across tools)
|
|
465
|
+
const ENV_PARAM = { type: "string", description: "Environment: 'prod' (default) or 'dev'. Use 'dev' for development against localhost:3000" };
|
|
356
466
|
const TOOLS = [
|
|
357
467
|
// ─────────────────────────────────────────────────────────────────────────
|
|
358
468
|
// Context & Info
|
|
@@ -368,14 +478,22 @@ const TOOLS = [
|
|
|
368
478
|
company_id: { type: "string", description: "Company ID to get context for" },
|
|
369
479
|
module_id: { type: "string", description: "Module ID to get detailed context for (requires company_id)" },
|
|
370
480
|
entity_id: { type: "string", description: "Entity ID to get detailed context for (requires company_id)" },
|
|
481
|
+
env: ENV_PARAM,
|
|
371
482
|
},
|
|
372
483
|
},
|
|
373
484
|
},
|
|
374
485
|
{
|
|
375
486
|
name: "gufi_whoami",
|
|
376
487
|
description: getDesc("gufi_whoami"),
|
|
377
|
-
inputSchema: {
|
|
488
|
+
inputSchema: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: {
|
|
491
|
+
env: { type: "string", description: "Environment: 'prod' (default) or 'local'" },
|
|
492
|
+
}
|
|
493
|
+
},
|
|
378
494
|
},
|
|
495
|
+
// gufi_set_env REMOVED - environment is now passed per-request via 'env' parameter
|
|
496
|
+
// This prevents cross-session conflicts when multiple Claude instances share the MCP process
|
|
379
497
|
// gufi_schema REMOVED - functionality merged into gufi_context (always full)
|
|
380
498
|
{
|
|
381
499
|
name: "gufi_schema_modify",
|
|
@@ -385,6 +503,7 @@ const TOOLS = [
|
|
|
385
503
|
properties: {
|
|
386
504
|
company_id: { type: "string", description: "Company ID (optional if user has only one company)" },
|
|
387
505
|
preview: { type: "boolean", description: "If true, show what would be done without executing (dry run)" },
|
|
506
|
+
skip_existing: { type: "boolean", description: "If true, skip fields/entities that already exist instead of failing" },
|
|
388
507
|
operations: {
|
|
389
508
|
type: "array",
|
|
390
509
|
description: "Array of operations to execute",
|
|
@@ -425,16 +544,34 @@ const TOOLS = [
|
|
|
425
544
|
inputSchema: {
|
|
426
545
|
type: "object",
|
|
427
546
|
properties: {
|
|
428
|
-
action: { type: "string", description: "Action: list, get, create" },
|
|
547
|
+
action: { type: "string", description: "Action: list, get, create, update, delete" },
|
|
429
548
|
company_id: { type: "string", description: "Company ID" },
|
|
430
549
|
id: { type: "string", description: "Script ID (for get)" },
|
|
431
|
-
name: { type: "string", description: "Script name (for create, snake_case)" },
|
|
432
|
-
code: { type: "string", description: "JavaScript code (for create)" },
|
|
550
|
+
name: { type: "string", description: "Script name (for create/update/delete, snake_case)" },
|
|
551
|
+
code: { type: "string", description: "JavaScript code (for create/update)" },
|
|
433
552
|
description: { type: "string", description: "Description (for create)" },
|
|
553
|
+
env: ENV_PARAM,
|
|
434
554
|
},
|
|
435
555
|
required: ["action", "company_id"],
|
|
436
556
|
},
|
|
437
557
|
},
|
|
558
|
+
{
|
|
559
|
+
name: "gufi_automation_test",
|
|
560
|
+
description: "Ejecutar/testear un automation script manualmente.\n\nEjemplo:\ngufi_automation_test({\n company_id: '116',\n script_name: 'send_email',\n entity: 'ventas.facturas',\n trigger_event: 'on_click',\n row: { id: 123, total: 150 },\n input: { email_to: 'test@example.com' } // Para on_click\n})",
|
|
561
|
+
inputSchema: {
|
|
562
|
+
type: "object",
|
|
563
|
+
properties: {
|
|
564
|
+
company_id: { type: "string", description: "Company ID" },
|
|
565
|
+
script_name: { type: "string", description: "Script name to execute" },
|
|
566
|
+
entity: { type: "string", description: "Entity name (module.entity format like 'ventas.facturas')" },
|
|
567
|
+
trigger_event: { type: "string", description: "Trigger event: on_create, on_update, on_delete, on_click" },
|
|
568
|
+
row: { type: "object", description: "Row data to pass to the script (must include id)" },
|
|
569
|
+
input: { type: "object", description: "Input data for on_click triggers (goes to context.input)" },
|
|
570
|
+
env: ENV_PARAM,
|
|
571
|
+
},
|
|
572
|
+
required: ["company_id", "script_name", "entity", "trigger_event", "row"],
|
|
573
|
+
},
|
|
574
|
+
},
|
|
438
575
|
{
|
|
439
576
|
name: "gufi_triggers",
|
|
440
577
|
description: getDesc("gufi_triggers"),
|
|
@@ -447,6 +584,7 @@ const TOOLS = [
|
|
|
447
584
|
type: "object",
|
|
448
585
|
description: "Trigger config to SET (omit to GET current config). Format: { on_create: [...], on_update: [...], on_delete: [...], on_click: [...] }",
|
|
449
586
|
},
|
|
587
|
+
env: ENV_PARAM,
|
|
450
588
|
},
|
|
451
589
|
required: ["entity_id", "company_id"],
|
|
452
590
|
},
|
|
@@ -460,6 +598,7 @@ const TOOLS = [
|
|
|
460
598
|
company_id: { type: "string", description: "Company ID" },
|
|
461
599
|
limit: { type: "number", description: "Number of records (default 20)" },
|
|
462
600
|
script_name: { type: "string", description: "Filter by script name" },
|
|
601
|
+
env: ENV_PARAM,
|
|
463
602
|
},
|
|
464
603
|
required: ["company_id"],
|
|
465
604
|
},
|
|
@@ -473,7 +612,7 @@ const TOOLS = [
|
|
|
473
612
|
inputSchema: {
|
|
474
613
|
type: "object",
|
|
475
614
|
properties: {
|
|
476
|
-
action: { type: "string", description: "Action: list, get, create, update, delete" },
|
|
615
|
+
action: { type: "string", description: "Action: list, get, create, update, delete, aggregate" },
|
|
477
616
|
table: { type: "string", description: "Table name (logical name like 'ventas.productos' or physical ID)" },
|
|
478
617
|
company_id: { type: "string", description: "Company ID (required)" },
|
|
479
618
|
id: { type: "number", description: "Row ID (for get, update, delete)" },
|
|
@@ -483,6 +622,13 @@ const TOOLS = [
|
|
|
483
622
|
sort: { type: "string", description: "Field to sort by (default: id)" },
|
|
484
623
|
order: { type: "string", description: "ASC or DESC (default: DESC)" },
|
|
485
624
|
filter: { type: "string", description: "Filter expression (field=value)" },
|
|
625
|
+
agg: { type: "string", description: "Aggregation function for 'aggregate' action: count, sum, avg, min, max" },
|
|
626
|
+
field: { type: "string", description: "Field to aggregate (for 'aggregate' action)" },
|
|
627
|
+
groupBy: { type: "string", description: "Field to group by (for 'aggregate' action)" },
|
|
628
|
+
dateField: { type: "string", description: "Date field name for date range filtering (for 'aggregate' action)" },
|
|
629
|
+
dateFrom: { type: "string", description: "Start date ISO string for date range (for 'aggregate' action)" },
|
|
630
|
+
dateTo: { type: "string", description: "End date ISO string for date range (for 'aggregate' action)" },
|
|
631
|
+
env: ENV_PARAM,
|
|
486
632
|
},
|
|
487
633
|
required: ["action", "table", "company_id"],
|
|
488
634
|
},
|
|
@@ -500,6 +646,7 @@ const TOOLS = [
|
|
|
500
646
|
company_id: { type: "string", description: "Company ID" },
|
|
501
647
|
key: { type: "string", description: "Variable name (for set/delete)" },
|
|
502
648
|
value: { type: "string", description: "Variable value (for set)" },
|
|
649
|
+
env: ENV_PARAM,
|
|
503
650
|
},
|
|
504
651
|
required: ["action", "company_id"],
|
|
505
652
|
},
|
|
@@ -523,6 +670,7 @@ Example: gufi_view_pull({ view_id: 13 })`,
|
|
|
523
670
|
properties: {
|
|
524
671
|
view_id: { type: "number", description: "View ID to pull" },
|
|
525
672
|
company_id: { type: "string", description: "Company ID (required for company-specific views)" },
|
|
673
|
+
env: ENV_PARAM,
|
|
526
674
|
},
|
|
527
675
|
required: ["view_id"],
|
|
528
676
|
},
|
|
@@ -543,6 +691,7 @@ Example: gufi_view_push({ view_id: 13, message: "Fixed bug in chart" })`,
|
|
|
543
691
|
properties: {
|
|
544
692
|
view_id: { type: "number", description: "View ID to push (optional if in view directory)" },
|
|
545
693
|
message: { type: "string", description: "Optional commit message for version history" },
|
|
694
|
+
env: ENV_PARAM,
|
|
546
695
|
},
|
|
547
696
|
required: [],
|
|
548
697
|
},
|
|
@@ -562,6 +711,7 @@ Example: gufi_view_push({ view_id: 13, message: "Fixed bug in chart" })`,
|
|
|
562
711
|
description: { type: "string", description: "Package description (for create)" },
|
|
563
712
|
module_id: { type: "string", description: "Module ID (for add_module, remove_module)" },
|
|
564
713
|
company_id: { type: "string", description: "Source company ID (for add_module)" },
|
|
714
|
+
env: ENV_PARAM,
|
|
565
715
|
},
|
|
566
716
|
required: ["action"],
|
|
567
717
|
},
|
|
@@ -692,13 +842,14 @@ const toolHandlers = {
|
|
|
692
842
|
// ─────────────────────────────────────────────────────────────────────────
|
|
693
843
|
// Context & Info
|
|
694
844
|
// ─────────────────────────────────────────────────────────────────────────
|
|
695
|
-
async gufi_whoami() {
|
|
845
|
+
async gufi_whoami(params) {
|
|
846
|
+
const env = params.env || DEFAULT_ENV;
|
|
696
847
|
const config = loadConfig();
|
|
697
848
|
const loggedIn = isLoggedIn();
|
|
698
849
|
let companies = [];
|
|
699
850
|
if (loggedIn) {
|
|
700
851
|
try {
|
|
701
|
-
const data = await
|
|
852
|
+
const data = await apiRequestWithEnv("/api/companies", {}, undefined, env);
|
|
702
853
|
const list = data.items || data.data || data || [];
|
|
703
854
|
companies = list.map((c) => ({ id: c.id, name: c.name }));
|
|
704
855
|
}
|
|
@@ -707,14 +858,16 @@ const toolHandlers = {
|
|
|
707
858
|
return {
|
|
708
859
|
logged_in: loggedIn,
|
|
709
860
|
email: config.email || null,
|
|
710
|
-
|
|
711
|
-
api_url: getApiUrl(),
|
|
861
|
+
env: env,
|
|
862
|
+
api_url: getApiUrl(env),
|
|
712
863
|
companies,
|
|
713
864
|
hint: companies.length > 0
|
|
714
865
|
? `Use gufi_context({ company_id: '${companies[0].id}' }) to get schema`
|
|
715
866
|
: "Login with 'gufi login' first",
|
|
867
|
+
tip: "Pass env: 'dev' to any tool to use localhost:3000 (Cloud SQL Dev) instead of gogufi.com (prod)",
|
|
716
868
|
};
|
|
717
869
|
},
|
|
870
|
+
// gufi_set_env REMOVED - use env parameter in each tool instead
|
|
718
871
|
async gufi_context(params) {
|
|
719
872
|
// Generate intelligent context based on what's requested
|
|
720
873
|
// Always returns FULL context (no more "detail" parameter)
|
|
@@ -796,12 +949,39 @@ const toolHandlers = {
|
|
|
796
949
|
// gufi_context({ company_id: "X", module_id: "Y" })
|
|
797
950
|
// gufi_context({ company_id: "X", entity_id: "Z" })
|
|
798
951
|
async gufi_schema_modify(params) {
|
|
952
|
+
// 💜 Map common type aliases to valid Gufi types
|
|
953
|
+
const TYPE_ALIASES = {
|
|
954
|
+
textarea: "text",
|
|
955
|
+
varchar: "text",
|
|
956
|
+
string: "text",
|
|
957
|
+
int: "number_int",
|
|
958
|
+
integer: "number_int",
|
|
959
|
+
float: "number_float",
|
|
960
|
+
decimal: "number_float",
|
|
961
|
+
double: "number_float",
|
|
962
|
+
bool: "boolean",
|
|
963
|
+
timestamp: "datetime",
|
|
964
|
+
money: "currency",
|
|
965
|
+
tel: "phone",
|
|
966
|
+
address: "location",
|
|
967
|
+
};
|
|
968
|
+
// Process operations: normalize field types
|
|
969
|
+
const normalizedOps = params.operations.map((op) => {
|
|
970
|
+
if (op.field?.type && TYPE_ALIASES[op.field.type]) {
|
|
971
|
+
return {
|
|
972
|
+
...op,
|
|
973
|
+
field: { ...op.field, type: TYPE_ALIASES[op.field.type] },
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
return op;
|
|
977
|
+
});
|
|
799
978
|
// Use the new /api/schema/operations endpoint for semantic operations
|
|
800
979
|
const result = await apiRequest("/api/schema/operations", {
|
|
801
980
|
method: "POST",
|
|
802
981
|
body: JSON.stringify({
|
|
803
|
-
operations:
|
|
982
|
+
operations: normalizedOps,
|
|
804
983
|
preview: params.preview || false,
|
|
984
|
+
skip_existing: params.skip_existing || false,
|
|
805
985
|
}),
|
|
806
986
|
}, params.company_id);
|
|
807
987
|
if (params.preview) {
|
|
@@ -910,12 +1090,12 @@ const toolHandlers = {
|
|
|
910
1090
|
// Automations
|
|
911
1091
|
// ─────────────────────────────────────────────────────────────────────────
|
|
912
1092
|
async gufi_automation(params) {
|
|
913
|
-
const { action, company_id, id, name, code, description } = params;
|
|
1093
|
+
const { action, company_id, id, name, code, description, env } = params;
|
|
914
1094
|
switch (action) {
|
|
915
1095
|
case "list": {
|
|
916
|
-
const data = await
|
|
1096
|
+
const data = await apiRequestWithEnv("/api/automation-scripts", {
|
|
917
1097
|
headers: { "X-Company-ID": company_id },
|
|
918
|
-
});
|
|
1098
|
+
}, company_id, env);
|
|
919
1099
|
const automations = Array.isArray(data) ? data : data.data || [];
|
|
920
1100
|
return {
|
|
921
1101
|
scripts: automations.map((a) => ({
|
|
@@ -928,9 +1108,9 @@ const toolHandlers = {
|
|
|
928
1108
|
case "get": {
|
|
929
1109
|
if (!id)
|
|
930
1110
|
throw new Error("id required for action 'get'");
|
|
931
|
-
const data = await
|
|
1111
|
+
const data = await apiRequestWithEnv("/api/automation-scripts", {
|
|
932
1112
|
headers: { "X-Company-ID": company_id },
|
|
933
|
-
});
|
|
1113
|
+
}, company_id, env);
|
|
934
1114
|
const automations = Array.isArray(data) ? data : data.data || [];
|
|
935
1115
|
const found = automations.find((a) => String(a.id) === id);
|
|
936
1116
|
if (!found)
|
|
@@ -945,33 +1125,127 @@ const toolHandlers = {
|
|
|
945
1125
|
case "create": {
|
|
946
1126
|
if (!name || !code)
|
|
947
1127
|
throw new Error("name and code required for action 'create'");
|
|
948
|
-
|
|
1128
|
+
// 💜 Check for hardcoded IDs that break between environments
|
|
1129
|
+
const createWarnings = checkForHardcodedIds(code);
|
|
1130
|
+
const response = await apiRequestWithEnv("/api/automation-scripts", {
|
|
949
1131
|
method: "POST",
|
|
950
1132
|
body: JSON.stringify({ name, code, description: description || "" }),
|
|
951
1133
|
headers: { "X-Company-ID": company_id },
|
|
952
|
-
});
|
|
953
|
-
|
|
1134
|
+
}, company_id, env);
|
|
1135
|
+
const createResult = { success: true, script: response.data || response };
|
|
1136
|
+
if (createWarnings.length > 0) {
|
|
1137
|
+
createResult.warnings = createWarnings;
|
|
1138
|
+
createResult.hint = "Use logical table names (e.g., 'stock.productos') and context.table for entity detection";
|
|
1139
|
+
}
|
|
1140
|
+
return createResult;
|
|
1141
|
+
}
|
|
1142
|
+
case "update": {
|
|
1143
|
+
if (!name || !code)
|
|
1144
|
+
throw new Error("name and code required for action 'update'");
|
|
1145
|
+
// 💜 Check for hardcoded IDs that break between environments
|
|
1146
|
+
const updateWarnings = checkForHardcodedIds(code);
|
|
1147
|
+
const response = await apiRequestWithEnv(`/api/automation-scripts/${encodeURIComponent(name)}`, {
|
|
1148
|
+
method: "PUT",
|
|
1149
|
+
body: JSON.stringify({ code }),
|
|
1150
|
+
headers: { "X-Company-ID": company_id },
|
|
1151
|
+
}, company_id, env);
|
|
1152
|
+
const updateResult = { success: true, script: response.data || response };
|
|
1153
|
+
if (updateWarnings.length > 0) {
|
|
1154
|
+
updateResult.warnings = updateWarnings;
|
|
1155
|
+
updateResult.hint = "Use logical table names (e.g., 'stock.productos') and context.table for entity detection";
|
|
1156
|
+
}
|
|
1157
|
+
return updateResult;
|
|
1158
|
+
}
|
|
1159
|
+
case "delete": {
|
|
1160
|
+
if (!name)
|
|
1161
|
+
throw new Error("name required for action 'delete'");
|
|
1162
|
+
await apiRequestWithEnv(`/api/automation-scripts/${encodeURIComponent(name)}`, {
|
|
1163
|
+
method: "DELETE",
|
|
1164
|
+
headers: { "X-Company-ID": company_id },
|
|
1165
|
+
}, company_id, env);
|
|
1166
|
+
return { success: true, deleted: name };
|
|
954
1167
|
}
|
|
955
1168
|
default:
|
|
956
|
-
throw new Error(`Unknown action: ${action}. Use: list, get, create`);
|
|
1169
|
+
throw new Error(`Unknown action: ${action}. Use: list, get, create, update, delete`);
|
|
957
1170
|
}
|
|
958
1171
|
},
|
|
1172
|
+
async gufi_automation_test(params) {
|
|
1173
|
+
const { company_id, script_name, entity, trigger_event, row, input, env } = params;
|
|
1174
|
+
// Resolve entity to get module_id and table_id
|
|
1175
|
+
const schema = await getCompanySchema(company_id, env);
|
|
1176
|
+
if (!schema?.modules) {
|
|
1177
|
+
throw new Error("Cannot get schema. Make sure company_id is correct.");
|
|
1178
|
+
}
|
|
1179
|
+
// Parse entity (module.entity format)
|
|
1180
|
+
const parts = entity.split(".");
|
|
1181
|
+
if (parts.length !== 2) {
|
|
1182
|
+
throw new Error(`Invalid entity format: "${entity}". Use module.entity (e.g., 'ventas.facturas')`);
|
|
1183
|
+
}
|
|
1184
|
+
const [moduleName, entityName] = parts;
|
|
1185
|
+
// Find module and entity IDs
|
|
1186
|
+
let moduleId = null;
|
|
1187
|
+
let tableId = null;
|
|
1188
|
+
let tableName = null;
|
|
1189
|
+
for (const mod of schema.modules) {
|
|
1190
|
+
const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
|
|
1191
|
+
mod.label?.toLowerCase() === moduleName.toLowerCase();
|
|
1192
|
+
if (!modMatch)
|
|
1193
|
+
continue;
|
|
1194
|
+
moduleId = mod.id;
|
|
1195
|
+
for (const ent of mod.entities || []) {
|
|
1196
|
+
const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
|
|
1197
|
+
ent.label?.toLowerCase() === entityName.toLowerCase();
|
|
1198
|
+
if (entMatch) {
|
|
1199
|
+
tableId = ent.id;
|
|
1200
|
+
tableName = `m${moduleId}_t${tableId}`;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (tableId)
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
if (!moduleId || !tableId) {
|
|
1208
|
+
throw new Error(`Entity "${entity}" not found in schema`);
|
|
1209
|
+
}
|
|
1210
|
+
// Call test endpoint (same as backend: accepts row and/or input)
|
|
1211
|
+
const response = await apiRequest("/api/automation-scripts/test", {
|
|
1212
|
+
method: "POST",
|
|
1213
|
+
body: JSON.stringify({
|
|
1214
|
+
company_id,
|
|
1215
|
+
module_id: moduleId,
|
|
1216
|
+
function_name: script_name,
|
|
1217
|
+
trigger_event,
|
|
1218
|
+
table_id: tableId,
|
|
1219
|
+
table_name: tableName,
|
|
1220
|
+
row,
|
|
1221
|
+
input: input || null,
|
|
1222
|
+
}),
|
|
1223
|
+
headers: { "X-Company-ID": company_id },
|
|
1224
|
+
}, company_id, true, env);
|
|
1225
|
+
return {
|
|
1226
|
+
success: true,
|
|
1227
|
+
script_name,
|
|
1228
|
+
entity,
|
|
1229
|
+
trigger_event,
|
|
1230
|
+
result: response.data || response,
|
|
1231
|
+
};
|
|
1232
|
+
},
|
|
959
1233
|
async gufi_triggers(params) {
|
|
960
|
-
const { entity_id, company_id, config } = params;
|
|
1234
|
+
const { entity_id, company_id, config, env } = params;
|
|
961
1235
|
if (config) {
|
|
962
1236
|
// SET triggers
|
|
963
1237
|
await apiRequest(`/api/entities/${entity_id}/automations`, {
|
|
964
1238
|
method: "PUT",
|
|
965
1239
|
body: JSON.stringify(config),
|
|
966
1240
|
headers: { "X-Company-ID": company_id },
|
|
967
|
-
}, company_id);
|
|
1241
|
+
}, company_id, true, env);
|
|
968
1242
|
return { success: true, entity_id, config };
|
|
969
1243
|
}
|
|
970
1244
|
else {
|
|
971
1245
|
// GET triggers
|
|
972
1246
|
const response = await apiRequest(`/api/entities/${entity_id}/automations`, {
|
|
973
1247
|
headers: { "X-Company-ID": company_id },
|
|
974
|
-
}, company_id);
|
|
1248
|
+
}, company_id, true, env);
|
|
975
1249
|
const data = response.data || response;
|
|
976
1250
|
return {
|
|
977
1251
|
entity_id,
|
|
@@ -995,27 +1269,29 @@ const toolHandlers = {
|
|
|
995
1269
|
// ─────────────────────────────────────────────────────────────────────────
|
|
996
1270
|
async gufi_data(params) {
|
|
997
1271
|
// 💜 Unified data tool - all CRUD operations in one
|
|
998
|
-
const { action, company_id } = params;
|
|
1272
|
+
const { action, company_id, env } = params;
|
|
999
1273
|
// 💜 Resolve logical table names (module.entity) to physical (m123_t456)
|
|
1000
|
-
const table = await resolveTableName(params.table, company_id);
|
|
1274
|
+
const table = await resolveTableName(params.table, company_id, env);
|
|
1001
1275
|
switch (action) {
|
|
1002
1276
|
case "list": {
|
|
1003
1277
|
const limit = params.limit || 20;
|
|
1004
1278
|
const offset = params.offset || 0;
|
|
1279
|
+
// 💜 IMPORTANTE: Backend espera page/perPage, NO pagination.current/pageSize
|
|
1005
1280
|
const body = {
|
|
1006
|
-
|
|
1281
|
+
page: Math.floor(offset / limit) + 1,
|
|
1282
|
+
perPage: limit,
|
|
1007
1283
|
sorters: [{ field: params.sort || "id", order: params.order || "desc" }],
|
|
1008
|
-
|
|
1284
|
+
filter: [], // Backend usa 'filter', no 'filters'
|
|
1009
1285
|
};
|
|
1010
1286
|
if (params.filter) {
|
|
1011
1287
|
const [field, value] = params.filter.split("=");
|
|
1012
1288
|
if (field && value)
|
|
1013
|
-
body.
|
|
1289
|
+
body.filter.push({ field, operator: "eq", value });
|
|
1014
1290
|
}
|
|
1015
1291
|
const result = await apiRequest(`/api/tables/${table}/list`, {
|
|
1016
1292
|
method: "POST",
|
|
1017
1293
|
body: JSON.stringify(body),
|
|
1018
|
-
}, company_id);
|
|
1294
|
+
}, company_id, true, env);
|
|
1019
1295
|
return { action: "list", table, total: result.total, rows: result.data || [] };
|
|
1020
1296
|
}
|
|
1021
1297
|
case "get": {
|
|
@@ -1024,7 +1300,7 @@ const toolHandlers = {
|
|
|
1024
1300
|
const result = await apiRequest(`/api/tables/${table}/getOne`, {
|
|
1025
1301
|
method: "POST",
|
|
1026
1302
|
body: JSON.stringify({ id: params.id }),
|
|
1027
|
-
}, company_id);
|
|
1303
|
+
}, company_id, true, env);
|
|
1028
1304
|
return { action: "get", table, id: params.id, row: result.data || result };
|
|
1029
1305
|
}
|
|
1030
1306
|
case "create": {
|
|
@@ -1033,7 +1309,7 @@ const toolHandlers = {
|
|
|
1033
1309
|
const result = await apiRequest(`/api/tables/${table}`, {
|
|
1034
1310
|
method: "POST",
|
|
1035
1311
|
body: JSON.stringify(params.data),
|
|
1036
|
-
}, company_id);
|
|
1312
|
+
}, company_id, true, env);
|
|
1037
1313
|
return { action: "create", table, success: true, id: result.id || result.data?.id, row: result.data || result };
|
|
1038
1314
|
}
|
|
1039
1315
|
case "update": {
|
|
@@ -1044,7 +1320,7 @@ const toolHandlers = {
|
|
|
1044
1320
|
const result = await apiRequest(`/api/tables/${table}/${params.id}`, {
|
|
1045
1321
|
method: "PUT",
|
|
1046
1322
|
body: JSON.stringify(params.data),
|
|
1047
|
-
}, company_id);
|
|
1323
|
+
}, company_id, true, env);
|
|
1048
1324
|
return { action: "update", table, success: true, id: params.id, row: result.data || result };
|
|
1049
1325
|
}
|
|
1050
1326
|
case "delete": {
|
|
@@ -1052,11 +1328,59 @@ const toolHandlers = {
|
|
|
1052
1328
|
throw new Error("id is required for 'delete' action");
|
|
1053
1329
|
await apiRequest(`/api/tables/${table}/${params.id}`, {
|
|
1054
1330
|
method: "DELETE",
|
|
1055
|
-
}, company_id);
|
|
1331
|
+
}, company_id, true, env);
|
|
1056
1332
|
return { action: "delete", table, success: true, id: params.id };
|
|
1057
1333
|
}
|
|
1334
|
+
case "aggregate": {
|
|
1335
|
+
// 💜 Analytics query using runAnalyticsSpec engine
|
|
1336
|
+
const agg = params.agg || "count";
|
|
1337
|
+
const field = params.field || "id";
|
|
1338
|
+
const groupBy = params.groupBy;
|
|
1339
|
+
const dateField = params.dateField;
|
|
1340
|
+
const dateFrom = params.dateFrom;
|
|
1341
|
+
const dateTo = params.dateTo;
|
|
1342
|
+
// Build analytics spec
|
|
1343
|
+
const spec = {
|
|
1344
|
+
source: table,
|
|
1345
|
+
measures: [
|
|
1346
|
+
{ alias: "value", fn: agg, from: "t", field },
|
|
1347
|
+
],
|
|
1348
|
+
filters: [],
|
|
1349
|
+
};
|
|
1350
|
+
// Add groupBy as dimension if provided
|
|
1351
|
+
if (groupBy) {
|
|
1352
|
+
spec.dimensions = [{ alias: groupBy, from: "t", field: groupBy }];
|
|
1353
|
+
}
|
|
1354
|
+
// Add date range filters if provided
|
|
1355
|
+
if (dateField && dateFrom) {
|
|
1356
|
+
spec.filters.push({ from: "t", field: dateField, op: ">=", value: dateFrom });
|
|
1357
|
+
}
|
|
1358
|
+
if (dateField && dateTo) {
|
|
1359
|
+
spec.filters.push({ from: "t", field: dateField, op: "<=", value: dateTo });
|
|
1360
|
+
}
|
|
1361
|
+
// Add simple filter if provided (field=value)
|
|
1362
|
+
if (params.filter) {
|
|
1363
|
+
const [filterField, filterValue] = params.filter.split("=");
|
|
1364
|
+
if (filterField && filterValue) {
|
|
1365
|
+
spec.filters.push({ from: "t", field: filterField, op: "=", value: filterValue });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const result = await apiRequest(`/api/analytics/query`, {
|
|
1369
|
+
method: "POST",
|
|
1370
|
+
body: JSON.stringify({ spec, ttlSeconds: 60 }),
|
|
1371
|
+
}, company_id, true, env);
|
|
1372
|
+
return {
|
|
1373
|
+
action: "aggregate",
|
|
1374
|
+
table,
|
|
1375
|
+
agg,
|
|
1376
|
+
field,
|
|
1377
|
+
groupBy: groupBy || null,
|
|
1378
|
+
dateRange: dateFrom || dateTo ? { from: dateFrom, to: dateTo } : null,
|
|
1379
|
+
result: Array.isArray(result) ? result : [result],
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1058
1382
|
default:
|
|
1059
|
-
throw new Error(`Unknown action: ${action}. Use: list, get, create, update, delete`);
|
|
1383
|
+
throw new Error(`Unknown action: ${action}. Use: list, get, create, update, delete, aggregate`);
|
|
1060
1384
|
}
|
|
1061
1385
|
},
|
|
1062
1386
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1281,7 +1605,7 @@ async function getClaudeOptimizedContext(companyId, moduleId, entityId, fullText
|
|
|
1281
1605
|
if (entityId)
|
|
1282
1606
|
params.set("entity_id", entityId);
|
|
1283
1607
|
const queryString = params.toString();
|
|
1284
|
-
const url = `${
|
|
1608
|
+
const url = `${getSessionApiUrl()}/api/schema/export-claude${queryString ? "?" + queryString : ""}`;
|
|
1285
1609
|
let token = getToken();
|
|
1286
1610
|
if (!token) {
|
|
1287
1611
|
token = await autoLogin();
|