@react-grab/cli 0.1.37 → 0.1.38

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/cli.cjs CHANGED
@@ -31,19 +31,15 @@ node_path = __toESM(node_path, 1);
31
31
  let package_manager_detector_detect = require("package-manager-detector/detect");
32
32
  let ignore = require("ignore");
33
33
  ignore = __toESM(ignore, 1);
34
- let node_os = require("node:os");
35
- node_os = __toESM(node_os, 1);
36
- let node_process = require("node:process");
37
- node_process = __toESM(node_process, 1);
38
- let jsonc_parser = require("jsonc-parser");
39
- jsonc_parser = __toESM(jsonc_parser, 1);
40
- let smol_toml = require("smol-toml");
41
- smol_toml = __toESM(smol_toml, 1);
34
+ let node_url = require("node:url");
35
+ let agent_install_skill = require("agent-install/skill");
42
36
  let prompts = require("prompts");
43
37
  prompts = __toESM(prompts, 1);
44
38
  let ora = require("ora");
45
39
  ora = __toESM(ora, 1);
46
40
  let tinyexec = require("tinyexec");
41
+ let node_child_process = require("node:child_process");
42
+ let node_crypto = require("node:crypto");
47
43
  //#region src/utils/is-non-interactive.ts
48
44
  const AGENT_ENVIRONMENT_VARIABLES = [
49
45
  "CI",
@@ -72,24 +68,61 @@ const detectPackageManager = async (projectRoot) => {
72
68
  }
73
69
  return "npm";
74
70
  };
75
- const detectFramework = (projectRoot) => {
71
+ const CONFIG_EXTENSIONS = [
72
+ "ts",
73
+ "mts",
74
+ "cts",
75
+ "js",
76
+ "mjs",
77
+ "cjs"
78
+ ];
79
+ const hasConfigFile = (projectRoot, configBaseName) => CONFIG_EXTENSIONS.some((extension) => (0, node_fs.existsSync)((0, node_path.join)(projectRoot, `${configBaseName}.${extension}`)));
80
+ const readMergedDependencies = (projectRoot) => {
76
81
  const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
77
- if (!(0, node_fs.existsSync)(packageJsonPath)) return "unknown";
82
+ if (!(0, node_fs.existsSync)(packageJsonPath)) return null;
78
83
  try {
79
84
  const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
80
- const allDependencies = {
85
+ return {
81
86
  ...packageJson.dependencies,
82
87
  ...packageJson.devDependencies
83
88
  };
84
- if (allDependencies["next"]) return "next";
85
- if (allDependencies["@tanstack/react-start"]) return "tanstack";
86
- if (allDependencies["vite"]) return "vite";
87
- if (allDependencies["webpack"]) return "webpack";
88
- return "unknown";
89
89
  } catch {
90
- return "unknown";
90
+ return null;
91
91
  }
92
92
  };
93
+ const detectFrameworkFromDependencies = (dependencies) => {
94
+ if (!dependencies) return "unknown";
95
+ if (dependencies["next"]) return "next";
96
+ if (dependencies["@tanstack/react-start"]) return "tanstack";
97
+ if (dependencies["vite"]) return "vite";
98
+ if (dependencies["webpack"]) return "webpack";
99
+ return "unknown";
100
+ };
101
+ const detectFrameworkFromConfigFiles = (projectRoot) => {
102
+ if (hasConfigFile(projectRoot, "next.config")) return "next";
103
+ if (hasConfigFile(projectRoot, "app.config")) return "tanstack";
104
+ if (hasConfigFile(projectRoot, "vite.config")) return "vite";
105
+ if (hasConfigFile(projectRoot, "webpack.config")) return "webpack";
106
+ return "unknown";
107
+ };
108
+ const findEnclosingMonorepoRoot = (projectRoot) => {
109
+ let currentDirectory = (0, node_path.dirname)(projectRoot);
110
+ while (currentDirectory !== (0, node_path.dirname)(currentDirectory)) {
111
+ if (detectMonorepo(currentDirectory)) return currentDirectory;
112
+ currentDirectory = (0, node_path.dirname)(currentDirectory);
113
+ }
114
+ return null;
115
+ };
116
+ const detectFramework = (projectRoot) => {
117
+ const localFramework = detectFrameworkFromDependencies(readMergedDependencies(projectRoot));
118
+ if (localFramework !== "unknown") return localFramework;
119
+ return detectFrameworkFromConfigFiles(projectRoot);
120
+ };
121
+ const detectFrameworkFromMonorepoRoot = (projectRoot) => {
122
+ const monorepoRoot = findEnclosingMonorepoRoot(projectRoot);
123
+ if (!monorepoRoot) return "unknown";
124
+ return detectFrameworkFromDependencies(readMergedDependencies(monorepoRoot));
125
+ };
93
126
  const detectNextRouterType = (projectRoot) => {
94
127
  const hasAppDir = (0, node_fs.existsSync)((0, node_path.join)(projectRoot, "app"));
95
128
  const hasSrcAppDir = (0, node_fs.existsSync)((0, node_path.join)(projectRoot, "src", "app"));
@@ -162,18 +195,9 @@ const expandWorkspacePattern = (projectRoot, pattern) => {
162
195
  return results;
163
196
  };
164
197
  const hasReactDependency = (projectPath) => {
165
- const packageJsonPath = (0, node_path.join)(projectPath, "package.json");
166
- if (!(0, node_fs.existsSync)(packageJsonPath)) return false;
167
- try {
168
- const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
169
- const allDeps = {
170
- ...packageJson.dependencies,
171
- ...packageJson.devDependencies
172
- };
173
- return Boolean(allDeps["react"] || allDeps["react-dom"]);
174
- } catch {
175
- return false;
176
- }
198
+ const dependencies = readMergedDependencies(projectPath);
199
+ if (!dependencies) return false;
200
+ return Boolean(dependencies["react"] || dependencies["react-dom"]);
177
201
  };
178
202
  const buildReactProject = (projectPath) => {
179
203
  const framework = detectFramework(projectPath);
@@ -275,14 +299,7 @@ const hasReactGrabInFile = (filePath) => {
275
299
  }
276
300
  };
277
301
  const detectReactGrab = (projectRoot) => {
278
- const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
279
- if ((0, node_fs.existsSync)(packageJsonPath)) try {
280
- const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
281
- if ({
282
- ...packageJson.dependencies,
283
- ...packageJson.devDependencies
284
- }["react-grab"]) return true;
285
- } catch {}
302
+ if (readMergedDependencies(projectRoot)?.["react-grab"]) return true;
286
303
  return [
287
304
  (0, node_path.join)(projectRoot, "app", "layout.tsx"),
288
305
  (0, node_path.join)(projectRoot, "app", "layout.jsx"),
@@ -307,22 +324,13 @@ const detectReactGrab = (projectRoot) => {
307
324
  ].some(hasReactGrabInFile);
308
325
  };
309
326
  const detectUnsupportedFramework = (projectRoot) => {
310
- const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
311
- if (!(0, node_fs.existsSync)(packageJsonPath)) return null;
312
- try {
313
- const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
314
- const allDependencies = {
315
- ...packageJson.dependencies,
316
- ...packageJson.devDependencies
317
- };
318
- if (allDependencies["@remix-run/react"] || allDependencies["remix"]) return "remix";
319
- if (allDependencies["astro"]) return "astro";
320
- if (allDependencies["@sveltejs/kit"]) return "sveltekit";
321
- if (allDependencies["gatsby"]) return "gatsby";
322
- return null;
323
- } catch {
324
- return null;
325
- }
327
+ const dependencies = readMergedDependencies(projectRoot);
328
+ if (!dependencies) return null;
329
+ if (dependencies["@remix-run/react"] || dependencies["remix"]) return "remix";
330
+ if (dependencies["astro"]) return "astro";
331
+ if (dependencies["@sveltejs/kit"]) return "sveltekit";
332
+ if (dependencies["gatsby"]) return "gatsby";
333
+ return null;
326
334
  };
327
335
  const detectReactGrabVersion = (projectRoot) => {
328
336
  const installedPackageJsonPath = (0, node_path.join)(projectRoot, "node_modules", "react-grab", "package.json");
@@ -332,7 +340,8 @@ const detectReactGrabVersion = (projectRoot) => {
332
340
  return null;
333
341
  };
334
342
  const detectProject = async (projectRoot = process.cwd()) => {
335
- const framework = detectFramework(projectRoot);
343
+ const localFramework = detectFramework(projectRoot);
344
+ const framework = localFramework === "unknown" ? detectFrameworkFromMonorepoRoot(projectRoot) : localFramework;
336
345
  return {
337
346
  packageManager: await detectPackageManager(projectRoot),
338
347
  framework,
@@ -398,204 +407,60 @@ const prompts$1 = (questions) => {
398
407
  //#region src/utils/spinner.ts
399
408
  const spinner = (text) => (0, ora.default)({ text });
400
409
  //#endregion
401
- //#region src/utils/install-mcp.ts
402
- const SERVER_NAME = "react-grab-mcp";
403
- const PACKAGE_NAME = "@react-grab/mcp";
404
- const getXdgConfigHome = () => node_process.default.env.XDG_CONFIG_HOME || node_path.default.join(node_os.default.homedir(), ".config");
405
- const getBaseDir = () => {
406
- const homeDir = node_os.default.homedir();
407
- if (node_process.default.platform === "win32") return node_process.default.env.APPDATA || node_path.default.join(homeDir, "AppData", "Roaming");
408
- if (node_process.default.platform === "darwin") return node_path.default.join(homeDir, "Library", "Application Support");
409
- return getXdgConfigHome();
410
- };
411
- const getZedConfigPath = () => {
412
- if (node_process.default.platform === "win32") return node_path.default.join(getBaseDir(), "Zed", "settings.json");
413
- return node_path.default.join(node_os.default.homedir(), ".config", "zed", "settings.json");
414
- };
415
- const getOpenCodeConfigPath = () => {
416
- const configDir = node_path.default.join(getXdgConfigHome(), "opencode");
417
- const jsoncPath = node_path.default.join(configDir, "opencode.jsonc");
418
- const jsonPath = node_path.default.join(configDir, "opencode.json");
419
- if (node_fs.default.existsSync(jsoncPath)) return jsoncPath;
420
- if (node_fs.default.existsSync(jsonPath)) return jsonPath;
421
- return jsoncPath;
422
- };
423
- const getClients = () => {
424
- const homeDir = node_os.default.homedir();
425
- const baseDir = getBaseDir();
426
- const stdioConfig = {
427
- command: "npx",
428
- args: [
429
- "-y",
430
- PACKAGE_NAME,
431
- "--stdio"
432
- ]
433
- };
434
- return [
435
- {
436
- name: "Claude Code",
437
- configPath: node_path.default.join(homeDir, ".claude.json"),
438
- configKey: "mcpServers",
439
- format: "json",
440
- serverConfig: stdioConfig
441
- },
442
- {
443
- name: "Codex",
444
- configPath: node_path.default.join(node_process.default.env.CODEX_HOME || node_path.default.join(homeDir, ".codex"), "config.toml"),
445
- configKey: "mcp_servers",
446
- format: "toml",
447
- serverConfig: stdioConfig
448
- },
449
- {
450
- name: "Cursor",
451
- configPath: node_path.default.join(homeDir, ".cursor", "mcp.json"),
452
- configKey: "mcpServers",
453
- format: "json",
454
- serverConfig: stdioConfig
455
- },
456
- {
457
- name: "OpenCode",
458
- configPath: getOpenCodeConfigPath(),
459
- configKey: "mcp",
460
- format: "json",
461
- serverConfig: {
462
- type: "local",
463
- command: [
464
- "npx",
465
- "-y",
466
- PACKAGE_NAME,
467
- "--stdio"
468
- ]
469
- }
470
- },
471
- {
472
- name: "VS Code",
473
- configPath: node_path.default.join(baseDir, "Code", "User", "mcp.json"),
474
- configKey: "servers",
475
- format: "json",
476
- serverConfig: {
477
- type: "stdio",
478
- ...stdioConfig
479
- }
480
- },
481
- {
482
- name: "Amp",
483
- configPath: node_path.default.join(homeDir, ".config", "amp", "settings.json"),
484
- configKey: "amp.mcpServers",
485
- format: "json",
486
- serverConfig: stdioConfig
487
- },
488
- {
489
- name: "Droid",
490
- configPath: node_path.default.join(homeDir, ".factory", "mcp.json"),
491
- configKey: "mcpServers",
492
- format: "json",
493
- serverConfig: {
494
- type: "stdio",
495
- ...stdioConfig
496
- }
497
- },
498
- {
499
- name: "Windsurf",
500
- configPath: node_path.default.join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
501
- configKey: "mcpServers",
502
- format: "json",
503
- serverConfig: stdioConfig
504
- },
505
- {
506
- name: "Zed",
507
- configPath: getZedConfigPath(),
508
- configKey: "context_servers",
509
- format: "json",
510
- serverConfig: {
511
- source: "custom",
512
- ...stdioConfig,
513
- env: {}
514
- }
515
- }
516
- ];
517
- };
518
- const ensureDirectory = (filePath) => {
519
- const directory = node_path.default.dirname(filePath);
520
- if (!node_fs.default.existsSync(directory)) node_fs.default.mkdirSync(directory, { recursive: true });
521
- };
522
- const JSONC_FORMAT_OPTIONS = {
523
- tabSize: 2,
524
- insertSpaces: true
525
- };
526
- const upsertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
527
- const edits = jsonc_parser.modify(content, [configKey, serverName], serverConfig, { formattingOptions: JSONC_FORMAT_OPTIONS });
528
- node_fs.default.writeFileSync(filePath, jsonc_parser.applyEdits(content, edits));
529
- };
530
- const installJsonClient = (client) => {
531
- ensureDirectory(client.configPath);
532
- const content = node_fs.default.existsSync(client.configPath) ? node_fs.default.readFileSync(client.configPath, "utf8") : "{}";
533
- upsertIntoJsonc(client.configPath, content, client.configKey, SERVER_NAME, client.serverConfig);
534
- };
535
- const installTomlClient = (client) => {
536
- ensureDirectory(client.configPath);
537
- const existingConfig = node_fs.default.existsSync(client.configPath) ? smol_toml.parse(node_fs.default.readFileSync(client.configPath, "utf8")) : {};
538
- const serverSection = existingConfig[client.configKey] ?? {};
539
- serverSection[SERVER_NAME] = client.serverConfig;
540
- existingConfig[client.configKey] = serverSection;
541
- node_fs.default.writeFileSync(client.configPath, smol_toml.stringify(existingConfig));
542
- };
543
- const getMcpClientNames = () => getClients().map((client) => client.name);
544
- const installMcpServers = (selectedClients) => {
545
- const allClients = getClients();
546
- const clients = selectedClients ? allClients.filter((client) => selectedClients.includes(client.name)) : allClients;
547
- const results = [];
548
- const installSpinner = spinner("Installing MCP server.").start();
549
- for (const client of clients) try {
550
- if (client.format === "toml") installTomlClient(client);
551
- else installJsonClient(client);
552
- results.push({
553
- client: client.name,
554
- configPath: client.configPath,
555
- success: true
556
- });
557
- } catch (error) {
558
- const message = error instanceof Error ? error.message : String(error);
559
- results.push({
560
- client: client.name,
561
- configPath: client.configPath,
562
- success: false,
563
- error: message
410
+ //#region src/utils/install-skill.ts
411
+ const SKILL_NAME = "react-grab";
412
+ const SKILL_SOURCE = (0, node_url.fileURLToPath)(new URL("../skills/react-grab", require("url").pathToFileURL(__filename).href));
413
+ const agentLabel = (agent) => (0, agent_install_skill.getSkillAgentConfig)(agent).displayName;
414
+ const installedSkillDir = (agent) => (0, node_path.join)((0, agent_install_skill.isUniversalSkillAgent)(agent) ? (0, agent_install_skill.getCanonicalSkillsDir)(true) : (0, agent_install_skill.getSkillAgentDir)(agent, { global: true }), SKILL_NAME);
415
+ const promptSkillInstall = async ({ yes = false } = {}) => {
416
+ const agents = await (0, agent_install_skill.detectInstalledSkillAgents)();
417
+ if (agents.length === 0) {
418
+ logger.warn("No supported agents detected.");
419
+ return false;
420
+ }
421
+ if (!yes) {
422
+ const { confirmed } = await prompts$1({
423
+ type: "confirm",
424
+ name: "confirmed",
425
+ message: `Install the React Grab skill for ${highlighter.info(agents.map(agentLabel).join(", "))}?`,
426
+ initial: true
564
427
  });
428
+ if (!confirmed) return false;
565
429
  }
566
- const successCount = results.filter((result) => result.success).length;
567
- if (successCount < results.length) installSpinner.warn(`Installed to ${successCount}/${results.length} agents.`);
568
- else installSpinner.succeed(`Installed to ${successCount} agents.`);
569
- for (const result of results) if (result.success) logger.log(` ${highlighter.success("✓")} ${result.client} ${highlighter.dim("→")} ${highlighter.dim(result.configPath)}`);
570
- else logger.log(` ${highlighter.error("✗")} ${result.client} ${highlighter.dim("→")} ${result.error}`);
571
- return results;
430
+ const installSpinner = spinner("Installing React Grab skill.").start();
431
+ const { installed, failed } = await (0, agent_install_skill.add)({
432
+ source: SKILL_SOURCE,
433
+ agents,
434
+ global: true,
435
+ mode: "copy"
436
+ });
437
+ if (installed.length === 0) {
438
+ installSpinner.fail("Failed to install React Grab skill.");
439
+ return false;
440
+ }
441
+ installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`);
442
+ for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
443
+ return true;
572
444
  };
573
- const promptMcpInstall = async () => {
574
- const { selectedAgents } = await prompts$1({
575
- type: "multiselect",
576
- name: "selectedAgents",
577
- message: "Select agents to install MCP server for:",
578
- choices: getMcpClientNames().map((name) => ({
579
- title: name,
580
- value: name,
581
- selected: true
582
- }))
445
+ const removeSkill = async () => {
446
+ const agentsWithSkill = (await (0, agent_install_skill.detectInstalledSkillAgents)()).filter((agent) => (0, node_fs.existsSync)(installedSkillDir(agent)));
447
+ for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) (0, node_fs.rmSync)(skillDir, {
448
+ recursive: true,
449
+ force: true
583
450
  });
584
- if (selectedAgents === void 0 || selectedAgents.length === 0) return false;
585
- logger.break();
586
- return installMcpServers(selectedAgents).some((result) => result.success);
451
+ for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
452
+ return agentsWithSkill.length;
587
453
  };
588
454
  //#endregion
589
455
  //#region src/commands/add.ts
590
- const VERSION$5 = "0.1.37";
591
- const add = new commander.Command().name("add").alias("install").description("connect React Grab to your agent via MCP").argument("[agent]", "agent to connect (mcp)").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (agentArg, opts) => {
456
+ const VERSION$5 = "0.1.38";
457
+ const add = new commander.Command().name("add").alias("install").description("install the React Grab skill for your agent").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
592
458
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
593
459
  console.log();
594
460
  try {
595
- const cwd = opts.cwd;
596
461
  const isNonInteractive = detectNonInteractive(opts.yes);
597
462
  const preflightSpinner = spinner("Preflight checks.").start();
598
- if (!(await detectProject(cwd)).hasReactGrab) {
463
+ if (!(await detectProject(opts.cwd)).hasReactGrab) {
599
464
  preflightSpinner.fail("React Grab is not installed.");
600
465
  logger.break();
601
466
  logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
@@ -603,39 +468,12 @@ const add = new commander.Command().name("add").alias("install").description("co
603
468
  process.exit(1);
604
469
  }
605
470
  preflightSpinner.succeed();
606
- if (agentArg && agentArg !== "mcp") {
607
- logger.break();
608
- logger.warn(`Legacy agent packages are deprecated. Use ${highlighter.info("mcp")} instead.`);
609
- logger.log(`Run ${highlighter.info("grab add mcp")} to install the MCP server.`);
471
+ logger.break();
472
+ if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
610
473
  logger.break();
611
474
  process.exit(1);
612
475
  }
613
- if (agentArg === "mcp" || isNonInteractive) {
614
- if (isNonInteractive) {
615
- if (!installMcpServers().some((result) => result.success)) {
616
- logger.break();
617
- logger.error("Failed to install MCP server.");
618
- logger.break();
619
- process.exit(1);
620
- }
621
- } else if (!await promptMcpInstall()) {
622
- logger.break();
623
- process.exit(0);
624
- }
625
- logger.break();
626
- logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
627
- logger.log("Restart your agents to activate.");
628
- logger.break();
629
- } else {
630
- if (!await promptMcpInstall()) {
631
- logger.break();
632
- process.exit(0);
633
- }
634
- logger.break();
635
- logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
636
- logger.log("Restart your agents to activate.");
637
- logger.break();
638
- }
476
+ logger.break();
639
477
  } catch (error) {
640
478
  handleError(error);
641
479
  }
@@ -1217,7 +1055,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1217
1055
  };
1218
1056
  //#endregion
1219
1057
  //#region src/commands/configure.ts
1220
- const VERSION$4 = "0.1.37";
1058
+ const VERSION$4 = "0.1.38";
1221
1059
  const isMac = process.platform === "darwin";
1222
1060
  const META_LABEL = isMac ? "Cmd" : "Win";
1223
1061
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1836,7 +1674,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
1836
1674
  };
1837
1675
  //#endregion
1838
1676
  //#region src/commands/init.ts
1839
- const VERSION$3 = "0.1.37";
1677
+ const VERSION$3 = "0.1.38";
1840
1678
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1841
1679
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1842
1680
  const reportToCli = (type, config, error) => {
@@ -2052,25 +1890,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
2052
1890
  }
2053
1891
  }
2054
1892
  logger.break();
2055
- const { wantAddMcp } = await prompts$1({
2056
- type: "confirm",
2057
- name: "wantAddMcp",
2058
- message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
2059
- initial: false
2060
- });
2061
- if (wantAddMcp === void 0) {
2062
- logger.break();
2063
- process.exit(1);
2064
- }
2065
- if (wantAddMcp) {
2066
- if (!await promptMcpInstall()) {
2067
- logger.break();
2068
- process.exit(0);
2069
- }
2070
- logger.break();
2071
- logger.success("MCP server has been configured.");
2072
- logger.log("Restart your agents to activate.");
2073
- }
1893
+ await promptSkillInstall({ yes: isNonInteractive });
2074
1894
  logger.break();
2075
1895
  process.exit(0);
2076
1896
  }
@@ -2122,7 +1942,16 @@ const init = new commander.Command().name("init").alias("setup").description("in
2122
1942
  process.chdir(selectedProject);
2123
1943
  const newProjectInfo = await detectProject(selectedProject);
2124
1944
  Object.assign(projectInfo, newProjectInfo);
2125
- spinner("Verifying framework.").start().succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
1945
+ const newFrameworkSpinner = spinner("Verifying framework.").start();
1946
+ if (newProjectInfo.framework === "unknown") {
1947
+ newFrameworkSpinner.fail("Could not detect a supported framework in this project.");
1948
+ logger.break();
1949
+ logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
1950
+ logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
1951
+ logger.break();
1952
+ process.exit(1);
1953
+ }
1954
+ newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
2126
1955
  } else {
2127
1956
  frameworkSpinner.fail("Could not detect a supported framework.");
2128
1957
  logger.break();
@@ -2137,30 +1966,10 @@ const init = new commander.Command().name("init").alias("setup").description("in
2137
1966
  const finalFramework = projectInfo.framework;
2138
1967
  const finalPackageManager = projectInfo.packageManager;
2139
1968
  const finalNextRouterType = projectInfo.nextRouterType;
2140
- let didInstallMcp = false;
1969
+ let didInstallSkill = false;
2141
1970
  if (!isNonInteractive) {
2142
1971
  logger.break();
2143
- const { wantAddMcp } = await prompts$1({
2144
- type: "confirm",
2145
- name: "wantAddMcp",
2146
- message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
2147
- initial: false
2148
- });
2149
- if (wantAddMcp === void 0) {
2150
- logger.break();
2151
- process.exit(1);
2152
- }
2153
- if (wantAddMcp) {
2154
- didInstallMcp = Boolean(await promptMcpInstall());
2155
- if (!didInstallMcp) {
2156
- logger.break();
2157
- process.exit(0);
2158
- }
2159
- logger.break();
2160
- logger.success("MCP server has been configured.");
2161
- logger.log("Continuing with React Grab installation...");
2162
- logger.break();
2163
- }
1972
+ didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
2164
1973
  }
2165
1974
  const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
2166
1975
  if (!result.success) {
@@ -2204,7 +2013,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
2204
2013
  framework: finalFramework,
2205
2014
  packageManager: finalPackageManager,
2206
2015
  router: finalNextRouterType,
2207
- agent: didInstallMcp ? "mcp" : void 0,
2016
+ agent: didInstallSkill ? "skill" : void 0,
2208
2017
  isMonorepo: projectInfo.isMonorepo
2209
2018
  });
2210
2019
  } catch (error) {
@@ -2214,29 +2023,16 @@ const init = new commander.Command().name("init").alias("setup").description("in
2214
2023
  });
2215
2024
  //#endregion
2216
2025
  //#region src/commands/remove.ts
2217
- const VERSION$2 = "0.1.37";
2218
- const remove = new commander.Command().name("remove").description("disconnect React Grab from your agent").argument("[agent]", "agent to disconnect (mcp)").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (agentArg, opts) => {
2026
+ const VERSION$2 = "0.1.38";
2027
+ const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
2219
2028
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
2220
2029
  console.log();
2221
2030
  try {
2222
- const cwd = opts.cwd;
2223
- const preflightSpinner = spinner("Preflight checks.").start();
2224
- if (!(await detectProject(cwd)).hasReactGrab) {
2225
- preflightSpinner.fail("React Grab is not installed.");
2226
- logger.break();
2227
- logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
2228
- logger.break();
2229
- process.exit(1);
2230
- }
2231
- preflightSpinner.succeed();
2232
- if (agentArg && agentArg !== "mcp") {
2233
- logger.break();
2234
- logger.warn(`Legacy agent packages are deprecated. Uninstall ${highlighter.info(`@react-grab/${agentArg}`)} manually with your package manager.`);
2235
- logger.break();
2236
- process.exit(0);
2237
- }
2238
2031
  logger.break();
2239
- logger.warn("To remove the MCP server, delete the react-grab-mcp entry from your agent's MCP config file.");
2032
+ const removedCount = await removeSkill();
2033
+ logger.break();
2034
+ if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
2035
+ else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
2240
2036
  logger.break();
2241
2037
  } catch (error) {
2242
2038
  handleError(error);
@@ -2244,7 +2040,7 @@ const remove = new commander.Command().name("remove").description("disconnect Re
2244
2040
  });
2245
2041
  //#endregion
2246
2042
  //#region src/commands/upgrade.ts
2247
- const VERSION$1 = "0.1.37";
2043
+ const VERSION$1 = "0.1.38";
2248
2044
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2249
2045
  const fetchLatestVersion = async () => {
2250
2046
  try {
@@ -2318,8 +2114,368 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
2318
2114
  }
2319
2115
  });
2320
2116
  //#endregion
2117
+ //#region src/utils/clipboard.ts
2118
+ const READ_TIMEOUT_MS = 2500;
2119
+ const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
2120
+ const ID_RADIX = 36;
2121
+ const HASH_LENGTH = 12;
2122
+ const PICKLE_HEADER_BYTES = 4;
2123
+ const PICKLE_ALIGN_BYTES = 4;
2124
+ const SIGNATURE_SCAN_CHARS = 32 * 1024;
2125
+ const GRAB_MIME = "application/x-react-grab";
2126
+ const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
2127
+ const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
2128
+ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
2129
+ const shortHash = (text) => (0, node_crypto.createHash)("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
2130
+ const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
2131
+ const parseChromiumPickle = (buffer) => {
2132
+ const formats = {};
2133
+ if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats;
2134
+ let offset = PICKLE_HEADER_BYTES;
2135
+ const pairCount = buffer.readUInt32LE(offset);
2136
+ offset += 4;
2137
+ for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
2138
+ if (offset + 4 > buffer.length) break;
2139
+ const formatCodeUnits = buffer.readUInt32LE(offset);
2140
+ offset += 4;
2141
+ if (offset + formatCodeUnits * 2 > buffer.length) break;
2142
+ const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2);
2143
+ offset = alignUp(offset + formatCodeUnits * 2);
2144
+ if (offset + 4 > buffer.length) break;
2145
+ const dataCodeUnits = buffer.readUInt32LE(offset);
2146
+ offset += 4;
2147
+ if (offset + dataCodeUnits * 2 > buffer.length) break;
2148
+ const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2);
2149
+ offset = alignUp(offset + dataCodeUnits * 2);
2150
+ formats[format] = value;
2151
+ }
2152
+ return formats;
2153
+ };
2154
+ const extractGrab = (raw) => {
2155
+ if (raw.grab) return raw.grab;
2156
+ if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME];
2157
+ };
2158
+ const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text);
2159
+ const extractPrompt = (record) => {
2160
+ const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean);
2161
+ if (comments.length > 0) return comments.join("\n");
2162
+ const lines = (typeof record.content === "string" ? record.content : "").split("\n");
2163
+ const firstReferenceLine = lines.findIndex((line) => line.startsWith("["));
2164
+ if (firstReferenceLine <= 0) return void 0;
2165
+ return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0;
2166
+ };
2167
+ const hasCommand = (name) => {
2168
+ return (0, node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0;
2169
+ };
2170
+ const runText = (command, args) => {
2171
+ const output = (0, node_child_process.spawnSync)(command, args, {
2172
+ encoding: "utf8",
2173
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2174
+ timeout: READ_TIMEOUT_MS
2175
+ });
2176
+ return output.status === 0 ? output.stdout : null;
2177
+ };
2178
+ const runBuffer = (command, args) => {
2179
+ const output = (0, node_child_process.spawnSync)(command, args, {
2180
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2181
+ timeout: READ_TIMEOUT_MS
2182
+ });
2183
+ return output.status === 0 && output.stdout?.length ? output.stdout : null;
2184
+ };
2185
+ const runJson = (command, args) => {
2186
+ const output = (0, node_child_process.spawnSync)(command, args, {
2187
+ encoding: "utf8",
2188
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2189
+ timeout: READ_TIMEOUT_MS
2190
+ });
2191
+ if (output.status !== 0 || !output.stdout) return null;
2192
+ try {
2193
+ return JSON.parse(output.stdout);
2194
+ } catch {
2195
+ return null;
2196
+ }
2197
+ };
2198
+ const compileSwiftReader = (readersDir, workDir) => {
2199
+ if (!hasCommand("swiftc")) return null;
2200
+ const source = node_path.default.join(readersDir, "read-clipboard.swift");
2201
+ if (!node_fs.default.existsSync(source)) return null;
2202
+ const binary = node_path.default.join(workDir, "pbread");
2203
+ if ((!node_fs.default.existsSync(binary) || node_fs.default.statSync(source).mtimeMs > node_fs.default.statSync(binary).mtimeMs) && (0, node_child_process.spawnSync)("swiftc", [
2204
+ "-O",
2205
+ source,
2206
+ "-o",
2207
+ binary
2208
+ ]).status !== 0) return null;
2209
+ return binary;
2210
+ };
2211
+ const createDarwinReader = (options) => {
2212
+ const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
2213
+ if (binary) return {
2214
+ mode: "darwin-native",
2215
+ read: () => {
2216
+ const raw = runJson(binary, []);
2217
+ if (!raw) return null;
2218
+ return {
2219
+ changeCount: raw.changeCount ?? null,
2220
+ text: raw.text,
2221
+ grab: extractGrab(raw)
2222
+ };
2223
+ }
2224
+ };
2225
+ if (!hasCommand("pbpaste")) return null;
2226
+ return {
2227
+ mode: "darwin-text",
2228
+ read: () => {
2229
+ const text = runText("pbpaste", []);
2230
+ return text == null ? null : {
2231
+ changeCount: null,
2232
+ text,
2233
+ grab: void 0
2234
+ };
2235
+ }
2236
+ };
2237
+ };
2238
+ const detectLinuxTool = () => {
2239
+ if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return {
2240
+ name: "wl-paste",
2241
+ readText: () => runText("wl-paste", [
2242
+ "-n",
2243
+ "-t",
2244
+ "text/plain"
2245
+ ]) ?? runText("wl-paste", ["-n"]),
2246
+ readCustom: () => runBuffer("wl-paste", [
2247
+ "-n",
2248
+ "-t",
2249
+ CHROMIUM_CUSTOM_FORMAT
2250
+ ])
2251
+ };
2252
+ if (hasCommand("xclip")) return {
2253
+ name: "xclip",
2254
+ readText: () => runText("xclip", [
2255
+ "-selection",
2256
+ "clipboard",
2257
+ "-o"
2258
+ ]),
2259
+ readCustom: () => runBuffer("xclip", [
2260
+ "-selection",
2261
+ "clipboard",
2262
+ "-t",
2263
+ CHROMIUM_CUSTOM_FORMAT,
2264
+ "-o"
2265
+ ])
2266
+ };
2267
+ if (hasCommand("xsel")) return {
2268
+ name: "xsel",
2269
+ readText: () => runText("xsel", ["--clipboard", "--output"]),
2270
+ readCustom: null
2271
+ };
2272
+ return null;
2273
+ };
2274
+ const createLinuxReader = (options) => {
2275
+ const tool = detectLinuxTool();
2276
+ if (!tool) return null;
2277
+ const useCustom = !options.textOnly && Boolean(tool.readCustom);
2278
+ return {
2279
+ mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`,
2280
+ read: () => {
2281
+ const text = tool.readText();
2282
+ if (text == null) return null;
2283
+ return {
2284
+ changeCount: null,
2285
+ text,
2286
+ grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0
2287
+ };
2288
+ }
2289
+ };
2290
+ };
2291
+ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null;
2292
+ const createWindowsReader = (options) => {
2293
+ const shell = detectPowershell();
2294
+ if (!shell) return null;
2295
+ const scriptPath = node_path.default.join(options.readersDir, "read-clipboard.ps1");
2296
+ if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
2297
+ mode: "win-text",
2298
+ read: () => {
2299
+ const text = runText(shell, [
2300
+ "-NoProfile",
2301
+ "-Command",
2302
+ "Get-Clipboard -Raw"
2303
+ ]);
2304
+ return text == null ? null : {
2305
+ changeCount: null,
2306
+ text,
2307
+ grab: void 0
2308
+ };
2309
+ }
2310
+ };
2311
+ const args = [
2312
+ "-NoProfile",
2313
+ "-ExecutionPolicy",
2314
+ "Bypass",
2315
+ "-File",
2316
+ scriptPath
2317
+ ];
2318
+ (0, node_child_process.spawnSync)(shell, args, {
2319
+ stdio: "ignore",
2320
+ maxBuffer: MAX_CLIPBOARD_BYTES
2321
+ });
2322
+ return {
2323
+ mode: "win-native",
2324
+ read: () => {
2325
+ const raw = runJson(shell, args);
2326
+ if (!raw) return null;
2327
+ return {
2328
+ changeCount: raw.changeCount ?? null,
2329
+ text: raw.text ?? void 0,
2330
+ grab: extractGrab(raw)
2331
+ };
2332
+ }
2333
+ };
2334
+ };
2335
+ const createReader = (options) => {
2336
+ if (process.platform === "darwin") return createDarwinReader(options);
2337
+ if (process.platform === "linux") return createLinuxReader(options);
2338
+ if (process.platform === "win32") return createWindowsReader(options);
2339
+ return null;
2340
+ };
2341
+ const ensureSafeDir = (dir) => {
2342
+ let stats;
2343
+ try {
2344
+ stats = node_fs.default.lstatSync(dir);
2345
+ } catch {
2346
+ return;
2347
+ }
2348
+ if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`);
2349
+ if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`);
2350
+ };
2351
+ const prepareWorkDir = (dir) => {
2352
+ ensureSafeDir(dir);
2353
+ node_fs.default.mkdirSync(dir, {
2354
+ recursive: true,
2355
+ mode: 448
2356
+ });
2357
+ const gitignore = node_path.default.join(dir, ".gitignore");
2358
+ if (!node_fs.default.existsSync(gitignore)) node_fs.default.writeFileSync(gitignore, "*\n");
2359
+ };
2360
+ const watchForNextGrab = async (options) => {
2361
+ const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
2362
+ const logPath = node_path.default.join(dir, "history.jsonl");
2363
+ const { read } = reader;
2364
+ let lastChangeCount = null;
2365
+ let lastTimestamp = 0;
2366
+ let lastTextHash = "";
2367
+ let lastErrorMessage = "";
2368
+ let sequence = 0;
2369
+ const initial = read();
2370
+ if (initial && !replayLast) {
2371
+ lastChangeCount = initial.changeCount;
2372
+ if (initial.text) lastTextHash = shortHash(initial.text);
2373
+ if (initial.grab) try {
2374
+ lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
2375
+ } catch {}
2376
+ }
2377
+ while (!signal?.aborted) {
2378
+ await sleep(intervalMs);
2379
+ if (signal?.aborted) return null;
2380
+ try {
2381
+ const snapshot = read();
2382
+ if (!snapshot) continue;
2383
+ if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue;
2384
+ const textHash = snapshot.text ? shortHash(snapshot.text) : "";
2385
+ const didTextChange = textHash !== lastTextHash;
2386
+ let record = null;
2387
+ let nextTimestamp = lastTimestamp;
2388
+ if (snapshot.grab) {
2389
+ let parsed = null;
2390
+ try {
2391
+ parsed = JSON.parse(snapshot.grab);
2392
+ } catch {}
2393
+ if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) {
2394
+ nextTimestamp = parsed.timestamp;
2395
+ record = {
2396
+ source: "custom",
2397
+ timestamp: parsed.timestamp,
2398
+ version: typeof parsed.version === "string" ? parsed.version : void 0,
2399
+ content: typeof parsed.content === "string" ? parsed.content : "",
2400
+ entries: Array.isArray(parsed.entries) ? parsed.entries : []
2401
+ };
2402
+ }
2403
+ } else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = {
2404
+ source: "text",
2405
+ timestamp: Date.now(),
2406
+ content: snapshot.text,
2407
+ entries: []
2408
+ };
2409
+ if (!record) {
2410
+ lastChangeCount = snapshot.changeCount;
2411
+ lastTextHash = textHash;
2412
+ continue;
2413
+ }
2414
+ const prompt = extractPrompt(record);
2415
+ if (prompt) record.prompt = prompt;
2416
+ const captured = {
2417
+ id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`,
2418
+ receivedAt: Date.now(),
2419
+ ...record
2420
+ };
2421
+ node_fs.default.appendFileSync(logPath, `${JSON.stringify(captured)}\n`);
2422
+ lastChangeCount = snapshot.changeCount;
2423
+ lastTextHash = textHash;
2424
+ lastTimestamp = nextTimestamp;
2425
+ return captured;
2426
+ } catch (error) {
2427
+ const message = String(error?.message ?? error);
2428
+ if (message !== lastErrorMessage) {
2429
+ lastErrorMessage = message;
2430
+ onWarn?.(message);
2431
+ }
2432
+ }
2433
+ }
2434
+ return null;
2435
+ };
2436
+ //#endregion
2437
+ //#region src/commands/watch.ts
2438
+ const DEFAULT_INTERVAL_MS = 800;
2439
+ const DEFAULT_DIR = ".react-grab";
2440
+ const readersDir = () => node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
2441
+ const watch = new commander.Command().name("watch").description("watch the clipboard for the next React Grab selection, print it, then exit").option("-d, --dir <dir>", "work dir for history.jsonl + cursor.txt", DEFAULT_DIR).option("-i, --interval <ms>", "clipboard poll interval in ms", String(DEFAULT_INTERVAL_MS)).option("--text-only", "skip the native reader and use the plain-text fallback").option("--replay-last", "also capture the grab already on the clipboard at startup").action((options) => {
2442
+ const dir = node_path.default.resolve(options.dir);
2443
+ const intervalMs = Number(options.interval);
2444
+ const textOnly = Boolean(options.textOnly);
2445
+ try {
2446
+ prepareWorkDir(dir);
2447
+ } catch (error) {
2448
+ process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2449
+ process.exit(1);
2450
+ }
2451
+ const reader = createReader({
2452
+ textOnly,
2453
+ readersDir: readersDir(),
2454
+ workDir: dir
2455
+ });
2456
+ if (!reader) {
2457
+ process.stderr.write("react-grab watch: no clipboard reader available. Linux: install xclip or wl-clipboard. macOS: install Xcode CLI tools (swiftc) or rely on pbpaste. Windows: ensure PowerShell is on PATH.\n");
2458
+ process.exit(1);
2459
+ }
2460
+ process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${node_path.default.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
2461
+ watchForNextGrab({
2462
+ reader,
2463
+ dir,
2464
+ intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
2465
+ replayLast: Boolean(options.replayLast),
2466
+ onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
2467
+ }).then((grab) => {
2468
+ if (!grab) process.exit(0);
2469
+ process.stdout.write(`${JSON.stringify(grab)}\n`);
2470
+ process.exit(0);
2471
+ }).catch((error) => {
2472
+ process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2473
+ process.exit(1);
2474
+ });
2475
+ });
2476
+ //#endregion
2321
2477
  //#region src/cli.ts
2322
- const VERSION = "0.1.37";
2478
+ const VERSION = "0.1.38";
2323
2479
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2324
2480
  process.on("SIGINT", () => process.exit(0));
2325
2481
  process.on("SIGTERM", () => process.exit(0));
@@ -2332,6 +2488,7 @@ program.addCommand(add);
2332
2488
  program.addCommand(remove);
2333
2489
  program.addCommand(configure);
2334
2490
  program.addCommand(upgrade);
2491
+ program.addCommand(watch);
2335
2492
  const main = async () => {
2336
2493
  await program.parseAsync();
2337
2494
  };