@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.js CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import pc from "picocolors";
4
- import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
- import path, { basename, dirname, join, relative, resolve } from "node:path";
4
+ import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import path, { basename, delimiter, dirname, join, relative, resolve } from "node:path";
6
6
  import { detect } from "package-manager-detector/detect";
7
7
  import ignore from "ignore";
8
- import os from "node:os";
9
- import process$1 from "node:process";
10
- import * as jsonc from "jsonc-parser";
11
- import * as TOML from "smol-toml";
8
+ import { fileURLToPath } from "node:url";
9
+ import { add, detectInstalledSkillAgents, getCanonicalSkillsDir, getSkillAgentConfig, getSkillAgentDir, getSkillAgentTypes, isUniversalSkillAgent } from "agent-install/skill";
12
10
  import basePrompts from "prompts";
13
11
  import ora from "ora";
14
12
  import { x } from "tinyexec";
13
+ import { spawnSync } from "node:child_process";
14
+ import { createHash } from "node:crypto";
15
15
  //#region src/utils/is-non-interactive.ts
16
16
  const AGENT_ENVIRONMENT_VARIABLES = [
17
17
  "CI",
@@ -40,24 +40,61 @@ const detectPackageManager = async (projectRoot) => {
40
40
  }
41
41
  return "npm";
42
42
  };
43
- const detectFramework = (projectRoot) => {
43
+ const CONFIG_EXTENSIONS = [
44
+ "ts",
45
+ "mts",
46
+ "cts",
47
+ "js",
48
+ "mjs",
49
+ "cjs"
50
+ ];
51
+ const hasConfigFile = (projectRoot, configBaseName) => CONFIG_EXTENSIONS.some((extension) => existsSync(join(projectRoot, `${configBaseName}.${extension}`)));
52
+ const readMergedDependencies = (projectRoot) => {
44
53
  const packageJsonPath = join(projectRoot, "package.json");
45
- if (!existsSync(packageJsonPath)) return "unknown";
54
+ if (!existsSync(packageJsonPath)) return null;
46
55
  try {
47
56
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
48
- const allDependencies = {
57
+ return {
49
58
  ...packageJson.dependencies,
50
59
  ...packageJson.devDependencies
51
60
  };
52
- if (allDependencies["next"]) return "next";
53
- if (allDependencies["@tanstack/react-start"]) return "tanstack";
54
- if (allDependencies["vite"]) return "vite";
55
- if (allDependencies["webpack"]) return "webpack";
56
- return "unknown";
57
61
  } catch {
58
- return "unknown";
62
+ return null;
59
63
  }
60
64
  };
65
+ const detectFrameworkFromDependencies = (dependencies) => {
66
+ if (!dependencies) return "unknown";
67
+ if (dependencies["next"]) return "next";
68
+ if (dependencies["@tanstack/react-start"]) return "tanstack";
69
+ if (dependencies["vite"]) return "vite";
70
+ if (dependencies["webpack"]) return "webpack";
71
+ return "unknown";
72
+ };
73
+ const detectFrameworkFromConfigFiles = (projectRoot) => {
74
+ if (hasConfigFile(projectRoot, "next.config")) return "next";
75
+ if (hasConfigFile(projectRoot, "app.config")) return "tanstack";
76
+ if (hasConfigFile(projectRoot, "vite.config")) return "vite";
77
+ if (hasConfigFile(projectRoot, "webpack.config")) return "webpack";
78
+ return "unknown";
79
+ };
80
+ const findEnclosingMonorepoRoot = (projectRoot) => {
81
+ let currentDirectory = dirname(projectRoot);
82
+ while (currentDirectory !== dirname(currentDirectory)) {
83
+ if (detectMonorepo(currentDirectory)) return currentDirectory;
84
+ currentDirectory = dirname(currentDirectory);
85
+ }
86
+ return null;
87
+ };
88
+ const detectFramework = (projectRoot) => {
89
+ const localFramework = detectFrameworkFromDependencies(readMergedDependencies(projectRoot));
90
+ if (localFramework !== "unknown") return localFramework;
91
+ return detectFrameworkFromConfigFiles(projectRoot);
92
+ };
93
+ const detectFrameworkFromMonorepoRoot = (projectRoot) => {
94
+ const monorepoRoot = findEnclosingMonorepoRoot(projectRoot);
95
+ if (!monorepoRoot) return "unknown";
96
+ return detectFrameworkFromDependencies(readMergedDependencies(monorepoRoot));
97
+ };
61
98
  const detectNextRouterType = (projectRoot) => {
62
99
  const hasAppDir = existsSync(join(projectRoot, "app"));
63
100
  const hasSrcAppDir = existsSync(join(projectRoot, "src", "app"));
@@ -130,18 +167,9 @@ const expandWorkspacePattern = (projectRoot, pattern) => {
130
167
  return results;
131
168
  };
132
169
  const hasReactDependency = (projectPath) => {
133
- const packageJsonPath = join(projectPath, "package.json");
134
- if (!existsSync(packageJsonPath)) return false;
135
- try {
136
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
137
- const allDeps = {
138
- ...packageJson.dependencies,
139
- ...packageJson.devDependencies
140
- };
141
- return Boolean(allDeps["react"] || allDeps["react-dom"]);
142
- } catch {
143
- return false;
144
- }
170
+ const dependencies = readMergedDependencies(projectPath);
171
+ if (!dependencies) return false;
172
+ return Boolean(dependencies["react"] || dependencies["react-dom"]);
145
173
  };
146
174
  const buildReactProject = (projectPath) => {
147
175
  const framework = detectFramework(projectPath);
@@ -243,14 +271,7 @@ const hasReactGrabInFile = (filePath) => {
243
271
  }
244
272
  };
245
273
  const detectReactGrab = (projectRoot) => {
246
- const packageJsonPath = join(projectRoot, "package.json");
247
- if (existsSync(packageJsonPath)) try {
248
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
249
- if ({
250
- ...packageJson.dependencies,
251
- ...packageJson.devDependencies
252
- }["react-grab"]) return true;
253
- } catch {}
274
+ if (readMergedDependencies(projectRoot)?.["react-grab"]) return true;
254
275
  return [
255
276
  join(projectRoot, "app", "layout.tsx"),
256
277
  join(projectRoot, "app", "layout.jsx"),
@@ -275,22 +296,13 @@ const detectReactGrab = (projectRoot) => {
275
296
  ].some(hasReactGrabInFile);
276
297
  };
277
298
  const detectUnsupportedFramework = (projectRoot) => {
278
- const packageJsonPath = join(projectRoot, "package.json");
279
- if (!existsSync(packageJsonPath)) return null;
280
- try {
281
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
282
- const allDependencies = {
283
- ...packageJson.dependencies,
284
- ...packageJson.devDependencies
285
- };
286
- if (allDependencies["@remix-run/react"] || allDependencies["remix"]) return "remix";
287
- if (allDependencies["astro"]) return "astro";
288
- if (allDependencies["@sveltejs/kit"]) return "sveltekit";
289
- if (allDependencies["gatsby"]) return "gatsby";
290
- return null;
291
- } catch {
292
- return null;
293
- }
299
+ const dependencies = readMergedDependencies(projectRoot);
300
+ if (!dependencies) return null;
301
+ if (dependencies["@remix-run/react"] || dependencies["remix"]) return "remix";
302
+ if (dependencies["astro"]) return "astro";
303
+ if (dependencies["@sveltejs/kit"]) return "sveltekit";
304
+ if (dependencies["gatsby"]) return "gatsby";
305
+ return null;
294
306
  };
295
307
  const detectReactGrabVersion = (projectRoot) => {
296
308
  const installedPackageJsonPath = join(projectRoot, "node_modules", "react-grab", "package.json");
@@ -300,7 +312,8 @@ const detectReactGrabVersion = (projectRoot) => {
300
312
  return null;
301
313
  };
302
314
  const detectProject = async (projectRoot = process.cwd()) => {
303
- const framework = detectFramework(projectRoot);
315
+ const localFramework = detectFramework(projectRoot);
316
+ const framework = localFramework === "unknown" ? detectFrameworkFromMonorepoRoot(projectRoot) : localFramework;
304
317
  return {
305
318
  packageManager: await detectPackageManager(projectRoot),
306
319
  framework,
@@ -352,6 +365,45 @@ const handleError = (error) => {
352
365
  process.exit(1);
353
366
  };
354
367
  //#endregion
368
+ //#region src/utils/detect-agents.ts
369
+ const PATH_BINARIES = {
370
+ "claude-code": ["claude"],
371
+ codex: ["codex"],
372
+ cursor: ["cursor", "cursor-agent"],
373
+ droid: ["droid"],
374
+ "gemini-cli": ["gemini"],
375
+ "github-copilot": ["copilot"],
376
+ opencode: ["opencode"],
377
+ pi: ["pi", "omegon"]
378
+ };
379
+ const isCommandAvailable = (command) => {
380
+ const pathDirectories = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
381
+ for (const directory of pathDirectories) {
382
+ const binaryPath = join(directory, command);
383
+ try {
384
+ if (statSync(binaryPath).isFile()) {
385
+ accessSync(binaryPath, constants.X_OK);
386
+ return true;
387
+ }
388
+ } catch {}
389
+ }
390
+ return false;
391
+ };
392
+ const detectAvailableAgents = async () => {
393
+ const installedAgents = new Set(await detectInstalledSkillAgents());
394
+ return getSkillAgentTypes().filter((agent) => {
395
+ if (agent === "universal") return false;
396
+ if (installedAgents.has(agent)) return true;
397
+ return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
398
+ });
399
+ };
400
+ //#endregion
401
+ //#region src/utils/unref-stdin.ts
402
+ const unrefStdin = () => {
403
+ if (process.stdin.isTTY) return;
404
+ process.stdin.unref?.();
405
+ };
406
+ //#endregion
355
407
  //#region src/utils/prompts.ts
356
408
  const onCancel = () => {
357
409
  logger.break();
@@ -360,210 +412,74 @@ const onCancel = () => {
360
412
  process.exit(0);
361
413
  };
362
414
  const prompts = (questions) => {
363
- return basePrompts(questions, { onCancel });
415
+ return basePrompts(questions, { onCancel }).finally(unrefStdin);
364
416
  };
365
417
  //#endregion
366
418
  //#region src/utils/spinner.ts
367
419
  const spinner = (text) => ora({ text });
368
420
  //#endregion
369
- //#region src/utils/install-mcp.ts
370
- const SERVER_NAME = "react-grab-mcp";
371
- const PACKAGE_NAME = "@react-grab/mcp";
372
- const getXdgConfigHome = () => process$1.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
373
- const getBaseDir = () => {
374
- const homeDir = os.homedir();
375
- if (process$1.platform === "win32") return process$1.env.APPDATA || path.join(homeDir, "AppData", "Roaming");
376
- if (process$1.platform === "darwin") return path.join(homeDir, "Library", "Application Support");
377
- return getXdgConfigHome();
378
- };
379
- const getZedConfigPath = () => {
380
- if (process$1.platform === "win32") return path.join(getBaseDir(), "Zed", "settings.json");
381
- return path.join(os.homedir(), ".config", "zed", "settings.json");
382
- };
383
- const getOpenCodeConfigPath = () => {
384
- const configDir = path.join(getXdgConfigHome(), "opencode");
385
- const jsoncPath = path.join(configDir, "opencode.jsonc");
386
- const jsonPath = path.join(configDir, "opencode.json");
387
- if (fs.existsSync(jsoncPath)) return jsoncPath;
388
- if (fs.existsSync(jsonPath)) return jsonPath;
389
- return jsoncPath;
390
- };
391
- const getClients = () => {
392
- const homeDir = os.homedir();
393
- const baseDir = getBaseDir();
394
- const stdioConfig = {
395
- command: "npx",
396
- args: [
397
- "-y",
398
- PACKAGE_NAME,
399
- "--stdio"
400
- ]
401
- };
402
- return [
403
- {
404
- name: "Claude Code",
405
- configPath: path.join(homeDir, ".claude.json"),
406
- configKey: "mcpServers",
407
- format: "json",
408
- serverConfig: stdioConfig
409
- },
410
- {
411
- name: "Codex",
412
- configPath: path.join(process$1.env.CODEX_HOME || path.join(homeDir, ".codex"), "config.toml"),
413
- configKey: "mcp_servers",
414
- format: "toml",
415
- serverConfig: stdioConfig
416
- },
417
- {
418
- name: "Cursor",
419
- configPath: path.join(homeDir, ".cursor", "mcp.json"),
420
- configKey: "mcpServers",
421
- format: "json",
422
- serverConfig: stdioConfig
423
- },
424
- {
425
- name: "OpenCode",
426
- configPath: getOpenCodeConfigPath(),
427
- configKey: "mcp",
428
- format: "json",
429
- serverConfig: {
430
- type: "local",
431
- command: [
432
- "npx",
433
- "-y",
434
- PACKAGE_NAME,
435
- "--stdio"
436
- ]
437
- }
438
- },
439
- {
440
- name: "VS Code",
441
- configPath: path.join(baseDir, "Code", "User", "mcp.json"),
442
- configKey: "servers",
443
- format: "json",
444
- serverConfig: {
445
- type: "stdio",
446
- ...stdioConfig
447
- }
448
- },
449
- {
450
- name: "Amp",
451
- configPath: path.join(homeDir, ".config", "amp", "settings.json"),
452
- configKey: "amp.mcpServers",
453
- format: "json",
454
- serverConfig: stdioConfig
455
- },
456
- {
457
- name: "Droid",
458
- configPath: path.join(homeDir, ".factory", "mcp.json"),
459
- configKey: "mcpServers",
460
- format: "json",
461
- serverConfig: {
462
- type: "stdio",
463
- ...stdioConfig
464
- }
465
- },
466
- {
467
- name: "Windsurf",
468
- configPath: path.join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
469
- configKey: "mcpServers",
470
- format: "json",
471
- serverConfig: stdioConfig
472
- },
473
- {
474
- name: "Zed",
475
- configPath: getZedConfigPath(),
476
- configKey: "context_servers",
477
- format: "json",
478
- serverConfig: {
479
- source: "custom",
480
- ...stdioConfig,
481
- env: {}
482
- }
483
- }
484
- ];
485
- };
486
- const ensureDirectory = (filePath) => {
487
- const directory = path.dirname(filePath);
488
- if (!fs.existsSync(directory)) fs.mkdirSync(directory, { recursive: true });
489
- };
490
- const JSONC_FORMAT_OPTIONS = {
491
- tabSize: 2,
492
- insertSpaces: true
493
- };
494
- const upsertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
495
- const edits = jsonc.modify(content, [configKey, serverName], serverConfig, { formattingOptions: JSONC_FORMAT_OPTIONS });
496
- fs.writeFileSync(filePath, jsonc.applyEdits(content, edits));
497
- };
498
- const installJsonClient = (client) => {
499
- ensureDirectory(client.configPath);
500
- const content = fs.existsSync(client.configPath) ? fs.readFileSync(client.configPath, "utf8") : "{}";
501
- upsertIntoJsonc(client.configPath, content, client.configKey, SERVER_NAME, client.serverConfig);
502
- };
503
- const installTomlClient = (client) => {
504
- ensureDirectory(client.configPath);
505
- const existingConfig = fs.existsSync(client.configPath) ? TOML.parse(fs.readFileSync(client.configPath, "utf8")) : {};
506
- const serverSection = existingConfig[client.configKey] ?? {};
507
- serverSection[SERVER_NAME] = client.serverConfig;
508
- existingConfig[client.configKey] = serverSection;
509
- fs.writeFileSync(client.configPath, TOML.stringify(existingConfig));
510
- };
511
- const getMcpClientNames = () => getClients().map((client) => client.name);
512
- const installMcpServers = (selectedClients) => {
513
- const allClients = getClients();
514
- const clients = selectedClients ? allClients.filter((client) => selectedClients.includes(client.name)) : allClients;
515
- const results = [];
516
- const installSpinner = spinner("Installing MCP server.").start();
517
- for (const client of clients) try {
518
- if (client.format === "toml") installTomlClient(client);
519
- else installJsonClient(client);
520
- results.push({
521
- client: client.name,
522
- configPath: client.configPath,
523
- success: true
524
- });
525
- } catch (error) {
526
- const message = error instanceof Error ? error.message : String(error);
527
- results.push({
528
- client: client.name,
529
- configPath: client.configPath,
530
- success: false,
531
- error: message
421
+ //#region src/utils/install-skill.ts
422
+ const SKILL_NAME = "react-grab";
423
+ const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.url));
424
+ const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
425
+ const installedSkillDir = (agent) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(true) : getSkillAgentDir(agent, { global: true }), SKILL_NAME);
426
+ const promptSkillInstall = async ({ yes = false } = {}) => {
427
+ const detectedAgents = await detectAvailableAgents();
428
+ if (detectedAgents.length === 0) {
429
+ logger.warn("No supported agents detected.");
430
+ return false;
431
+ }
432
+ let selectedAgents = detectedAgents;
433
+ if (!yes) {
434
+ const { agents } = await prompts({
435
+ type: "multiselect",
436
+ name: "agents",
437
+ message: "Install the React Grab skill for:",
438
+ choices: detectedAgents.map((agent) => ({
439
+ title: agentLabel(agent),
440
+ value: agent,
441
+ selected: true
442
+ })),
443
+ instructions: false,
444
+ min: 1
532
445
  });
446
+ selectedAgents = agents ?? [];
447
+ if (selectedAgents.length === 0) return false;
533
448
  }
534
- const successCount = results.filter((result) => result.success).length;
535
- if (successCount < results.length) installSpinner.warn(`Installed to ${successCount}/${results.length} agents.`);
536
- else installSpinner.succeed(`Installed to ${successCount} agents.`);
537
- for (const result of results) if (result.success) logger.log(` ${highlighter.success("✓")} ${result.client} ${highlighter.dim("→")} ${highlighter.dim(result.configPath)}`);
538
- else logger.log(` ${highlighter.error("✗")} ${result.client} ${highlighter.dim("→")} ${result.error}`);
539
- return results;
449
+ const installSpinner = spinner("Installing React Grab skill.").start();
450
+ const { installed, failed } = await add({
451
+ source: SKILL_SOURCE,
452
+ agents: selectedAgents,
453
+ global: true,
454
+ mode: "copy"
455
+ });
456
+ if (installed.length === 0) {
457
+ installSpinner.fail("Failed to install React Grab skill.");
458
+ return false;
459
+ }
460
+ installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`);
461
+ for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
462
+ return true;
540
463
  };
541
- const promptMcpInstall = async () => {
542
- const { selectedAgents } = await prompts({
543
- type: "multiselect",
544
- name: "selectedAgents",
545
- message: "Select agents to install MCP server for:",
546
- choices: getMcpClientNames().map((name) => ({
547
- title: name,
548
- value: name,
549
- selected: true
550
- }))
464
+ const removeSkill = async () => {
465
+ const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => existsSync(installedSkillDir(agent)));
466
+ for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) rmSync(skillDir, {
467
+ recursive: true,
468
+ force: true
551
469
  });
552
- if (selectedAgents === void 0 || selectedAgents.length === 0) return false;
553
- logger.break();
554
- return installMcpServers(selectedAgents).some((result) => result.success);
470
+ for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
471
+ return agentsWithSkill.length;
555
472
  };
556
473
  //#endregion
557
474
  //#region src/commands/add.ts
558
- const VERSION$5 = "0.1.37";
559
- const add = new 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) => {
475
+ const VERSION$5 = "0.1.39";
476
+ const add$1 = new 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) => {
560
477
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
561
478
  console.log();
562
479
  try {
563
- const cwd = opts.cwd;
564
480
  const isNonInteractive = detectNonInteractive(opts.yes);
565
481
  const preflightSpinner = spinner("Preflight checks.").start();
566
- if (!(await detectProject(cwd)).hasReactGrab) {
482
+ if (!(await detectProject(opts.cwd)).hasReactGrab) {
567
483
  preflightSpinner.fail("React Grab is not installed.");
568
484
  logger.break();
569
485
  logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
@@ -571,39 +487,12 @@ const add = new Command().name("add").alias("install").description("connect Reac
571
487
  process.exit(1);
572
488
  }
573
489
  preflightSpinner.succeed();
574
- if (agentArg && agentArg !== "mcp") {
575
- logger.break();
576
- logger.warn(`Legacy agent packages are deprecated. Use ${highlighter.info("mcp")} instead.`);
577
- logger.log(`Run ${highlighter.info("grab add mcp")} to install the MCP server.`);
490
+ logger.break();
491
+ if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
578
492
  logger.break();
579
493
  process.exit(1);
580
494
  }
581
- if (agentArg === "mcp" || isNonInteractive) {
582
- if (isNonInteractive) {
583
- if (!installMcpServers().some((result) => result.success)) {
584
- logger.break();
585
- logger.error("Failed to install MCP server.");
586
- logger.break();
587
- process.exit(1);
588
- }
589
- } else if (!await promptMcpInstall()) {
590
- logger.break();
591
- process.exit(0);
592
- }
593
- logger.break();
594
- logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
595
- logger.log("Restart your agents to activate.");
596
- logger.break();
597
- } else {
598
- if (!await promptMcpInstall()) {
599
- logger.break();
600
- process.exit(0);
601
- }
602
- logger.break();
603
- logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
604
- logger.log("Restart your agents to activate.");
605
- logger.break();
606
- }
495
+ logger.break();
607
496
  } catch (error) {
608
497
  handleError(error);
609
498
  }
@@ -986,6 +875,15 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
986
875
  newContent
987
876
  };
988
877
  };
878
+ const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
879
+ switch (framework) {
880
+ case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
881
+ case "vite":
882
+ case "webpack": return findEntryFile(projectRoot) !== null;
883
+ case "tanstack": return findTanStackRootFile(projectRoot) !== null;
884
+ default: return false;
885
+ }
886
+ };
989
887
  const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
990
888
  switch (framework) {
991
889
  case "next":
@@ -1185,7 +1083,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1185
1083
  };
1186
1084
  //#endregion
1187
1085
  //#region src/commands/configure.ts
1188
- const VERSION$4 = "0.1.37";
1086
+ const VERSION$4 = "0.1.39";
1189
1087
  const isMac = process.platform === "darwin";
1190
1088
  const META_LABEL = isMac ? "Cmd" : "Win";
1191
1089
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1804,7 +1702,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
1804
1702
  };
1805
1703
  //#endregion
1806
1704
  //#region src/commands/init.ts
1807
- const VERSION$3 = "0.1.37";
1705
+ const VERSION$3 = "0.1.39";
1808
1706
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1809
1707
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1810
1708
  const reportToCli = (type, config, error) => {
@@ -1862,6 +1760,15 @@ const printSubprojects = (searchRoot, sortedProjects) => {
1862
1760
  logger.log(` ${highlighter.dim("$")} npx grab@latest init -c ${relative(searchRoot, sortedProjects[0].path)}`);
1863
1761
  logger.break();
1864
1762
  };
1763
+ const SUPPORTED_FRAMEWORKS_LINE = "React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.";
1764
+ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks = false } = {}) => {
1765
+ failingSpinner.fail(message);
1766
+ logger.break();
1767
+ if (listSupportedFrameworks) logger.log(SUPPORTED_FRAMEWORKS_LINE);
1768
+ logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
1769
+ logger.break();
1770
+ process.exit(1);
1771
+ };
1865
1772
  const init = new 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) => {
1866
1773
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`);
1867
1774
  console.log();
@@ -2020,25 +1927,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
2020
1927
  }
2021
1928
  }
2022
1929
  logger.break();
2023
- const { wantAddMcp } = await prompts({
2024
- type: "confirm",
2025
- name: "wantAddMcp",
2026
- message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
2027
- initial: false
2028
- });
2029
- if (wantAddMcp === void 0) {
2030
- logger.break();
2031
- process.exit(1);
2032
- }
2033
- if (wantAddMcp) {
2034
- if (!await promptMcpInstall()) {
2035
- logger.break();
2036
- process.exit(0);
2037
- }
2038
- logger.break();
2039
- logger.success("MCP server has been configured.");
2040
- logger.log("Restart your agents to activate.");
2041
- }
1930
+ await promptSkillInstall({ yes: isNonInteractive });
2042
1931
  logger.break();
2043
1932
  process.exit(0);
2044
1933
  }
@@ -2053,7 +1942,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
2053
1942
  logger.break();
2054
1943
  process.exit(1);
2055
1944
  }
2056
- if (projectInfo.framework === "unknown") {
1945
+ if (projectInfo.framework === "unknown" || projectInfo.isMonorepo && !hasFrameworkEntryPoint(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType)) {
2057
1946
  let searchRoot = cwd;
2058
1947
  let reactProjects = findReactProjects(searchRoot);
2059
1948
  if (reactProjects.length === 0 && cwd !== process.cwd()) {
@@ -2090,45 +1979,21 @@ const init = new Command().name("init").alias("setup").description("initialize R
2090
1979
  process.chdir(selectedProject);
2091
1980
  const newProjectInfo = await detectProject(selectedProject);
2092
1981
  Object.assign(projectInfo, newProjectInfo);
2093
- spinner("Verifying framework.").start().succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
2094
- } else {
2095
- frameworkSpinner.fail("Could not detect a supported framework.");
2096
- logger.break();
2097
- logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
2098
- logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
2099
- logger.break();
2100
- process.exit(1);
2101
- }
1982
+ const newFrameworkSpinner = spinner("Verifying framework.").start();
1983
+ if (newProjectInfo.framework === "unknown") failWithManualSetup(newFrameworkSpinner, "Could not detect a supported framework in this project.", { listSupportedFrameworks: true });
1984
+ newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
1985
+ } else if (projectInfo.framework !== "unknown") failWithManualSetup(frameworkSpinner, `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}, but could not find an entry file.`);
1986
+ else failWithManualSetup(frameworkSpinner, "Could not detect a supported framework.", { listSupportedFrameworks: true });
2102
1987
  } else frameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`);
2103
1988
  if (projectInfo.framework === "next") spinner("Detecting router type.").start().succeed(`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`);
2104
1989
  spinner("Detecting package manager.").start().succeed(`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`);
2105
1990
  const finalFramework = projectInfo.framework;
2106
1991
  const finalPackageManager = projectInfo.packageManager;
2107
1992
  const finalNextRouterType = projectInfo.nextRouterType;
2108
- let didInstallMcp = false;
1993
+ let didInstallSkill = false;
2109
1994
  if (!isNonInteractive) {
2110
1995
  logger.break();
2111
- const { wantAddMcp } = await prompts({
2112
- type: "confirm",
2113
- name: "wantAddMcp",
2114
- message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
2115
- initial: false
2116
- });
2117
- if (wantAddMcp === void 0) {
2118
- logger.break();
2119
- process.exit(1);
2120
- }
2121
- if (wantAddMcp) {
2122
- didInstallMcp = Boolean(await promptMcpInstall());
2123
- if (!didInstallMcp) {
2124
- logger.break();
2125
- process.exit(0);
2126
- }
2127
- logger.break();
2128
- logger.success("MCP server has been configured.");
2129
- logger.log("Continuing with React Grab installation...");
2130
- logger.break();
2131
- }
1996
+ didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
2132
1997
  }
2133
1998
  const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
2134
1999
  if (!result.success) {
@@ -2172,7 +2037,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
2172
2037
  framework: finalFramework,
2173
2038
  packageManager: finalPackageManager,
2174
2039
  router: finalNextRouterType,
2175
- agent: didInstallMcp ? "mcp" : void 0,
2040
+ agent: didInstallSkill ? "skill" : void 0,
2176
2041
  isMonorepo: projectInfo.isMonorepo
2177
2042
  });
2178
2043
  } catch (error) {
@@ -2182,29 +2047,16 @@ const init = new Command().name("init").alias("setup").description("initialize R
2182
2047
  });
2183
2048
  //#endregion
2184
2049
  //#region src/commands/remove.ts
2185
- const VERSION$2 = "0.1.37";
2186
- const remove = new 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) => {
2050
+ const VERSION$2 = "0.1.39";
2051
+ const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
2187
2052
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
2188
2053
  console.log();
2189
2054
  try {
2190
- const cwd = opts.cwd;
2191
- const preflightSpinner = spinner("Preflight checks.").start();
2192
- if (!(await detectProject(cwd)).hasReactGrab) {
2193
- preflightSpinner.fail("React Grab is not installed.");
2194
- logger.break();
2195
- logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
2196
- logger.break();
2197
- process.exit(1);
2198
- }
2199
- preflightSpinner.succeed();
2200
- if (agentArg && agentArg !== "mcp") {
2201
- logger.break();
2202
- logger.warn(`Legacy agent packages are deprecated. Uninstall ${highlighter.info(`@react-grab/${agentArg}`)} manually with your package manager.`);
2203
- logger.break();
2204
- process.exit(0);
2205
- }
2206
2055
  logger.break();
2207
- logger.warn("To remove the MCP server, delete the react-grab-mcp entry from your agent's MCP config file.");
2056
+ const removedCount = await removeSkill();
2057
+ logger.break();
2058
+ if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
2059
+ else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
2208
2060
  logger.break();
2209
2061
  } catch (error) {
2210
2062
  handleError(error);
@@ -2212,7 +2064,7 @@ const remove = new Command().name("remove").description("disconnect React Grab f
2212
2064
  });
2213
2065
  //#endregion
2214
2066
  //#region src/commands/upgrade.ts
2215
- const VERSION$1 = "0.1.37";
2067
+ const VERSION$1 = "0.1.39";
2216
2068
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2217
2069
  const fetchLatestVersion = async () => {
2218
2070
  try {
@@ -2286,8 +2138,368 @@ const upgrade = new Command().name("upgrade").alias("update").description("upgra
2286
2138
  }
2287
2139
  });
2288
2140
  //#endregion
2141
+ //#region src/utils/clipboard.ts
2142
+ const READ_TIMEOUT_MS = 2500;
2143
+ const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
2144
+ const ID_RADIX = 36;
2145
+ const HASH_LENGTH = 12;
2146
+ const PICKLE_HEADER_BYTES = 4;
2147
+ const PICKLE_ALIGN_BYTES = 4;
2148
+ const SIGNATURE_SCAN_CHARS = 32 * 1024;
2149
+ const GRAB_MIME = "application/x-react-grab";
2150
+ const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
2151
+ const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
2152
+ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
2153
+ const shortHash = (text) => createHash("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
2154
+ const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
2155
+ const parseChromiumPickle = (buffer) => {
2156
+ const formats = {};
2157
+ if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats;
2158
+ let offset = PICKLE_HEADER_BYTES;
2159
+ const pairCount = buffer.readUInt32LE(offset);
2160
+ offset += 4;
2161
+ for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
2162
+ if (offset + 4 > buffer.length) break;
2163
+ const formatCodeUnits = buffer.readUInt32LE(offset);
2164
+ offset += 4;
2165
+ if (offset + formatCodeUnits * 2 > buffer.length) break;
2166
+ const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2);
2167
+ offset = alignUp(offset + formatCodeUnits * 2);
2168
+ if (offset + 4 > buffer.length) break;
2169
+ const dataCodeUnits = buffer.readUInt32LE(offset);
2170
+ offset += 4;
2171
+ if (offset + dataCodeUnits * 2 > buffer.length) break;
2172
+ const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2);
2173
+ offset = alignUp(offset + dataCodeUnits * 2);
2174
+ formats[format] = value;
2175
+ }
2176
+ return formats;
2177
+ };
2178
+ const extractGrab = (raw) => {
2179
+ if (raw.grab) return raw.grab;
2180
+ if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME];
2181
+ };
2182
+ const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text);
2183
+ const extractPrompt = (record) => {
2184
+ const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean);
2185
+ if (comments.length > 0) return comments.join("\n");
2186
+ const lines = (typeof record.content === "string" ? record.content : "").split("\n");
2187
+ const firstReferenceLine = lines.findIndex((line) => line.startsWith("["));
2188
+ if (firstReferenceLine <= 0) return void 0;
2189
+ return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0;
2190
+ };
2191
+ const hasCommand = (name) => {
2192
+ return spawnSync(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0;
2193
+ };
2194
+ const runText = (command, args) => {
2195
+ const output = spawnSync(command, args, {
2196
+ encoding: "utf8",
2197
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2198
+ timeout: READ_TIMEOUT_MS
2199
+ });
2200
+ return output.status === 0 ? output.stdout : null;
2201
+ };
2202
+ const runBuffer = (command, args) => {
2203
+ const output = spawnSync(command, args, {
2204
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2205
+ timeout: READ_TIMEOUT_MS
2206
+ });
2207
+ return output.status === 0 && output.stdout?.length ? output.stdout : null;
2208
+ };
2209
+ const runJson = (command, args) => {
2210
+ const output = spawnSync(command, args, {
2211
+ encoding: "utf8",
2212
+ maxBuffer: MAX_CLIPBOARD_BYTES,
2213
+ timeout: READ_TIMEOUT_MS
2214
+ });
2215
+ if (output.status !== 0 || !output.stdout) return null;
2216
+ try {
2217
+ return JSON.parse(output.stdout);
2218
+ } catch {
2219
+ return null;
2220
+ }
2221
+ };
2222
+ const compileSwiftReader = (readersDir, workDir) => {
2223
+ if (!hasCommand("swiftc")) return null;
2224
+ const source = path.join(readersDir, "read-clipboard.swift");
2225
+ if (!fs.existsSync(source)) return null;
2226
+ const binary = path.join(workDir, "pbread");
2227
+ if ((!fs.existsSync(binary) || fs.statSync(source).mtimeMs > fs.statSync(binary).mtimeMs) && spawnSync("swiftc", [
2228
+ "-O",
2229
+ source,
2230
+ "-o",
2231
+ binary
2232
+ ]).status !== 0) return null;
2233
+ return binary;
2234
+ };
2235
+ const createDarwinReader = (options) => {
2236
+ const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
2237
+ if (binary) return {
2238
+ mode: "darwin-native",
2239
+ read: () => {
2240
+ const raw = runJson(binary, []);
2241
+ if (!raw) return null;
2242
+ return {
2243
+ changeCount: raw.changeCount ?? null,
2244
+ text: raw.text,
2245
+ grab: extractGrab(raw)
2246
+ };
2247
+ }
2248
+ };
2249
+ if (!hasCommand("pbpaste")) return null;
2250
+ return {
2251
+ mode: "darwin-text",
2252
+ read: () => {
2253
+ const text = runText("pbpaste", []);
2254
+ return text == null ? null : {
2255
+ changeCount: null,
2256
+ text,
2257
+ grab: void 0
2258
+ };
2259
+ }
2260
+ };
2261
+ };
2262
+ const detectLinuxTool = () => {
2263
+ if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return {
2264
+ name: "wl-paste",
2265
+ readText: () => runText("wl-paste", [
2266
+ "-n",
2267
+ "-t",
2268
+ "text/plain"
2269
+ ]) ?? runText("wl-paste", ["-n"]),
2270
+ readCustom: () => runBuffer("wl-paste", [
2271
+ "-n",
2272
+ "-t",
2273
+ CHROMIUM_CUSTOM_FORMAT
2274
+ ])
2275
+ };
2276
+ if (hasCommand("xclip")) return {
2277
+ name: "xclip",
2278
+ readText: () => runText("xclip", [
2279
+ "-selection",
2280
+ "clipboard",
2281
+ "-o"
2282
+ ]),
2283
+ readCustom: () => runBuffer("xclip", [
2284
+ "-selection",
2285
+ "clipboard",
2286
+ "-t",
2287
+ CHROMIUM_CUSTOM_FORMAT,
2288
+ "-o"
2289
+ ])
2290
+ };
2291
+ if (hasCommand("xsel")) return {
2292
+ name: "xsel",
2293
+ readText: () => runText("xsel", ["--clipboard", "--output"]),
2294
+ readCustom: null
2295
+ };
2296
+ return null;
2297
+ };
2298
+ const createLinuxReader = (options) => {
2299
+ const tool = detectLinuxTool();
2300
+ if (!tool) return null;
2301
+ const useCustom = !options.textOnly && Boolean(tool.readCustom);
2302
+ return {
2303
+ mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`,
2304
+ read: () => {
2305
+ const text = tool.readText();
2306
+ if (text == null) return null;
2307
+ return {
2308
+ changeCount: null,
2309
+ text,
2310
+ grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0
2311
+ };
2312
+ }
2313
+ };
2314
+ };
2315
+ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null;
2316
+ const createWindowsReader = (options) => {
2317
+ const shell = detectPowershell();
2318
+ if (!shell) return null;
2319
+ const scriptPath = path.join(options.readersDir, "read-clipboard.ps1");
2320
+ if (options.textOnly || !fs.existsSync(scriptPath)) return {
2321
+ mode: "win-text",
2322
+ read: () => {
2323
+ const text = runText(shell, [
2324
+ "-NoProfile",
2325
+ "-Command",
2326
+ "Get-Clipboard -Raw"
2327
+ ]);
2328
+ return text == null ? null : {
2329
+ changeCount: null,
2330
+ text,
2331
+ grab: void 0
2332
+ };
2333
+ }
2334
+ };
2335
+ const args = [
2336
+ "-NoProfile",
2337
+ "-ExecutionPolicy",
2338
+ "Bypass",
2339
+ "-File",
2340
+ scriptPath
2341
+ ];
2342
+ spawnSync(shell, args, {
2343
+ stdio: "ignore",
2344
+ maxBuffer: MAX_CLIPBOARD_BYTES
2345
+ });
2346
+ return {
2347
+ mode: "win-native",
2348
+ read: () => {
2349
+ const raw = runJson(shell, args);
2350
+ if (!raw) return null;
2351
+ return {
2352
+ changeCount: raw.changeCount ?? null,
2353
+ text: raw.text ?? void 0,
2354
+ grab: extractGrab(raw)
2355
+ };
2356
+ }
2357
+ };
2358
+ };
2359
+ const createReader = (options) => {
2360
+ if (process.platform === "darwin") return createDarwinReader(options);
2361
+ if (process.platform === "linux") return createLinuxReader(options);
2362
+ if (process.platform === "win32") return createWindowsReader(options);
2363
+ return null;
2364
+ };
2365
+ const ensureSafeDir = (dir) => {
2366
+ let stats;
2367
+ try {
2368
+ stats = fs.lstatSync(dir);
2369
+ } catch {
2370
+ return;
2371
+ }
2372
+ if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`);
2373
+ if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`);
2374
+ };
2375
+ const prepareWorkDir = (dir) => {
2376
+ ensureSafeDir(dir);
2377
+ fs.mkdirSync(dir, {
2378
+ recursive: true,
2379
+ mode: 448
2380
+ });
2381
+ const gitignore = path.join(dir, ".gitignore");
2382
+ if (!fs.existsSync(gitignore)) fs.writeFileSync(gitignore, "*\n");
2383
+ };
2384
+ const watchForNextGrab = async (options) => {
2385
+ const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
2386
+ const logPath = path.join(dir, "history.jsonl");
2387
+ const { read } = reader;
2388
+ let lastChangeCount = null;
2389
+ let lastTimestamp = 0;
2390
+ let lastTextHash = "";
2391
+ let lastErrorMessage = "";
2392
+ let sequence = 0;
2393
+ const initial = read();
2394
+ if (initial && !replayLast) {
2395
+ lastChangeCount = initial.changeCount;
2396
+ if (initial.text) lastTextHash = shortHash(initial.text);
2397
+ if (initial.grab) try {
2398
+ lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
2399
+ } catch {}
2400
+ }
2401
+ while (!signal?.aborted) {
2402
+ await sleep(intervalMs);
2403
+ if (signal?.aborted) return null;
2404
+ try {
2405
+ const snapshot = read();
2406
+ if (!snapshot) continue;
2407
+ if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue;
2408
+ const textHash = snapshot.text ? shortHash(snapshot.text) : "";
2409
+ const didTextChange = textHash !== lastTextHash;
2410
+ let record = null;
2411
+ let nextTimestamp = lastTimestamp;
2412
+ if (snapshot.grab) {
2413
+ let parsed = null;
2414
+ try {
2415
+ parsed = JSON.parse(snapshot.grab);
2416
+ } catch {}
2417
+ if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) {
2418
+ nextTimestamp = parsed.timestamp;
2419
+ record = {
2420
+ source: "custom",
2421
+ timestamp: parsed.timestamp,
2422
+ version: typeof parsed.version === "string" ? parsed.version : void 0,
2423
+ content: typeof parsed.content === "string" ? parsed.content : "",
2424
+ entries: Array.isArray(parsed.entries) ? parsed.entries : []
2425
+ };
2426
+ }
2427
+ } else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = {
2428
+ source: "text",
2429
+ timestamp: Date.now(),
2430
+ content: snapshot.text,
2431
+ entries: []
2432
+ };
2433
+ if (!record) {
2434
+ lastChangeCount = snapshot.changeCount;
2435
+ lastTextHash = textHash;
2436
+ continue;
2437
+ }
2438
+ const prompt = extractPrompt(record);
2439
+ if (prompt) record.prompt = prompt;
2440
+ const captured = {
2441
+ id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`,
2442
+ receivedAt: Date.now(),
2443
+ ...record
2444
+ };
2445
+ fs.appendFileSync(logPath, `${JSON.stringify(captured)}\n`);
2446
+ lastChangeCount = snapshot.changeCount;
2447
+ lastTextHash = textHash;
2448
+ lastTimestamp = nextTimestamp;
2449
+ return captured;
2450
+ } catch (error) {
2451
+ const message = String(error?.message ?? error);
2452
+ if (message !== lastErrorMessage) {
2453
+ lastErrorMessage = message;
2454
+ onWarn?.(message);
2455
+ }
2456
+ }
2457
+ }
2458
+ return null;
2459
+ };
2460
+ //#endregion
2461
+ //#region src/commands/watch.ts
2462
+ const DEFAULT_INTERVAL_MS = 800;
2463
+ const DEFAULT_DIR = ".react-grab";
2464
+ const readersDir = () => path.dirname(fileURLToPath(import.meta.url));
2465
+ const watch = new 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) => {
2466
+ const dir = path.resolve(options.dir);
2467
+ const intervalMs = Number(options.interval);
2468
+ const textOnly = Boolean(options.textOnly);
2469
+ try {
2470
+ prepareWorkDir(dir);
2471
+ } catch (error) {
2472
+ process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2473
+ process.exit(1);
2474
+ }
2475
+ const reader = createReader({
2476
+ textOnly,
2477
+ readersDir: readersDir(),
2478
+ workDir: dir
2479
+ });
2480
+ if (!reader) {
2481
+ 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");
2482
+ process.exit(1);
2483
+ }
2484
+ process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${path.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
2485
+ watchForNextGrab({
2486
+ reader,
2487
+ dir,
2488
+ intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
2489
+ replayLast: Boolean(options.replayLast),
2490
+ onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
2491
+ }).then((grab) => {
2492
+ if (!grab) process.exit(0);
2493
+ process.stdout.write(`${JSON.stringify(grab)}\n`);
2494
+ process.exit(0);
2495
+ }).catch((error) => {
2496
+ process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2497
+ process.exit(1);
2498
+ });
2499
+ });
2500
+ //#endregion
2289
2501
  //#region src/cli.ts
2290
- const VERSION = "0.1.37";
2502
+ const VERSION = "0.1.39";
2291
2503
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2292
2504
  process.on("SIGINT", () => process.exit(0));
2293
2505
  process.on("SIGTERM", () => process.exit(0));
@@ -2296,10 +2508,11 @@ try {
2296
2508
  } catch {}
2297
2509
  const program = new Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number");
2298
2510
  program.addCommand(init);
2299
- program.addCommand(add);
2511
+ program.addCommand(add$1);
2300
2512
  program.addCommand(remove);
2301
2513
  program.addCommand(configure);
2302
2514
  program.addCommand(upgrade);
2515
+ program.addCommand(watch);
2303
2516
  const main = async () => {
2304
2517
  await program.parseAsync();
2305
2518
  };