@react-grab/cli 0.1.37 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
@@ -384,6 +393,45 @@ const handleError = (error) => {
384
393
  process.exit(1);
385
394
  };
386
395
  //#endregion
396
+ //#region src/utils/detect-agents.ts
397
+ const PATH_BINARIES = {
398
+ "claude-code": ["claude"],
399
+ codex: ["codex"],
400
+ cursor: ["cursor", "cursor-agent"],
401
+ droid: ["droid"],
402
+ "gemini-cli": ["gemini"],
403
+ "github-copilot": ["copilot"],
404
+ opencode: ["opencode"],
405
+ pi: ["pi", "omegon"]
406
+ };
407
+ const isCommandAvailable = (command) => {
408
+ const pathDirectories = (process.env.PATH ?? "").split(node_path.delimiter).filter(Boolean);
409
+ for (const directory of pathDirectories) {
410
+ const binaryPath = (0, node_path.join)(directory, command);
411
+ try {
412
+ if ((0, node_fs.statSync)(binaryPath).isFile()) {
413
+ (0, node_fs.accessSync)(binaryPath, node_fs.constants.X_OK);
414
+ return true;
415
+ }
416
+ } catch {}
417
+ }
418
+ return false;
419
+ };
420
+ const detectAvailableAgents = async () => {
421
+ const installedAgents = new Set(await (0, agent_install_skill.detectInstalledSkillAgents)());
422
+ return (0, agent_install_skill.getSkillAgentTypes)().filter((agent) => {
423
+ if (agent === "universal") return false;
424
+ if (installedAgents.has(agent)) return true;
425
+ return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
426
+ });
427
+ };
428
+ //#endregion
429
+ //#region src/utils/unref-stdin.ts
430
+ const unrefStdin = () => {
431
+ if (process.stdin.isTTY) return;
432
+ process.stdin.unref?.();
433
+ };
434
+ //#endregion
387
435
  //#region src/utils/prompts.ts
388
436
  const onCancel = () => {
389
437
  logger.break();
@@ -392,210 +440,74 @@ const onCancel = () => {
392
440
  process.exit(0);
393
441
  };
394
442
  const prompts$1 = (questions) => {
395
- return (0, prompts.default)(questions, { onCancel });
443
+ return (0, prompts.default)(questions, { onCancel }).finally(unrefStdin);
396
444
  };
397
445
  //#endregion
398
446
  //#region src/utils/spinner.ts
399
447
  const spinner = (text) => (0, ora.default)({ text });
400
448
  //#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
449
+ //#region src/utils/install-skill.ts
450
+ const SKILL_NAME = "react-grab";
451
+ const SKILL_SOURCE = (0, node_url.fileURLToPath)(new URL("../skills/react-grab", require("url").pathToFileURL(__filename).href));
452
+ const agentLabel = (agent) => (0, agent_install_skill.getSkillAgentConfig)(agent).displayName;
453
+ 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);
454
+ const promptSkillInstall = async ({ yes = false } = {}) => {
455
+ const detectedAgents = await detectAvailableAgents();
456
+ if (detectedAgents.length === 0) {
457
+ logger.warn("No supported agents detected.");
458
+ return false;
459
+ }
460
+ let selectedAgents = detectedAgents;
461
+ if (!yes) {
462
+ const { agents } = await prompts$1({
463
+ type: "multiselect",
464
+ name: "agents",
465
+ message: "Install the React Grab skill for:",
466
+ choices: detectedAgents.map((agent) => ({
467
+ title: agentLabel(agent),
468
+ value: agent,
469
+ selected: true
470
+ })),
471
+ instructions: false,
472
+ min: 1
564
473
  });
474
+ selectedAgents = agents ?? [];
475
+ if (selectedAgents.length === 0) return false;
565
476
  }
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;
477
+ const installSpinner = spinner("Installing React Grab skill.").start();
478
+ const { installed, failed } = await (0, agent_install_skill.add)({
479
+ source: SKILL_SOURCE,
480
+ agents: selectedAgents,
481
+ global: true,
482
+ mode: "copy"
483
+ });
484
+ if (installed.length === 0) {
485
+ installSpinner.fail("Failed to install React Grab skill.");
486
+ return false;
487
+ }
488
+ installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`);
489
+ for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
490
+ return true;
572
491
  };
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
- }))
492
+ const removeSkill = async () => {
493
+ const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => (0, node_fs.existsSync)(installedSkillDir(agent)));
494
+ for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) (0, node_fs.rmSync)(skillDir, {
495
+ recursive: true,
496
+ force: true
583
497
  });
584
- if (selectedAgents === void 0 || selectedAgents.length === 0) return false;
585
- logger.break();
586
- return installMcpServers(selectedAgents).some((result) => result.success);
498
+ for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
499
+ return agentsWithSkill.length;
587
500
  };
588
501
  //#endregion
589
502
  //#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) => {
503
+ const VERSION$5 = "0.1.39";
504
+ 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
505
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
593
506
  console.log();
594
507
  try {
595
- const cwd = opts.cwd;
596
508
  const isNonInteractive = detectNonInteractive(opts.yes);
597
509
  const preflightSpinner = spinner("Preflight checks.").start();
598
- if (!(await detectProject(cwd)).hasReactGrab) {
510
+ if (!(await detectProject(opts.cwd)).hasReactGrab) {
599
511
  preflightSpinner.fail("React Grab is not installed.");
600
512
  logger.break();
601
513
  logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
@@ -603,39 +515,12 @@ const add = new commander.Command().name("add").alias("install").description("co
603
515
  process.exit(1);
604
516
  }
605
517
  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.`);
518
+ logger.break();
519
+ if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
610
520
  logger.break();
611
521
  process.exit(1);
612
522
  }
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
- }
523
+ logger.break();
639
524
  } catch (error) {
640
525
  handleError(error);
641
526
  }
@@ -1018,6 +903,15 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
1018
903
  newContent
1019
904
  };
1020
905
  };
906
+ const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
907
+ switch (framework) {
908
+ case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
909
+ case "vite":
910
+ case "webpack": return findEntryFile(projectRoot) !== null;
911
+ case "tanstack": return findTanStackRootFile(projectRoot) !== null;
912
+ default: return false;
913
+ }
914
+ };
1021
915
  const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
1022
916
  switch (framework) {
1023
917
  case "next":
@@ -1217,7 +1111,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1217
1111
  };
1218
1112
  //#endregion
1219
1113
  //#region src/commands/configure.ts
1220
- const VERSION$4 = "0.1.37";
1114
+ const VERSION$4 = "0.1.39";
1221
1115
  const isMac = process.platform === "darwin";
1222
1116
  const META_LABEL = isMac ? "Cmd" : "Win";
1223
1117
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1836,7 +1730,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
1836
1730
  };
1837
1731
  //#endregion
1838
1732
  //#region src/commands/init.ts
1839
- const VERSION$3 = "0.1.37";
1733
+ const VERSION$3 = "0.1.39";
1840
1734
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1841
1735
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1842
1736
  const reportToCli = (type, config, error) => {
@@ -1894,6 +1788,15 @@ const printSubprojects = (searchRoot, sortedProjects) => {
1894
1788
  logger.log(` ${highlighter.dim("$")} npx grab@latest init -c ${(0, node_path.relative)(searchRoot, sortedProjects[0].path)}`);
1895
1789
  logger.break();
1896
1790
  };
1791
+ const SUPPORTED_FRAMEWORKS_LINE = "React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.";
1792
+ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks = false } = {}) => {
1793
+ failingSpinner.fail(message);
1794
+ logger.break();
1795
+ if (listSupportedFrameworks) logger.log(SUPPORTED_FRAMEWORKS_LINE);
1796
+ logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
1797
+ logger.break();
1798
+ process.exit(1);
1799
+ };
1897
1800
  const init = new commander.Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "force overwrite existing config", false).option("-k, --key <key>", "activation key (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
1898
1801
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$3)}`);
1899
1802
  console.log();
@@ -2052,25 +1955,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
2052
1955
  }
2053
1956
  }
2054
1957
  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
- }
1958
+ await promptSkillInstall({ yes: isNonInteractive });
2074
1959
  logger.break();
2075
1960
  process.exit(0);
2076
1961
  }
@@ -2085,7 +1970,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
2085
1970
  logger.break();
2086
1971
  process.exit(1);
2087
1972
  }
2088
- if (projectInfo.framework === "unknown") {
1973
+ if (projectInfo.framework === "unknown" || projectInfo.isMonorepo && !hasFrameworkEntryPoint(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType)) {
2089
1974
  let searchRoot = cwd;
2090
1975
  let reactProjects = findReactProjects(searchRoot);
2091
1976
  if (reactProjects.length === 0 && cwd !== process.cwd()) {
@@ -2122,45 +2007,21 @@ const init = new commander.Command().name("init").alias("setup").description("in
2122
2007
  process.chdir(selectedProject);
2123
2008
  const newProjectInfo = await detectProject(selectedProject);
2124
2009
  Object.assign(projectInfo, newProjectInfo);
2125
- spinner("Verifying framework.").start().succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
2126
- } else {
2127
- frameworkSpinner.fail("Could not detect a supported framework.");
2128
- logger.break();
2129
- logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
2130
- logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
2131
- logger.break();
2132
- process.exit(1);
2133
- }
2010
+ const newFrameworkSpinner = spinner("Verifying framework.").start();
2011
+ if (newProjectInfo.framework === "unknown") failWithManualSetup(newFrameworkSpinner, "Could not detect a supported framework in this project.", { listSupportedFrameworks: true });
2012
+ newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
2013
+ } else if (projectInfo.framework !== "unknown") failWithManualSetup(frameworkSpinner, `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}, but could not find an entry file.`);
2014
+ else failWithManualSetup(frameworkSpinner, "Could not detect a supported framework.", { listSupportedFrameworks: true });
2134
2015
  } else frameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`);
2135
2016
  if (projectInfo.framework === "next") spinner("Detecting router type.").start().succeed(`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`);
2136
2017
  spinner("Detecting package manager.").start().succeed(`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`);
2137
2018
  const finalFramework = projectInfo.framework;
2138
2019
  const finalPackageManager = projectInfo.packageManager;
2139
2020
  const finalNextRouterType = projectInfo.nextRouterType;
2140
- let didInstallMcp = false;
2021
+ let didInstallSkill = false;
2141
2022
  if (!isNonInteractive) {
2142
2023
  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
- }
2024
+ didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
2164
2025
  }
2165
2026
  const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
2166
2027
  if (!result.success) {
@@ -2204,7 +2065,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
2204
2065
  framework: finalFramework,
2205
2066
  packageManager: finalPackageManager,
2206
2067
  router: finalNextRouterType,
2207
- agent: didInstallMcp ? "mcp" : void 0,
2068
+ agent: didInstallSkill ? "skill" : void 0,
2208
2069
  isMonorepo: projectInfo.isMonorepo
2209
2070
  });
2210
2071
  } catch (error) {
@@ -2214,29 +2075,16 @@ const init = new commander.Command().name("init").alias("setup").description("in
2214
2075
  });
2215
2076
  //#endregion
2216
2077
  //#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) => {
2078
+ const VERSION$2 = "0.1.39";
2079
+ const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
2219
2080
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
2220
2081
  console.log();
2221
2082
  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
2083
  logger.break();
2239
- logger.warn("To remove the MCP server, delete the react-grab-mcp entry from your agent's MCP config file.");
2084
+ const removedCount = await removeSkill();
2085
+ logger.break();
2086
+ if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
2087
+ else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
2240
2088
  logger.break();
2241
2089
  } catch (error) {
2242
2090
  handleError(error);
@@ -2244,7 +2092,7 @@ const remove = new commander.Command().name("remove").description("disconnect Re
2244
2092
  });
2245
2093
  //#endregion
2246
2094
  //#region src/commands/upgrade.ts
2247
- const VERSION$1 = "0.1.37";
2095
+ const VERSION$1 = "0.1.39";
2248
2096
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2249
2097
  const fetchLatestVersion = async () => {
2250
2098
  try {
@@ -2318,8 +2166,368 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
2318
2166
  }
2319
2167
  });
2320
2168
  //#endregion
2169
+ //#region src/utils/clipboard.ts
2170
+ const READ_TIMEOUT_MS = 2500;
2171
+ const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
2172
+ const ID_RADIX = 36;
2173
+ const HASH_LENGTH = 12;
2174
+ const PICKLE_HEADER_BYTES = 4;
2175
+ const PICKLE_ALIGN_BYTES = 4;
2176
+ const SIGNATURE_SCAN_CHARS = 32 * 1024;
2177
+ const GRAB_MIME = "application/x-react-grab";
2178
+ const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
2179
+ const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
2180
+ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
2181
+ const shortHash = (text) => (0, node_crypto.createHash)("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
2182
+ const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
2183
+ const parseChromiumPickle = (buffer) => {
2184
+ const formats = {};
2185
+ if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats;
2186
+ let offset = PICKLE_HEADER_BYTES;
2187
+ const pairCount = buffer.readUInt32LE(offset);
2188
+ offset += 4;
2189
+ for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
2190
+ if (offset + 4 > buffer.length) break;
2191
+ const formatCodeUnits = buffer.readUInt32LE(offset);
2192
+ offset += 4;
2193
+ if (offset + formatCodeUnits * 2 > buffer.length) break;
2194
+ const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2);
2195
+ offset = alignUp(offset + formatCodeUnits * 2);
2196
+ if (offset + 4 > buffer.length) break;
2197
+ const dataCodeUnits = buffer.readUInt32LE(offset);
2198
+ offset += 4;
2199
+ if (offset + dataCodeUnits * 2 > buffer.length) break;
2200
+ const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2);
2201
+ offset = alignUp(offset + dataCodeUnits * 2);
2202
+ formats[format] = value;
2203
+ }
2204
+ return formats;
2205
+ };
2206
+ const extractGrab = (raw) => {
2207
+ if (raw.grab) return raw.grab;
2208
+ if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME];
2209
+ };
2210
+ const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text);
2211
+ const extractPrompt = (record) => {
2212
+ const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean);
2213
+ if (comments.length > 0) return comments.join("\n");
2214
+ const lines = (typeof record.content === "string" ? record.content : "").split("\n");
2215
+ const firstReferenceLine = lines.findIndex((line) => line.startsWith("["));
2216
+ if (firstReferenceLine <= 0) return void 0;
2217
+ return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0;
2218
+ };
2219
+ const hasCommand = (name) => {
2220
+ return (0, node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0;
2221
+ };
2222
+ const runText = (command, args) => {
2223
+ const output = (0, node_child_process.spawnSync)(command, args, {
2224
+ encoding: "utf8",
2225
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2226
+ timeout: READ_TIMEOUT_MS
2227
+ });
2228
+ return output.status === 0 ? output.stdout : null;
2229
+ };
2230
+ const runBuffer = (command, args) => {
2231
+ const output = (0, node_child_process.spawnSync)(command, args, {
2232
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2233
+ timeout: READ_TIMEOUT_MS
2234
+ });
2235
+ return output.status === 0 && output.stdout?.length ? output.stdout : null;
2236
+ };
2237
+ const runJson = (command, args) => {
2238
+ const output = (0, node_child_process.spawnSync)(command, args, {
2239
+ encoding: "utf8",
2240
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2241
+ timeout: READ_TIMEOUT_MS
2242
+ });
2243
+ if (output.status !== 0 || !output.stdout) return null;
2244
+ try {
2245
+ return JSON.parse(output.stdout);
2246
+ } catch {
2247
+ return null;
2248
+ }
2249
+ };
2250
+ const compileSwiftReader = (readersDir, workDir) => {
2251
+ if (!hasCommand("swiftc")) return null;
2252
+ const source = node_path.default.join(readersDir, "read-clipboard.swift");
2253
+ if (!node_fs.default.existsSync(source)) return null;
2254
+ const binary = node_path.default.join(workDir, "pbread");
2255
+ if ((!node_fs.default.existsSync(binary) || node_fs.default.statSync(source).mtimeMs > node_fs.default.statSync(binary).mtimeMs) && (0, node_child_process.spawnSync)("swiftc", [
2256
+ "-O",
2257
+ source,
2258
+ "-o",
2259
+ binary
2260
+ ]).status !== 0) return null;
2261
+ return binary;
2262
+ };
2263
+ const createDarwinReader = (options) => {
2264
+ const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
2265
+ if (binary) return {
2266
+ mode: "darwin-native",
2267
+ read: () => {
2268
+ const raw = runJson(binary, []);
2269
+ if (!raw) return null;
2270
+ return {
2271
+ changeCount: raw.changeCount ?? null,
2272
+ text: raw.text,
2273
+ grab: extractGrab(raw)
2274
+ };
2275
+ }
2276
+ };
2277
+ if (!hasCommand("pbpaste")) return null;
2278
+ return {
2279
+ mode: "darwin-text",
2280
+ read: () => {
2281
+ const text = runText("pbpaste", []);
2282
+ return text == null ? null : {
2283
+ changeCount: null,
2284
+ text,
2285
+ grab: void 0
2286
+ };
2287
+ }
2288
+ };
2289
+ };
2290
+ const detectLinuxTool = () => {
2291
+ if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return {
2292
+ name: "wl-paste",
2293
+ readText: () => runText("wl-paste", [
2294
+ "-n",
2295
+ "-t",
2296
+ "text/plain"
2297
+ ]) ?? runText("wl-paste", ["-n"]),
2298
+ readCustom: () => runBuffer("wl-paste", [
2299
+ "-n",
2300
+ "-t",
2301
+ CHROMIUM_CUSTOM_FORMAT
2302
+ ])
2303
+ };
2304
+ if (hasCommand("xclip")) return {
2305
+ name: "xclip",
2306
+ readText: () => runText("xclip", [
2307
+ "-selection",
2308
+ "clipboard",
2309
+ "-o"
2310
+ ]),
2311
+ readCustom: () => runBuffer("xclip", [
2312
+ "-selection",
2313
+ "clipboard",
2314
+ "-t",
2315
+ CHROMIUM_CUSTOM_FORMAT,
2316
+ "-o"
2317
+ ])
2318
+ };
2319
+ if (hasCommand("xsel")) return {
2320
+ name: "xsel",
2321
+ readText: () => runText("xsel", ["--clipboard", "--output"]),
2322
+ readCustom: null
2323
+ };
2324
+ return null;
2325
+ };
2326
+ const createLinuxReader = (options) => {
2327
+ const tool = detectLinuxTool();
2328
+ if (!tool) return null;
2329
+ const useCustom = !options.textOnly && Boolean(tool.readCustom);
2330
+ return {
2331
+ mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`,
2332
+ read: () => {
2333
+ const text = tool.readText();
2334
+ if (text == null) return null;
2335
+ return {
2336
+ changeCount: null,
2337
+ text,
2338
+ grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0
2339
+ };
2340
+ }
2341
+ };
2342
+ };
2343
+ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null;
2344
+ const createWindowsReader = (options) => {
2345
+ const shell = detectPowershell();
2346
+ if (!shell) return null;
2347
+ const scriptPath = node_path.default.join(options.readersDir, "read-clipboard.ps1");
2348
+ if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
2349
+ mode: "win-text",
2350
+ read: () => {
2351
+ const text = runText(shell, [
2352
+ "-NoProfile",
2353
+ "-Command",
2354
+ "Get-Clipboard -Raw"
2355
+ ]);
2356
+ return text == null ? null : {
2357
+ changeCount: null,
2358
+ text,
2359
+ grab: void 0
2360
+ };
2361
+ }
2362
+ };
2363
+ const args = [
2364
+ "-NoProfile",
2365
+ "-ExecutionPolicy",
2366
+ "Bypass",
2367
+ "-File",
2368
+ scriptPath
2369
+ ];
2370
+ (0, node_child_process.spawnSync)(shell, args, {
2371
+ stdio: "ignore",
2372
+ maxBuffer: MAX_CLIPBOARD_BYTES
2373
+ });
2374
+ return {
2375
+ mode: "win-native",
2376
+ read: () => {
2377
+ const raw = runJson(shell, args);
2378
+ if (!raw) return null;
2379
+ return {
2380
+ changeCount: raw.changeCount ?? null,
2381
+ text: raw.text ?? void 0,
2382
+ grab: extractGrab(raw)
2383
+ };
2384
+ }
2385
+ };
2386
+ };
2387
+ const createReader = (options) => {
2388
+ if (process.platform === "darwin") return createDarwinReader(options);
2389
+ if (process.platform === "linux") return createLinuxReader(options);
2390
+ if (process.platform === "win32") return createWindowsReader(options);
2391
+ return null;
2392
+ };
2393
+ const ensureSafeDir = (dir) => {
2394
+ let stats;
2395
+ try {
2396
+ stats = node_fs.default.lstatSync(dir);
2397
+ } catch {
2398
+ return;
2399
+ }
2400
+ if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`);
2401
+ if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`);
2402
+ };
2403
+ const prepareWorkDir = (dir) => {
2404
+ ensureSafeDir(dir);
2405
+ node_fs.default.mkdirSync(dir, {
2406
+ recursive: true,
2407
+ mode: 448
2408
+ });
2409
+ const gitignore = node_path.default.join(dir, ".gitignore");
2410
+ if (!node_fs.default.existsSync(gitignore)) node_fs.default.writeFileSync(gitignore, "*\n");
2411
+ };
2412
+ const watchForNextGrab = async (options) => {
2413
+ const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
2414
+ const logPath = node_path.default.join(dir, "history.jsonl");
2415
+ const { read } = reader;
2416
+ let lastChangeCount = null;
2417
+ let lastTimestamp = 0;
2418
+ let lastTextHash = "";
2419
+ let lastErrorMessage = "";
2420
+ let sequence = 0;
2421
+ const initial = read();
2422
+ if (initial && !replayLast) {
2423
+ lastChangeCount = initial.changeCount;
2424
+ if (initial.text) lastTextHash = shortHash(initial.text);
2425
+ if (initial.grab) try {
2426
+ lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
2427
+ } catch {}
2428
+ }
2429
+ while (!signal?.aborted) {
2430
+ await sleep(intervalMs);
2431
+ if (signal?.aborted) return null;
2432
+ try {
2433
+ const snapshot = read();
2434
+ if (!snapshot) continue;
2435
+ if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue;
2436
+ const textHash = snapshot.text ? shortHash(snapshot.text) : "";
2437
+ const didTextChange = textHash !== lastTextHash;
2438
+ let record = null;
2439
+ let nextTimestamp = lastTimestamp;
2440
+ if (snapshot.grab) {
2441
+ let parsed = null;
2442
+ try {
2443
+ parsed = JSON.parse(snapshot.grab);
2444
+ } catch {}
2445
+ if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) {
2446
+ nextTimestamp = parsed.timestamp;
2447
+ record = {
2448
+ source: "custom",
2449
+ timestamp: parsed.timestamp,
2450
+ version: typeof parsed.version === "string" ? parsed.version : void 0,
2451
+ content: typeof parsed.content === "string" ? parsed.content : "",
2452
+ entries: Array.isArray(parsed.entries) ? parsed.entries : []
2453
+ };
2454
+ }
2455
+ } else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = {
2456
+ source: "text",
2457
+ timestamp: Date.now(),
2458
+ content: snapshot.text,
2459
+ entries: []
2460
+ };
2461
+ if (!record) {
2462
+ lastChangeCount = snapshot.changeCount;
2463
+ lastTextHash = textHash;
2464
+ continue;
2465
+ }
2466
+ const prompt = extractPrompt(record);
2467
+ if (prompt) record.prompt = prompt;
2468
+ const captured = {
2469
+ id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`,
2470
+ receivedAt: Date.now(),
2471
+ ...record
2472
+ };
2473
+ node_fs.default.appendFileSync(logPath, `${JSON.stringify(captured)}\n`);
2474
+ lastChangeCount = snapshot.changeCount;
2475
+ lastTextHash = textHash;
2476
+ lastTimestamp = nextTimestamp;
2477
+ return captured;
2478
+ } catch (error) {
2479
+ const message = String(error?.message ?? error);
2480
+ if (message !== lastErrorMessage) {
2481
+ lastErrorMessage = message;
2482
+ onWarn?.(message);
2483
+ }
2484
+ }
2485
+ }
2486
+ return null;
2487
+ };
2488
+ //#endregion
2489
+ //#region src/commands/watch.ts
2490
+ const DEFAULT_INTERVAL_MS = 800;
2491
+ const DEFAULT_DIR = ".react-grab";
2492
+ const readersDir = () => node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
2493
+ 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) => {
2494
+ const dir = node_path.default.resolve(options.dir);
2495
+ const intervalMs = Number(options.interval);
2496
+ const textOnly = Boolean(options.textOnly);
2497
+ try {
2498
+ prepareWorkDir(dir);
2499
+ } catch (error) {
2500
+ process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2501
+ process.exit(1);
2502
+ }
2503
+ const reader = createReader({
2504
+ textOnly,
2505
+ readersDir: readersDir(),
2506
+ workDir: dir
2507
+ });
2508
+ if (!reader) {
2509
+ 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");
2510
+ process.exit(1);
2511
+ }
2512
+ process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${node_path.default.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
2513
+ watchForNextGrab({
2514
+ reader,
2515
+ dir,
2516
+ intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
2517
+ replayLast: Boolean(options.replayLast),
2518
+ onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
2519
+ }).then((grab) => {
2520
+ if (!grab) process.exit(0);
2521
+ process.stdout.write(`${JSON.stringify(grab)}\n`);
2522
+ process.exit(0);
2523
+ }).catch((error) => {
2524
+ process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2525
+ process.exit(1);
2526
+ });
2527
+ });
2528
+ //#endregion
2321
2529
  //#region src/cli.ts
2322
- const VERSION = "0.1.37";
2530
+ const VERSION = "0.1.39";
2323
2531
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2324
2532
  process.on("SIGINT", () => process.exit(0));
2325
2533
  process.on("SIGTERM", () => process.exit(0));
@@ -2332,6 +2540,7 @@ program.addCommand(add);
2332
2540
  program.addCommand(remove);
2333
2541
  program.addCommand(configure);
2334
2542
  program.addCommand(upgrade);
2543
+ program.addCommand(watch);
2335
2544
  const main = async () => {
2336
2545
  await program.parseAsync();
2337
2546
  };