@eyeglass/cli 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -8,20 +8,38 @@ CLI for initializing [Eyeglass](https://github.com/donutboyband/eyeglass) in you
8
8
  npx @eyeglass/cli init
9
9
  ```
10
10
 
11
- This single command will:
12
-
13
- 1. **Install** `@eyeglass/inspector` as a dev dependency
14
- 2. **Create** `.claude/settings.json` with MCP server configuration
15
- 3. **Configure** your bundler (Vite, Next.js, CRA, or Remix)
11
+ This will interactively prompt you to select which AI coding agents to configure:
12
+ - **Claude Code** - stdio MCP (`.claude/settings.json`)
13
+ - **GitHub Copilot CLI** - local MCP (`.copilot/mcp-config.json`)
14
+ - **OpenAI Codex CLI** - HTTP API (`.codex/eyeglass.md`)
16
15
 
17
16
  ## Options
18
17
 
19
18
  ```bash
19
+ # Interactive setup (prompts for agent selection)
20
+ npx @eyeglass/cli init
21
+
22
+ # Configure specific agents
23
+ npx @eyeglass/cli init --claude # Claude Code only
24
+ npx @eyeglass/cli init --copilot # GitHub Copilot CLI only
25
+ npx @eyeglass/cli init --codex # OpenAI Codex only
26
+ npx @eyeglass/cli init --claude --copilot # Multiple agents
27
+
28
+ # Other options
20
29
  npx @eyeglass/cli init --dry-run # Preview changes without making them
21
30
  npx @eyeglass/cli init --skip-install # Skip installing @eyeglass/inspector
22
31
  npx @eyeglass/cli help # Show help
23
32
  ```
24
33
 
34
+ ## What It Does
35
+
36
+ 1. **Installs** `@eyeglass/inspector` as a dev dependency
37
+ 2. **Creates** agent-specific config files:
38
+ - Claude: `.claude/settings.json` + `.claude/skills/eyeglass.md`
39
+ - Copilot CLI: `.copilot/mcp-config.json`
40
+ - Codex: `.codex/eyeglass.md` (HTTP API instructions)
41
+ 3. **Configures** your bundler (Vite, Next.js, CRA, or Remix)
42
+
25
43
  ## Supported Frameworks
26
44
 
27
45
  | Framework | Auto-Detection | Component Names | File Paths |
@@ -34,8 +52,11 @@ npx @eyeglass/cli help # Show help
34
52
  ## After Setup
35
53
 
36
54
  1. Start your dev server (`npm run dev`)
37
- 2. Run `claude` in your project directory
38
- 3. Tell Claude: `watch eyeglass` or `eg`
55
+ 2. Start the Eyeglass bridge: `npx eyeglass-bridge`
56
+ 3. Start your AI coding agent:
57
+ - **Claude Code**: Run `claude` and say "watch eyeglass" or "/eyeglass"
58
+ - **Copilot CLI**: Run `gh copilot` in your project directory
59
+ - **Codex**: See `.codex/eyeglass.md` for HTTP API usage
39
60
  4. Select elements in your browser and submit requests!
40
61
 
41
62
  See the [main repo](https://github.com/donutboyband/eyeglass) for full documentation.
package/dist/index.js CHANGED
@@ -1,10 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
+ import * as readline from 'readline';
4
5
  import { execFileSync } from 'child_process';
5
6
  // ============================================================================
6
7
  // Templates
7
8
  // ============================================================================
9
+ // ASCII art logo based on eyeglass.svg - glasses with cursor pointer in right lens
10
+ // SVG has: two round lenses, short vertical bridge, temples angling up-outward, pointer in right lens
11
+ const LOGO = `
12
+ \x1b[36m ╱ ╲
13
+ ╲ ╭───────╮ ╭───────╮ ╱
14
+ │ │ │ \x1b[34m▲\x1b[36m │
15
+ │ │ │ │ \x1b[34m█▸\x1b[36m │
16
+ ╰───────╯ ╰───────╯\x1b[0m
17
+ \x1b[1meyeglass\x1b[0m
18
+ `;
8
19
  const NEXT_CONFIG_ADDITION = `
9
20
  // Eyeglass configuration - Auto-generated by eyeglass init
10
21
  const withEyeglass = (nextConfig) => {
@@ -26,14 +37,28 @@ const withEyeglass = (nextConfig) => {
26
37
  };
27
38
  };
28
39
  `;
40
+ const MCP_STDIO_CONFIG = {
41
+ command: 'npx',
42
+ args: ['eyeglass-bridge'],
43
+ };
29
44
  const CLAUDE_CONFIG = {
45
+ mcpServers: {
46
+ eyeglass: MCP_STDIO_CONFIG,
47
+ },
48
+ };
49
+ // GitHub Copilot CLI agent config format (.copilot/mcp-config.json)
50
+ const COPILOT_MCP_CONFIG = {
30
51
  mcpServers: {
31
52
  eyeglass: {
53
+ type: 'local',
32
54
  command: 'npx',
33
55
  args: ['eyeglass-bridge'],
56
+ tools: ['*'],
34
57
  },
35
58
  },
36
59
  };
60
+ // Codex CLI uses HTTP endpoints, so we just provide instructions
61
+ const CODEX_BASE_URL = 'http://localhost:3300/api';
37
62
  const EYEGLASS_SKILL = `---
38
63
  name: eyeglass
39
64
  description: Listen for Eyeglass requests from the browser and make UI changes
@@ -120,6 +145,48 @@ function readFile(filePath) {
120
145
  function writeFile(filePath, content) {
121
146
  fs.writeFileSync(filePath, content, 'utf-8');
122
147
  }
148
+ async function promptAgentSelection() {
149
+ const agents = [
150
+ { key: '1', name: 'Claude Code', type: 'claude', selected: false },
151
+ { key: '2', name: 'GitHub Copilot CLI', type: 'copilot', selected: false },
152
+ { key: '3', name: 'OpenAI Codex CLI', type: 'codex', selected: false },
153
+ ];
154
+ const rl = readline.createInterface({
155
+ input: process.stdin,
156
+ output: process.stdout,
157
+ });
158
+ const question = (prompt) => {
159
+ return new Promise((resolve) => {
160
+ rl.question(prompt, resolve);
161
+ });
162
+ };
163
+ console.log('\n\x1b[1mSelect coding agents to configure:\x1b[0m');
164
+ console.log('\x1b[2m(Enter numbers separated by spaces, or "a" for all)\x1b[0m\n');
165
+ for (const agent of agents) {
166
+ console.log(` ${agent.key}) ${agent.name}`);
167
+ }
168
+ console.log('');
169
+ const answer = await question('\x1b[36m>\x1b[0m ');
170
+ rl.close();
171
+ const input = answer.trim().toLowerCase();
172
+ if (input === 'a' || input === 'all') {
173
+ return agents.map((a) => a.type);
174
+ }
175
+ const selected = [];
176
+ const parts = input.split(/[\s,]+/);
177
+ for (const part of parts) {
178
+ const agent = agents.find((a) => a.key === part);
179
+ if (agent && !selected.includes(agent.type)) {
180
+ selected.push(agent.type);
181
+ }
182
+ }
183
+ // Default to Claude if nothing selected
184
+ if (selected.length === 0) {
185
+ log('No selection made, defaulting to Claude Code', 'warn');
186
+ return ['claude'];
187
+ }
188
+ return selected;
189
+ }
123
190
  function detectPackageManager() {
124
191
  const cwd = process.cwd();
125
192
  if (fileExists(path.join(cwd, 'bun.lockb')))
@@ -200,6 +267,46 @@ function detectProject() {
200
267
  return { type: 'unknown', typescript: hasTs };
201
268
  }
202
269
  // ============================================================================
270
+ // HTML Entry Point Detection
271
+ // ============================================================================
272
+ /**
273
+ * Parses index.html to find the entry script path.
274
+ * Looks for <script type="module" src="..."> tags and returns the first match.
275
+ * Works for vanilla Vite, React, Vue, and other projects that use index.html.
276
+ */
277
+ function findEntryFromHtml(cwd) {
278
+ // Common locations for index.html
279
+ const htmlFiles = [
280
+ 'index.html',
281
+ 'public/index.html',
282
+ 'src/index.html',
283
+ ];
284
+ for (const htmlFile of htmlFiles) {
285
+ const htmlPath = path.join(cwd, htmlFile);
286
+ if (!fileExists(htmlPath))
287
+ continue;
288
+ const content = readFile(htmlPath);
289
+ // Match <script type="module" src="..."> - handles various quote styles and spacing
290
+ // This regex captures the src attribute value from module scripts
291
+ const moduleScriptRegex = /<script[^>]*type\s*=\s*["']module["'][^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
292
+ const altModuleScriptRegex = /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*type\s*=\s*["']module["'][^>]*>/gi;
293
+ let match = moduleScriptRegex.exec(content) || altModuleScriptRegex.exec(content);
294
+ if (match && match[1]) {
295
+ let scriptSrc = match[1];
296
+ // Remove leading slash if present (Vite uses /src/main.js format)
297
+ if (scriptSrc.startsWith('/')) {
298
+ scriptSrc = scriptSrc.slice(1);
299
+ }
300
+ // Resolve the path relative to the project root
301
+ const scriptPath = path.join(cwd, scriptSrc);
302
+ if (fileExists(scriptPath)) {
303
+ return scriptPath;
304
+ }
305
+ }
306
+ }
307
+ return null;
308
+ }
309
+ // ============================================================================
203
310
  // Config Modifiers
204
311
  // ============================================================================
205
312
  function setupEyeglassSkill(dryRun) {
@@ -268,6 +375,132 @@ function setupClaudeConfig(dryRun) {
268
375
  }
269
376
  return true;
270
377
  }
378
+ function setupCopilotConfig(dryRun) {
379
+ const cwd = process.cwd();
380
+ const copilotDir = path.join(cwd, '.copilot');
381
+ const configPath = path.join(copilotDir, 'mcp-config.json');
382
+ if (fileExists(configPath)) {
383
+ try {
384
+ const existing = JSON.parse(readFile(configPath));
385
+ if (existing.mcpServers?.eyeglass) {
386
+ log('Copilot CLI MCP config already exists', 'success');
387
+ return true;
388
+ }
389
+ // Merge with existing config
390
+ const merged = {
391
+ ...existing,
392
+ mcpServers: {
393
+ ...existing.mcpServers,
394
+ ...COPILOT_MCP_CONFIG.mcpServers,
395
+ },
396
+ };
397
+ if (dryRun) {
398
+ log('Would update .copilot/mcp-config.json with eyeglass MCP config');
399
+ }
400
+ else {
401
+ writeFile(configPath, JSON.stringify(merged, null, 2));
402
+ log('Added eyeglass to existing Copilot CLI MCP config', 'success');
403
+ }
404
+ return true;
405
+ }
406
+ catch {
407
+ log('Could not parse existing .copilot/mcp-config.json', 'warn');
408
+ return false;
409
+ }
410
+ }
411
+ if (dryRun) {
412
+ log(`Would create ${configPath}`);
413
+ }
414
+ else {
415
+ if (!fileExists(copilotDir)) {
416
+ fs.mkdirSync(copilotDir, { recursive: true });
417
+ }
418
+ writeFile(configPath, JSON.stringify(COPILOT_MCP_CONFIG, null, 2));
419
+ log('Created .copilot/mcp-config.json for GitHub Copilot CLI', 'success');
420
+ }
421
+ return true;
422
+ }
423
+ function setupCodexConfig(dryRun) {
424
+ const cwd = process.cwd();
425
+ const codexDir = path.join(cwd, '.codex');
426
+ const configPath = path.join(codexDir, 'eyeglass.md');
427
+ const codexInstructions = `# Eyeglass Integration for Codex CLI
428
+
429
+ Eyeglass provides HTTP endpoints for interacting with UI elements selected in the browser.
430
+
431
+ ## Base URL
432
+ \`${CODEX_BASE_URL}\`
433
+
434
+ ## Endpoints
435
+
436
+ ### Wait for Request (blocking)
437
+ \`\`\`
438
+ GET /api/wait?timeout=300000
439
+ \`\`\`
440
+ Blocks until user selects an element. Returns markdown with element context.
441
+
442
+ ### Get Current Focus
443
+ \`\`\`
444
+ GET /api/focus
445
+ \`\`\`
446
+ Returns the currently focused element as markdown.
447
+
448
+ ### Update Status
449
+ \`\`\`
450
+ POST /api/status
451
+ Content-Type: application/json
452
+
453
+ {"status": "fixing", "message": "Working on it..."}
454
+ \`\`\`
455
+ Status values: \`idle\`, \`pending\`, \`fixing\`, \`success\`, \`failed\`
456
+
457
+ ### Report Action
458
+ \`\`\`
459
+ POST /api/action
460
+ Content-Type: application/json
461
+
462
+ {"action": "reading", "target": "src/components/Button.tsx"}
463
+ \`\`\`
464
+ Actions: \`reading\`, \`writing\`, \`searching\`, \`thinking\`
465
+
466
+ ### Send Thought
467
+ \`\`\`
468
+ POST /api/thought
469
+ Content-Type: application/json
470
+
471
+ {"content": "I think we should use a CSS variable for this color..."}
472
+ \`\`\`
473
+
474
+ ### Get History
475
+ \`\`\`
476
+ GET /api/history
477
+ \`\`\`
478
+ Returns up to 5 previous focus requests.
479
+
480
+ ## Workflow
481
+
482
+ 1. Start the bridge: \`npx eyeglass-bridge\`
483
+ 2. Call \`GET /api/wait\` to block until user selects an element
484
+ 3. Make your changes based on the returned context
485
+ 4. Call \`POST /api/status\` with \`{"status": "success", "message": "Done!"}\`
486
+ 5. Loop back to step 2
487
+ `;
488
+ if (fileExists(configPath)) {
489
+ log('Codex eyeglass instructions already exist', 'success');
490
+ return true;
491
+ }
492
+ if (dryRun) {
493
+ log(`Would create ${configPath}`);
494
+ }
495
+ else {
496
+ if (!fileExists(codexDir)) {
497
+ fs.mkdirSync(codexDir, { recursive: true });
498
+ }
499
+ writeFile(configPath, codexInstructions);
500
+ log('Created .codex/eyeglass.md with HTTP API instructions', 'success');
501
+ }
502
+ return true;
503
+ }
271
504
  function setupVite(configFile, dryRun) {
272
505
  const cwd = process.cwd();
273
506
  // Find entry file (main.tsx, main.ts, main.jsx, main.js, src/main.*, etc.)
@@ -289,8 +522,16 @@ function setupVite(configFile, dryRun) {
289
522
  break;
290
523
  }
291
524
  }
525
+ // Fallback: parse index.html to find the actual entry script
526
+ if (!entryFile) {
527
+ entryFile = findEntryFromHtml(cwd);
528
+ if (entryFile) {
529
+ log(`Found entry file from index.html: ${path.relative(cwd, entryFile)}`, 'info');
530
+ }
531
+ }
292
532
  if (!entryFile) {
293
533
  log('Could not find entry file - please import @eyeglass/inspector manually in your main file', 'warn');
534
+ log('Tip: Check your index.html for the entry script path', 'info');
294
535
  return false;
295
536
  }
296
537
  const content = readFile(entryFile);
@@ -380,8 +621,16 @@ function setupCRA(entryFile, dryRun) {
380
621
  }
381
622
  }
382
623
  }
624
+ // Fallback: parse index.html to find the actual entry script
625
+ if (!entryFile) {
626
+ entryFile = findEntryFromHtml(cwd) ?? undefined;
627
+ if (entryFile) {
628
+ log(`Found entry file from index.html: ${path.relative(cwd, entryFile)}`, 'info');
629
+ }
630
+ }
383
631
  if (!entryFile || !fileExists(entryFile)) {
384
632
  log('Could not find entry file - please import @eyeglass/inspector manually', 'warn');
633
+ log('Tip: Check your index.html for the entry script path', 'info');
385
634
  return false;
386
635
  }
387
636
  const content = readFile(entryFile);
@@ -402,12 +651,25 @@ function setupCRA(entryFile, dryRun) {
402
651
  }
403
652
  return true;
404
653
  }
405
- function init(options) {
654
+ async function init(options) {
406
655
  const { dryRun, skipInstall } = options;
407
- console.log('\n\x1b[1m🔍 Eyeglass Setup\x1b[0m\n');
656
+ let { agents } = options;
657
+ console.log(LOGO);
408
658
  if (dryRun) {
409
659
  log('Running in dry-run mode - no changes will be made\n', 'warn');
410
660
  }
661
+ // Prompt for agent selection if not provided
662
+ if (!agents || agents.length === 0) {
663
+ agents = await promptAgentSelection();
664
+ }
665
+ const agentNames = agents.map((a) => {
666
+ switch (a) {
667
+ case 'claude': return 'Claude Code';
668
+ case 'copilot': return 'GitHub Copilot CLI';
669
+ case 'codex': return 'OpenAI Codex CLI';
670
+ }
671
+ });
672
+ log(`Configuring for: ${agentNames.join(', ')}\n`);
411
673
  // Detect project
412
674
  const project = detectProject();
413
675
  log(`Detected: ${project.type}${project.typescript ? ' (TypeScript)' : ''}`);
@@ -426,10 +688,22 @@ function init(options) {
426
688
  }
427
689
  }
428
690
  }
429
- // Step 2: Setup Claude Code MCP config
430
- setupClaudeConfig(dryRun);
431
- // Step 3: Setup Eyeglass skill for Claude
432
- setupEyeglassSkill(dryRun);
691
+ // Step 2: Setup agent-specific configs
692
+ console.log('');
693
+ for (const agent of agents) {
694
+ switch (agent) {
695
+ case 'claude':
696
+ setupClaudeConfig(dryRun);
697
+ setupEyeglassSkill(dryRun);
698
+ break;
699
+ case 'copilot':
700
+ setupCopilotConfig(dryRun);
701
+ break;
702
+ case 'codex':
703
+ setupCodexConfig(dryRun);
704
+ break;
705
+ }
706
+ }
433
707
  // Step 4: Setup project-specific integration
434
708
  console.log('');
435
709
  switch (project.type) {
@@ -447,51 +721,105 @@ function init(options) {
447
721
  case 'remix':
448
722
  setupCRA(project.entryFile, dryRun);
449
723
  break;
450
- default:
451
- log('Unknown project type - please configure manually:', 'warn');
452
- console.log(' 1. Import @eyeglass/inspector in your entry file');
453
- console.log(' 2. Or add <script type="module">import "@eyeglass/inspector";</script> to your HTML\n');
724
+ default: {
725
+ // Try to find entry file from index.html for unknown project types
726
+ const entryFromHtml = findEntryFromHtml(process.cwd());
727
+ if (entryFromHtml) {
728
+ log(`Found entry file from index.html: ${path.relative(process.cwd(), entryFromHtml)}`, 'info');
729
+ const content = readFile(entryFromHtml);
730
+ if (content.includes('@eyeglass/inspector')) {
731
+ log('Inspector already imported', 'success');
732
+ }
733
+ else {
734
+ const importStatement = `import '@eyeglass/inspector';\n`;
735
+ const newContent = importStatement + content;
736
+ if (dryRun) {
737
+ log(`Would add inspector import to ${path.relative(process.cwd(), entryFromHtml)}`);
738
+ }
739
+ else {
740
+ writeFile(entryFromHtml, newContent);
741
+ log(`Added inspector import to ${path.relative(process.cwd(), entryFromHtml)}`, 'success');
742
+ }
743
+ }
744
+ }
745
+ else {
746
+ log('Unknown project type - please configure manually:', 'warn');
747
+ console.log(' 1. Import @eyeglass/inspector in your entry file');
748
+ console.log(' 2. Or add <script type="module">import "@eyeglass/inspector";</script> to your HTML\n');
749
+ }
750
+ break;
751
+ }
454
752
  }
455
753
  // Done!
456
- console.log('\n\x1b[1m✨ Setup complete!\x1b[0m\n');
754
+ console.log('\n\x1b[1mSetup complete!\x1b[0m\n');
755
+ printUsageInstructions(agents);
756
+ }
757
+ function printUsageInstructions(agents) {
457
758
  console.log('\x1b[1mHow to use Eyeglass:\x1b[0m\n');
458
759
  console.log(' 1. Start your dev server');
459
- console.log(' 2. Run \x1b[36mclaude\x1b[0m in this directory');
460
- console.log(' 3. Tell Claude: \x1b[36m"watch eyeglass"\x1b[0m or \x1b[36m"eg"\x1b[0m');
461
- console.log(' → Claude will start listening for requests');
462
- console.log(' 4. In your browser, hover over any element and click it to select for Eyeglass context. You can multi-select up to 5 items.');
463
- console.log(' 5. Type your request (e.g., "make this blue") and submit');
464
- console.log(' Claude automatically receives it and starts working!\n');
465
- console.log('\x1b[2mTip: You never need to leave your browser - Claude watches for requests.\x1b[0m\n');
760
+ console.log(' 2. Start the Eyeglass bridge: \x1b[36mnpx eyeglass-bridge\x1b[0m');
761
+ console.log('');
762
+ if (agents.includes('claude')) {
763
+ console.log(' \x1b[1mClaude Code:\x1b[0m');
764
+ console.log(' - Run \x1b[36mclaude\x1b[0m in this directory');
765
+ console.log(' - Say \x1b[36m"watch eyeglass"\x1b[0m or \x1b[36m"/eyeglass"\x1b[0m');
766
+ console.log('');
767
+ }
768
+ if (agents.includes('copilot')) {
769
+ console.log(' \x1b[1mGitHub Copilot CLI:\x1b[0m');
770
+ console.log(' - Run \x1b[36mgh copilot\x1b[0m in this directory');
771
+ console.log(' - Copilot will auto-connect to the eyeglass MCP server');
772
+ console.log('');
773
+ }
774
+ if (agents.includes('codex')) {
775
+ console.log(' \x1b[1mOpenAI Codex CLI:\x1b[0m');
776
+ console.log(' - See \x1b[36m.codex/eyeglass.md\x1b[0m for HTTP API docs');
777
+ console.log(' - Use \x1b[36mGET http://localhost:3300/api/wait\x1b[0m to listen');
778
+ console.log('');
779
+ }
780
+ console.log(' 3. In your browser, hover over any element and click to select');
781
+ console.log(' 4. Type your request (e.g., "make this blue") and submit');
782
+ console.log(' → Your agent automatically receives it and starts working!\n');
783
+ console.log('\x1b[2mTip: You never need to leave your browser - your agent watches for requests.\x1b[0m\n');
466
784
  }
467
785
  function help() {
468
- console.log(`
469
- \x1b[1mEyeglass\x1b[0m - Visual debugging for AI coding agents
786
+ console.log(LOGO);
787
+ console.log(`\x1b[2mVisual debugging for AI coding agents\x1b[0m
470
788
 
471
- Point at UI elements in your browser and tell Claude what to change.
472
- Claude watches for requests, so you never need to leave your browser.
789
+ Point at UI elements in your browser and tell your AI agent what to change.
790
+ Works with Claude Code, GitHub Copilot CLI, and OpenAI Codex CLI.
473
791
 
474
792
  \x1b[1mUSAGE\x1b[0m
475
793
  npx @eyeglass/cli <command> [options]
476
794
 
477
795
  \x1b[1mCOMMANDS\x1b[0m
478
- init Initialize Eyeglass in your project
796
+ init Initialize Eyeglass in your project (interactive)
479
797
  help Show this help message
480
798
 
481
799
  \x1b[1mOPTIONS\x1b[0m
482
800
  --dry-run Preview changes without making them
483
801
  --skip-install Skip installing @eyeglass/inspector
802
+ --claude Configure for Claude Code (stdio MCP)
803
+ --copilot Configure for GitHub Copilot CLI (local MCP)
804
+ --codex Configure for OpenAI Codex CLI (HTTP API)
484
805
 
485
806
  \x1b[1mEXAMPLES\x1b[0m
486
- npx @eyeglass/cli init # Full automatic setup
807
+ npx @eyeglass/cli init # Interactive agent selection
808
+ npx @eyeglass/cli init --claude # Setup for Claude Code only
809
+ npx @eyeglass/cli init --copilot --codex # Setup for multiple agents
487
810
  npx @eyeglass/cli init --dry-run # Preview what would change
488
811
 
812
+ \x1b[1mSUPPORTED AGENTS\x1b[0m
813
+ Claude Code - Uses stdio MCP (.claude/settings.json)
814
+ GitHub Copilot CLI - Uses local MCP (.copilot/mcp-config.json)
815
+ OpenAI Codex - Uses HTTP API (.codex/eyeglass.md)
816
+
489
817
  \x1b[1mWORKFLOW\x1b[0m
490
818
  1. Run \x1b[36mnpx @eyeglass/cli init\x1b[0m in your project
491
819
  2. Start your dev server
492
- 3. Run \x1b[36mclaude\x1b[0m and say "watch eyeglass" or "eg"
493
- 4. In your browser: hover over element → press E → type request
494
- 5. Claude automatically picks up requests and makes changes!
820
+ 3. Start your AI coding agent
821
+ 4. In your browser: hover over element → click → type request
822
+ 5. Your agent automatically picks up requests and makes changes!
495
823
 
496
824
  \x1b[1mMORE INFO\x1b[0m
497
825
  https://github.com/donutboyband/eyeglass
@@ -503,13 +831,25 @@ Claude watches for requests, so you never need to leave your browser.
503
831
  const args = process.argv.slice(2);
504
832
  const command = args.find((arg) => !arg.startsWith('-'));
505
833
  const flags = args.filter((arg) => arg.startsWith('-'));
834
+ // Parse agent flags
835
+ const agentFlags = [];
836
+ if (flags.includes('--claude'))
837
+ agentFlags.push('claude');
838
+ if (flags.includes('--copilot'))
839
+ agentFlags.push('copilot');
840
+ if (flags.includes('--codex'))
841
+ agentFlags.push('codex');
506
842
  const options = {
507
843
  dryRun: flags.includes('--dry-run'),
508
844
  skipInstall: flags.includes('--skip-install'),
845
+ agents: agentFlags.length > 0 ? agentFlags : undefined,
509
846
  };
510
847
  switch (command) {
511
848
  case 'init':
512
- init(options);
849
+ init(options).catch((err) => {
850
+ console.error('Error:', err.message);
851
+ process.exit(1);
852
+ });
513
853
  break;
514
854
  case 'help':
515
855
  case undefined:
@@ -58,6 +58,34 @@ function detectPackageManager(cwd, mockFs) {
58
58
  return 'yarn';
59
59
  return 'npm';
60
60
  }
61
+ // Helper to recreate findEntryFromHtml logic
62
+ function findEntryFromHtml(cwd, mockFs) {
63
+ const htmlFiles = [
64
+ 'index.html',
65
+ 'public/index.html',
66
+ 'src/index.html',
67
+ ];
68
+ for (const htmlFile of htmlFiles) {
69
+ const htmlPath = path.join(cwd, htmlFile);
70
+ if (!mockFs.existsSync(htmlPath))
71
+ continue;
72
+ const content = mockFs.readFileSync(htmlPath);
73
+ const moduleScriptRegex = /<script[^>]*type\s*=\s*["']module["'][^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
74
+ const altModuleScriptRegex = /<script[^>]*src\s*=\s*["']([^"']+)["'][^>]*type\s*=\s*["']module["'][^>]*>/gi;
75
+ let match = moduleScriptRegex.exec(content) || altModuleScriptRegex.exec(content);
76
+ if (match && match[1]) {
77
+ let scriptSrc = match[1];
78
+ if (scriptSrc.startsWith('/')) {
79
+ scriptSrc = scriptSrc.slice(1);
80
+ }
81
+ const scriptPath = path.join(cwd, scriptSrc);
82
+ if (mockFs.existsSync(scriptPath)) {
83
+ return scriptPath;
84
+ }
85
+ }
86
+ }
87
+ return null;
88
+ }
61
89
  describe('eyeglass CLI', () => {
62
90
  beforeEach(() => {
63
91
  vi.clearAllMocks();
@@ -234,6 +262,89 @@ export default defineConfig({
234
262
  expect(layoutFiles.length).toBe(8);
235
263
  });
236
264
  });
265
+ describe('findEntryFromHtml', () => {
266
+ it('should find entry script from index.html with leading slash', () => {
267
+ const mockFs = {
268
+ existsSync: (p) => p.endsWith('index.html') || p.endsWith('src/main.js'),
269
+ readFileSync: () => `<!DOCTYPE html>
270
+ <html>
271
+ <body>
272
+ <script type="module" src="/src/main.js"></script>
273
+ </body>
274
+ </html>`,
275
+ };
276
+ const result = findEntryFromHtml('/my/project', mockFs);
277
+ expect(result).toContain('src/main.js');
278
+ });
279
+ it('should find entry script from index.html without leading slash', () => {
280
+ const mockFs = {
281
+ existsSync: (p) => p.endsWith('index.html') || p.endsWith('src/app.js'),
282
+ readFileSync: () => `<!DOCTYPE html>
283
+ <html>
284
+ <body>
285
+ <script type="module" src="src/app.js"></script>
286
+ </body>
287
+ </html>`,
288
+ };
289
+ const result = findEntryFromHtml('/my/project', mockFs);
290
+ expect(result).toContain('src/app.js');
291
+ });
292
+ it('should find entry script with src before type attribute', () => {
293
+ const mockFs = {
294
+ existsSync: (p) => p.endsWith('index.html') || p.endsWith('src/counter.ts'),
295
+ readFileSync: () => `<!DOCTYPE html>
296
+ <html>
297
+ <body>
298
+ <script src="/src/counter.ts" type="module"></script>
299
+ </body>
300
+ </html>`,
301
+ };
302
+ const result = findEntryFromHtml('/my/project', mockFs);
303
+ expect(result).toContain('src/counter.ts');
304
+ });
305
+ it('should find entry script from public/index.html', () => {
306
+ const mockFs = {
307
+ existsSync: (p) => p.endsWith('public/index.html') || p.endsWith('src/index.js'),
308
+ readFileSync: () => `<!DOCTYPE html>
309
+ <html>
310
+ <body>
311
+ <script type="module" src="/src/index.js"></script>
312
+ </body>
313
+ </html>`,
314
+ };
315
+ const result = findEntryFromHtml('/my/project', mockFs);
316
+ expect(result).toContain('src/index.js');
317
+ });
318
+ it('should return null when index.html does not exist', () => {
319
+ const mockFs = {
320
+ existsSync: () => false,
321
+ readFileSync: () => '',
322
+ };
323
+ const result = findEntryFromHtml('/my/project', mockFs);
324
+ expect(result).toBeNull();
325
+ });
326
+ it('should return null when script file does not exist', () => {
327
+ const mockFs = {
328
+ existsSync: (p) => p.endsWith('index.html'),
329
+ readFileSync: () => `<script type="module" src="/src/main.js"></script>`,
330
+ };
331
+ const result = findEntryFromHtml('/my/project', mockFs);
332
+ expect(result).toBeNull();
333
+ });
334
+ it('should return null when no module script is found', () => {
335
+ const mockFs = {
336
+ existsSync: (p) => p.endsWith('index.html'),
337
+ readFileSync: () => `<!DOCTYPE html>
338
+ <html>
339
+ <body>
340
+ <script src="/src/main.js"></script>
341
+ </body>
342
+ </html>`,
343
+ };
344
+ const result = findEntryFromHtml('/my/project', mockFs);
345
+ expect(result).toBeNull();
346
+ });
347
+ });
237
348
  describe('use client directive handling', () => {
238
349
  it('should preserve use client directive at top', () => {
239
350
  const fileContent = `'use client';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeglass/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Visual debugging for AI coding agents - CLI",
5
5
  "type": "module",
6
6
  "bin": {