gufi-cli 0.1.50 → 0.1.52

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.
Files changed (37) hide show
  1. package/dist/commands/docs.js +1 -5
  2. package/dist/index.js +1 -0
  3. package/dist/lib/docs-resolver.d.ts +8 -0
  4. package/dist/lib/docs-resolver.js +27 -0
  5. package/dist/mcp.d.ts +3 -1
  6. package/dist/mcp.js +232 -34
  7. package/docs/dev-guide/1-01-architecture.md +358 -0
  8. package/docs/dev-guide/1-02-multi-tenant.md +415 -0
  9. package/docs/dev-guide/1-03-column-types.md +594 -0
  10. package/docs/dev-guide/1-04-json-config.md +442 -0
  11. package/docs/dev-guide/1-05-authentication.md +427 -0
  12. package/docs/dev-guide/2-01-api-reference.md +564 -0
  13. package/docs/dev-guide/2-02-automations.md +508 -0
  14. package/docs/dev-guide/2-03-gufi-cli.md +568 -0
  15. package/docs/dev-guide/2-04-realtime.md +401 -0
  16. package/docs/dev-guide/2-05-permissions.md +497 -0
  17. package/docs/dev-guide/2-06-integrations-overview.md +104 -0
  18. package/docs/dev-guide/2-07-stripe.md +173 -0
  19. package/docs/dev-guide/2-08-nayax.md +297 -0
  20. package/docs/dev-guide/2-09-ourvend.md +226 -0
  21. package/docs/dev-guide/2-10-tns.md +177 -0
  22. package/docs/dev-guide/2-11-custom-http.md +268 -0
  23. package/docs/dev-guide/3-01-custom-views.md +555 -0
  24. package/docs/dev-guide/3-02-webhooks-api.md +446 -0
  25. package/docs/mcp/00-overview.md +329 -0
  26. package/docs/mcp/01-architecture.md +220 -0
  27. package/docs/mcp/02-modules.md +285 -0
  28. package/docs/mcp/03-fields.md +357 -0
  29. package/docs/mcp/04-views.md +613 -0
  30. package/docs/mcp/05-automations.md +461 -0
  31. package/docs/mcp/06-api.md +480 -0
  32. package/docs/mcp/07-packages.md +246 -0
  33. package/docs/mcp/08-common-errors.md +284 -0
  34. package/docs/mcp/09-examples.md +453 -0
  35. package/docs/mcp/README.md +71 -0
  36. package/docs/mcp/tool-descriptions.json +49 -0
  37. package/package.json +3 -2
@@ -9,12 +9,8 @@
9
9
  */
10
10
  import * as fs from "fs";
11
11
  import * as path from "path";
12
- import { fileURLToPath } from "url";
13
12
  import chalk from "chalk";
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = path.dirname(__filename);
16
- // docs/mcp path relative to CLI
17
- const DOCS_MCP_PATH = path.resolve(__dirname, "../../../../docs/mcp");
13
+ import { DOCS_MCP_PATH } from "../lib/docs-resolver.js";
18
14
  // Topic to file mapping
19
15
  const TOPIC_FILES = {
20
16
  overview: { file: "00-overview.md", description: "Overview and workflow" },
package/dist/index.js CHANGED
@@ -428,6 +428,7 @@ program
428
428
  program
429
429
  .command("mcp")
430
430
  .description("Start MCP server for Claude integration (stdio transport)")
431
+ .option("--company <id>", "Company ID for role-based tool filtering (Workforce mode)")
431
432
  .action(startMcpServer);
432
433
  // ════════════════════════════════════════════════════════════════════
433
434
  // 💜 Claude Code Integration
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Resolves documentation paths for both development (monorepo) and global install.
3
+ *
4
+ * - Global install: docs are bundled at <pkg>/docs/
5
+ * - Development: docs live at <monorepo>/docs/
6
+ */
7
+ export declare const DOCS_MCP_PATH: string;
8
+ export declare const DOCS_DEV_GUIDE_PATH: string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Resolves documentation paths for both development (monorepo) and global install.
3
+ *
4
+ * - Global install: docs are bundled at <pkg>/docs/
5
+ * - Development: docs live at <monorepo>/docs/
6
+ */
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import { fileURLToPath } from "url";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ // Package root: from dist/lib/ go up 2 levels
13
+ const PKG_ROOT = path.resolve(__dirname, "../..");
14
+ function resolveDocsPath(subdir) {
15
+ // 1. Package-local (global install): <pkg>/docs/<subdir>
16
+ const localPath = path.join(PKG_ROOT, "docs", subdir);
17
+ if (fs.existsSync(localPath))
18
+ return localPath;
19
+ // 2. Monorepo (development): <pkg>/../../docs/<subdir>
20
+ const monorepoPath = path.resolve(PKG_ROOT, "../../docs", subdir);
21
+ if (fs.existsSync(monorepoPath))
22
+ return monorepoPath;
23
+ // Fallback to local path (will fail gracefully downstream)
24
+ return localPath;
25
+ }
26
+ export const DOCS_MCP_PATH = resolveDocsPath("mcp");
27
+ export const DOCS_DEV_GUIDE_PATH = resolveDocsPath("dev-guide");
package/dist/mcp.d.ts CHANGED
@@ -10,4 +10,6 @@
10
10
  *
11
11
  * The MCP server exposes all CLI functionality as tools that Claude can call.
12
12
  */
13
- export declare function startMcpServer(): Promise<void>;
13
+ export declare function startMcpServer(options?: {
14
+ company?: string;
15
+ }): Promise<void>;
package/dist/mcp.js CHANGED
@@ -48,10 +48,8 @@ const COLUMN_TYPE_NAMES = [
48
48
  // For ES modules __dirname equivalent
49
49
  const __filename = fileURLToPath(import.meta.url);
50
50
  const __dirname = path.dirname(__filename);
51
- // docs/mcp path relative to CLI
52
- const DOCS_MCP_PATH = path.resolve(__dirname, "../../../docs/mcp");
53
- // docs/dev-guide path for integration docs
54
- const DOCS_DEV_GUIDE_PATH = path.resolve(__dirname, "../../../docs/dev-guide");
51
+ // Docs paths - resolved for both global install and monorepo dev
52
+ import { DOCS_MCP_PATH, DOCS_DEV_GUIDE_PATH } from "./lib/docs-resolver.js";
55
53
  /**
56
54
  * Parse frontmatter from markdown content
57
55
  */
@@ -587,6 +585,47 @@ function getDesc(toolName, fallback = "") {
587
585
  }
588
586
  // Environment parameter description (reused across tools)
589
587
  const ENV_PARAM = { type: "string", description: "Environment: 'prod' (default) or 'dev'. Use 'dev' for development against localhost:3000" };
588
+ // ════════════════════════════════════════════════════════════════════════════
589
+ // Workforce Mode - Role-based tool filtering
590
+ // ════════════════════════════════════════════════════════════════════════════
591
+ const ADMIN_ROLES = ['admin', 'superadmin', 'consultant'];
592
+ function hasAdminAccess(roles) {
593
+ return roles.some(r => ADMIN_ROLES.includes(r.toLowerCase()));
594
+ }
595
+ // Tools that require Admin/Consultant role
596
+ const ADMIN_ONLY_TOOLS = [
597
+ 'gufi_schema_modify',
598
+ 'gufi_env',
599
+ 'gufi_automation_scripts',
600
+ 'gufi_automation_meta',
601
+ 'gufi_view_pull',
602
+ 'gufi_view_push',
603
+ 'gufi_view_test',
604
+ 'gufi_browse',
605
+ 'gufi_endpoints',
606
+ ];
607
+ // Workforce state (null = developer mode, everything allowed)
608
+ let workforceState = null;
609
+ function checkEntityWritePermission(table, action, state) {
610
+ const entityPerms = state.entityPermissions[table];
611
+ if (!entityPerms)
612
+ return false;
613
+ const permMap = {
614
+ create: 'entity:edit',
615
+ update: 'entity:edit',
616
+ delete: 'entity:delete',
617
+ };
618
+ const required = permMap[action];
619
+ for (const role of state.roles) {
620
+ const perms = entityPerms[role];
621
+ if (perms === '*')
622
+ return true;
623
+ if (Array.isArray(perms) && (perms.includes(required) || perms.includes(`${required}_own`))) {
624
+ return true;
625
+ }
626
+ }
627
+ return false;
628
+ }
590
629
  const TOOLS = [
591
630
  // ─────────────────────────────────────────────────────────────────────────
592
631
  // Context & Info
@@ -944,6 +983,61 @@ Examples:
944
983
  },
945
984
  },
946
985
  // ─────────────────────────────────────────────────────────────────────────
986
+ // Browse - Headless browser for any Gufi page
987
+ // ─────────────────────────────────────────────────────────────────────────
988
+ {
989
+ name: "gufi_browse",
990
+ description: `Browse any Gufi page in headless browser. Returns console logs, errors, API calls, and screenshot.
991
+
992
+ Use to visually verify UI changes, test dialogs, check table layouts, etc.
993
+
994
+ 3 ways to specify the page:
995
+ entity: 'ventas.productos' → opens the table view (resolves module/entity automatically)
996
+ page: 'home' | 'automations' | 'audit' | 'views' | 'env-variables' | 'endpoints' | 'integrations' | 'agents' | 'organization' | 'database'
997
+ path: '/150/m308_t4136' → any raw frontend path
998
+
999
+ Examples:
1000
+ gufi_browse({ entity: 'ventas.productos', company_id: '150' })
1001
+ gufi_browse({ page: 'home', company_id: '150' })
1002
+ gufi_browse({ page: 'automations', company_id: '150' })
1003
+ gufi_browse({ entity: 'ventas.productos', company_id: '150', actions: [{ type: 'click', selector: 'text:Exportar' }, { type: 'delay', ms: 500 }] })`,
1004
+ inputSchema: {
1005
+ type: "object",
1006
+ properties: {
1007
+ path: { type: "string", description: "Raw URL path (e.g., '/150/m308_t4136'). Use entity or page shortcuts instead when possible." },
1008
+ entity: { type: "string", description: "Entity shortcut in 'module.entity' format (e.g., 'ventas.productos'). Resolves to table view automatically." },
1009
+ page: { type: "string", description: "Page shortcut: home, automations, audit, views, env-variables, endpoints, integrations, agents, organization, database" },
1010
+ company_id: { type: "string", description: "Company ID for authentication context" },
1011
+ timeout: { type: "number", description: "Navigation timeout in ms (default: 15000)" },
1012
+ actions: {
1013
+ type: "array",
1014
+ description: `Actions to perform after page loads. Same as gufi_view_test.
1015
+ - Explore: {type:'explore'}
1016
+ - Click: {type:'click', selector:'text:Exportar'}
1017
+ - Screenshot: {type:'screenshot'}
1018
+ - Delay: {type:'delay', ms:1000}
1019
+ - Fill: {type:'fill', selector:'input[name=q]', value:'test'}`,
1020
+ items: {
1021
+ type: "object",
1022
+ properties: {
1023
+ type: { type: "string", description: "explore, click, closeModals, fill, clear, select, wait, waitForText, delay, scroll, hover, screenshot, getText, eval" },
1024
+ selector: { type: "string", description: "CSS selector OR 'text:Button Text'" },
1025
+ value: { type: "string", description: "Value for fill/select" },
1026
+ text: { type: "string", description: "Text to wait for (waitForText)" },
1027
+ ms: { type: "number", description: "Milliseconds for delay" },
1028
+ y: { type: "number", description: "Pixels to scroll" },
1029
+ timeout: { type: "number", description: "Timeout for wait (default 5000)" },
1030
+ code: { type: "string", description: "JS code for eval action" },
1031
+ },
1032
+ },
1033
+ },
1034
+ capture_screenshot: { type: "boolean", description: "Include base64 screenshot (default: true)" },
1035
+ env: ENV_PARAM,
1036
+ },
1037
+ required: ["company_id"],
1038
+ },
1039
+ },
1040
+ // ─────────────────────────────────────────────────────────────────────────
947
1041
  // 💜 DISABLED: Packages (not actively used, keeping MCP simple)
948
1042
  // ─────────────────────────────────────────────────────────────────────────
949
1043
  // gufi_package → Re-enable when marketplace packages become priority
@@ -1806,11 +1900,6 @@ const toolHandlers = {
1806
1900
  // 💜 CLI: Save to ~/gufi-dev/company_<id>/view_<id>/
1807
1901
  const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
1808
1902
  fs.mkdirSync(viewDir, { recursive: true });
1809
- // 💜 Migrate: if old path exists (~/gufi-dev/view_<id>/), remove it
1810
- const oldViewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
1811
- if (fs.existsSync(oldViewDir)) {
1812
- fs.rmSync(oldViewDir, { recursive: true, force: true });
1813
- }
1814
1903
  // 💜 Get latest snapshot for version tracking
1815
1904
  let latestSnapshot;
1816
1905
  try {
@@ -1890,7 +1979,6 @@ const toolHandlers = {
1890
1979
  }
1891
1980
  // 💜 Try to get company_id from view metadata if not provided
1892
1981
  if (!companyId && useLocal) {
1893
- // Try new path first (company_*/view_*)
1894
1982
  const companyDirs = fs.existsSync(LOCAL_VIEWS_DIR)
1895
1983
  ? fs.readdirSync(LOCAL_VIEWS_DIR).filter(d => d.startsWith("company_"))
1896
1984
  : [];
@@ -1906,18 +1994,6 @@ const toolHandlers = {
1906
1994
  break;
1907
1995
  }
1908
1996
  }
1909
- // Fallback: old path (view_*)
1910
- if (!companyId) {
1911
- const oldMetaPath = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`, ".gufi-view.json");
1912
- if (fs.existsSync(oldMetaPath)) {
1913
- try {
1914
- const meta = JSON.parse(fs.readFileSync(oldMetaPath, "utf-8"));
1915
- if (meta.company_id)
1916
- companyId = String(meta.company_id);
1917
- }
1918
- catch { /* ignore */ }
1919
- }
1920
- }
1921
1997
  }
1922
1998
  if (!companyId) {
1923
1999
  throw new Error("company_id is required. Pass it as parameter or pull the view first.");
@@ -1925,12 +2001,8 @@ const toolHandlers = {
1925
2001
  let files = [];
1926
2002
  let lastPulledSnapshot;
1927
2003
  if (useLocal) {
1928
- // 💜 CLI: Read from ~/gufi-dev/company_<id>/view_<id>/ (or legacy ~/gufi-dev/view_<id>/)
1929
- let viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
1930
- if (!fs.existsSync(viewDir)) {
1931
- // Fallback to old path
1932
- viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
1933
- }
2004
+ // 💜 CLI: Read from ~/gufi-dev/company_<id>/view_<id>/
2005
+ const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
1934
2006
  if (!fs.existsSync(viewDir)) {
1935
2007
  throw new Error(`View directory not found. Run gufi_view_pull({ view_id: ${viewId}, company_id: '${companyId}' }) first.`);
1936
2008
  }
@@ -2055,9 +2127,7 @@ const toolHandlers = {
2055
2127
  }
2056
2128
  // 💜 Update local metadata with new snapshot number (for version tracking)
2057
2129
  if (useLocal && result.snapshot) {
2058
- let viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
2059
- if (!fs.existsSync(viewDir))
2060
- viewDir = path.join(LOCAL_VIEWS_DIR, `view_${viewId}`);
2130
+ const viewDir = path.join(LOCAL_VIEWS_DIR, `company_${companyId}`, `view_${viewId}`);
2061
2131
  const metaPath = path.join(viewDir, ".gufi-view.json");
2062
2132
  if (fs.existsSync(metaPath)) {
2063
2133
  try {
@@ -2189,6 +2259,58 @@ const toolHandlers = {
2189
2259
  },
2190
2260
  // gufi_view_create removed - views are created from UI, edited via pull/push
2191
2261
  // ─────────────────────────────────────────────────────────────────────────
2262
+ // Browse - Headless browser for any page
2263
+ // ─────────────────────────────────────────────────────────────────────────
2264
+ async gufi_browse(params) {
2265
+ const { path, entity, page, company_id, timeout, actions, capture_screenshot = true, env } = params;
2266
+ if (!path && !entity && !page) {
2267
+ return { error: "One of path, entity, or page is required" };
2268
+ }
2269
+ const result = await apiRequest("/api/cli/browse", {
2270
+ method: "POST",
2271
+ body: JSON.stringify({
2272
+ path,
2273
+ entity,
2274
+ page,
2275
+ company_id,
2276
+ timeout: timeout || 15000,
2277
+ actions: actions || [],
2278
+ capture_screenshot,
2279
+ }),
2280
+ }, company_id, true, env);
2281
+ const response = {
2282
+ success: result.success,
2283
+ url: result.url,
2284
+ duration_ms: result.duration_ms,
2285
+ };
2286
+ if (result.console?.errors?.length > 0)
2287
+ response.console_errors = result.console.errors;
2288
+ if (result.console?.warnings?.length > 0)
2289
+ response.console_warnings = result.console.warnings;
2290
+ if (result.console?.logs?.length > 0)
2291
+ response.console_logs = result.console.logs;
2292
+ if (result.errors?.length > 0)
2293
+ response.errors = result.errors;
2294
+ const failedCalls = (result.api_calls || []).filter((c) => c.status >= 400);
2295
+ if (failedCalls.length > 0)
2296
+ response.failed_api_calls = failedCalls;
2297
+ response.dom = result.dom;
2298
+ if (result.actions?.length > 0)
2299
+ response.actions = result.actions;
2300
+ if (result.screenshot)
2301
+ response.screenshot = result.screenshot;
2302
+ if (!result.success) {
2303
+ response._hint = `Browse failed: ${result.error}. Check console_errors and errors for details.`;
2304
+ }
2305
+ else if (result.errors?.length > 0 || result.console?.errors?.length > 0) {
2306
+ response._hint = "Page loaded but has errors. Check console_errors and errors arrays.";
2307
+ }
2308
+ else {
2309
+ response._hint = "Page loaded successfully. Check screenshot to verify visual appearance.";
2310
+ }
2311
+ return response;
2312
+ },
2313
+ // ─────────────────────────────────────────────────────────────────────────
2192
2314
  // Packages
2193
2315
  // ─────────────────────────────────────────────────────────────────────────
2194
2316
  async gufi_package(params) {
@@ -2690,16 +2812,71 @@ async function handleRequest(request) {
2690
2812
  case "notifications/initialized":
2691
2813
  // No response needed for notifications
2692
2814
  break;
2693
- case "tools/list":
2815
+ case "tools/list": {
2816
+ let visibleTools = TOOLS;
2817
+ if (workforceState && !workforceState.isAdmin) {
2818
+ visibleTools = TOOLS.filter(t => !ADMIN_ONLY_TOOLS.includes(t.name));
2819
+ // Check if user has write on ANY entity - if not, adjust gufi_data description
2820
+ const hasAnyWrite = Object.values(workforceState.entityPermissions).some((perms) => {
2821
+ for (const role of workforceState.roles) {
2822
+ const p = perms[role];
2823
+ if (p === '*' || (Array.isArray(p) && p.some((x) => x.includes('edit'))))
2824
+ return true;
2825
+ }
2826
+ return false;
2827
+ });
2828
+ if (!hasAnyWrite) {
2829
+ visibleTools = visibleTools.map(t => {
2830
+ if (t.name !== 'gufi_data')
2831
+ return t;
2832
+ return {
2833
+ ...t,
2834
+ description: t.description.replace(/create:.*?delete:.*?\n/gs, '') + '\n\nNote: Your role only has read access (list, get, aggregate).',
2835
+ };
2836
+ });
2837
+ }
2838
+ }
2694
2839
  sendResponse({
2695
2840
  jsonrpc: "2.0",
2696
2841
  id,
2697
- result: { tools: TOOLS },
2842
+ result: { tools: visibleTools },
2698
2843
  });
2699
2844
  break;
2845
+ }
2700
2846
  case "tools/call": {
2701
2847
  const toolName = params?.name;
2702
2848
  const toolParams = params?.arguments || {};
2849
+ // Workforce permission check
2850
+ if (workforceState && !workforceState.isAdmin) {
2851
+ if (ADMIN_ONLY_TOOLS.includes(toolName)) {
2852
+ sendResponse({
2853
+ jsonrpc: "2.0",
2854
+ id,
2855
+ result: {
2856
+ content: [{ type: "text", text: JSON.stringify({
2857
+ error: `Permission denied: tool '${toolName}' requires Admin/Consultant role. Your roles: [${workforceState.roles.join(', ')}]`
2858
+ }, null, 2) }],
2859
+ isError: true,
2860
+ },
2861
+ });
2862
+ return;
2863
+ }
2864
+ if (toolName === 'gufi_data' && ['create', 'update', 'delete'].includes(toolParams.action)) {
2865
+ if (!checkEntityWritePermission(toolParams.table, toolParams.action, workforceState)) {
2866
+ sendResponse({
2867
+ jsonrpc: "2.0",
2868
+ id,
2869
+ result: {
2870
+ content: [{ type: "text", text: JSON.stringify({
2871
+ error: `Permission denied: your role cannot '${toolParams.action}' on table '${toolParams.table}'`
2872
+ }, null, 2) }],
2873
+ isError: true,
2874
+ },
2875
+ });
2876
+ return;
2877
+ }
2878
+ }
2879
+ }
2703
2880
  const handler = toolHandlers[toolName];
2704
2881
  if (!handler) {
2705
2882
  sendError(id, -32601, `Unknown tool: ${toolName}`);
@@ -2771,7 +2948,7 @@ async function handleRequest(request) {
2771
2948
  sendError(id, -32603, err.message);
2772
2949
  }
2773
2950
  }
2774
- export async function startMcpServer() {
2951
+ export async function startMcpServer(options) {
2775
2952
  // Check if logged in
2776
2953
  if (!isLoggedIn()) {
2777
2954
  const token = await autoLogin();
@@ -2780,6 +2957,27 @@ export async function startMcpServer() {
2780
2957
  process.exit(1);
2781
2958
  }
2782
2959
  }
2960
+ // Workforce mode: fetch user permissions for the specified company
2961
+ if (options?.company) {
2962
+ try {
2963
+ const env = getDefaultEnv();
2964
+ const perms = await apiRequestWithEnv("/api/cli/my-permissions", {}, options.company, env);
2965
+ workforceState = {
2966
+ companyId: options.company,
2967
+ roles: perms.roles || [],
2968
+ platformRole: perms.platform_role || 'client',
2969
+ isAdmin: hasAdminAccess(perms.roles || []),
2970
+ entityPermissions: Object.fromEntries((perms.entity_permissions || []).map((e) => [
2971
+ `${e.module_name}.${e.name}`, e.permissions
2972
+ ])),
2973
+ };
2974
+ console.error(`[gufi-mcp] Workforce mode: company=${options.company}, roles=[${workforceState.roles}], admin=${workforceState.isAdmin}`);
2975
+ }
2976
+ catch (err) {
2977
+ console.error(`[gufi-mcp] Failed to fetch permissions: ${err.message}`);
2978
+ process.exit(1);
2979
+ }
2980
+ }
2783
2981
  const rl = readline.createInterface({
2784
2982
  input: process.stdin,
2785
2983
  output: process.stdout,