agent-react-devtools 0.1.0 → 0.1.1-canary-20260210005551

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # agent-react-devtools
2
2
 
3
+ ## 0.1.1-canary-20260210005551
4
+
5
+ ### Patch Changes
6
+
7
+ - 10ac53c: Add comprehensive README with usage examples and MIT LICENSE file
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  ### Minor Changes
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # agent-react-devtools
2
+
3
+ Give your AI agent eyes into your React app. Inspect component trees, read props and state, and profile rendering performance — all from the command line. Inspired by Vercel's [agent-browser](https://github.com/vercel-labs/agent-browser) and Callstack's [agent-device](https://github.com/callstackincubator/agent-device).
4
+
5
+ The project is in early development and considered experimental. Pull requests are welcome!
6
+
7
+ ## Features
8
+
9
+ - Walk the full component tree with props, state, and hooks
10
+ - Search for components by display name
11
+ - Profile renders: find slow components, excessive re-renders, and commit timelines
12
+ - Persistent background daemon that survives across CLI calls
13
+ - Token-efficient output built for LLM consumption
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ npm install -g agent-react-devtools
19
+ ```
20
+
21
+ Or run it directly:
22
+
23
+ ```sh
24
+ npx agent-react-devtools start
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```sh
30
+ agent-react-devtools start
31
+ agent-react-devtools status
32
+ ```
33
+
34
+ ```
35
+ Daemon: running (port 8097)
36
+ Apps: 1 connected, 24 components
37
+ Uptime: 12s
38
+ ```
39
+
40
+ Browse the component tree:
41
+
42
+ ```sh
43
+ agent-react-devtools get tree --depth 3
44
+ ```
45
+
46
+ ```
47
+ @c1 [fn] "App"
48
+ ├─ @c2 [fn] "Header"
49
+ │ ├─ @c3 [fn] "Nav"
50
+ │ └─ @c4 [fn] "SearchBar"
51
+ ├─ @c5 [fn] "TodoList"
52
+ │ ├─ @c6 [fn] "TodoItem" key="1"
53
+ │ ├─ @c7 [fn] "TodoItem" key="2"
54
+ │ └─ @c8 [fn] "TodoItem" key="3"
55
+ └─ @c9 [fn] "Footer"
56
+ ```
57
+
58
+ Inspect a component's props, state, and hooks:
59
+
60
+ ```sh
61
+ agent-react-devtools get component @c6
62
+ ```
63
+
64
+ ```
65
+ @c6 [fn] "TodoItem" key="1"
66
+ props:
67
+ id: 1
68
+ text: "Buy groceries"
69
+ done: false
70
+ onToggle: ƒ
71
+ hooks:
72
+ State: false
73
+ Callback: ƒ
74
+ ```
75
+
76
+ Find components by name:
77
+
78
+ ```sh
79
+ agent-react-devtools find TodoItem
80
+ ```
81
+
82
+ ```
83
+ @c6 [fn] "TodoItem" key="1"
84
+ @c7 [fn] "TodoItem" key="2"
85
+ @c8 [fn] "TodoItem" key="3"
86
+ ```
87
+
88
+ Profile rendering performance:
89
+
90
+ ```sh
91
+ agent-react-devtools profile start
92
+ # ... interact with the app ...
93
+ agent-react-devtools profile stop
94
+ agent-react-devtools profile slow
95
+ ```
96
+
97
+ ```
98
+ Slowest (by avg render time):
99
+ TodoList avg:4.2ms max:8.1ms renders:6 cause:props
100
+ SearchBar avg:2.1ms max:3.4ms renders:12 cause:hooks
101
+ Header avg:0.8ms max:1.2ms renders:3 cause:parent
102
+ ```
103
+
104
+ ## Commands
105
+
106
+ ### Daemon
107
+
108
+ ```sh
109
+ agent-react-devtools start [--port 8097] # Start daemon
110
+ agent-react-devtools stop # Stop daemon
111
+ agent-react-devtools status # Connection status
112
+ ```
113
+
114
+ ### Components
115
+
116
+ ```sh
117
+ agent-react-devtools get tree [--depth N] # Component hierarchy
118
+ agent-react-devtools get component <@c1 | id> # Props, state, hooks
119
+ agent-react-devtools find <name> [--exact] # Search by display name
120
+ agent-react-devtools count # Component count by type
121
+ ```
122
+
123
+ Components are labeled `@c1`, `@c2`, etc. You can use these labels or numeric IDs interchangeably.
124
+
125
+ ### Profiling
126
+
127
+ ```sh
128
+ agent-react-devtools profile start [name] # Begin a profiling session
129
+ agent-react-devtools profile stop # Stop and collect data
130
+ agent-react-devtools profile report <@c1 | id> # Render report for a component
131
+ agent-react-devtools profile slow [--limit N] # Slowest components by avg duration
132
+ agent-react-devtools profile rerenders [--limit N] # Most re-rendered components
133
+ agent-react-devtools profile timeline [--limit N] # Commit timeline
134
+ agent-react-devtools profile commit <N | #N> [--limit N] # Single commit detail
135
+ ```
136
+
137
+ ## Connecting Your App
138
+
139
+ Install `react-devtools-core` in your app and connect before React renders (e.g. in `src/main.tsx`):
140
+
141
+ ```ts
142
+ import { initialize, connectToDevTools } from 'react-devtools-core';
143
+ import { createRoot } from 'react-dom/client';
144
+ import App from './App';
145
+
146
+ initialize();
147
+ connectToDevTools({ port: 8097 });
148
+
149
+ createRoot(document.getElementById('root')!).render(<App />);
150
+ ```
151
+
152
+ ## Using with AI Agents
153
+
154
+ Add something like this to your project's `CLAUDE.md` (or equivalent agent instructions):
155
+
156
+ ```markdown
157
+ ## React Debugging
158
+
159
+ This project uses agent-react-devtools to inspect the running React app.
160
+
161
+ - `agent-react-devtools start` — start the daemon
162
+ - `agent-react-devtools status` — check if the app is connected
163
+ - `agent-react-devtools get tree` — see the component hierarchy
164
+ - `agent-react-devtools get component @c1` — inspect a specific component
165
+ - `agent-react-devtools find <Name>` — search for components
166
+ - `agent-react-devtools profile start` / `profile stop` / `profile slow` — diagnose render performance
167
+ ```
168
+
169
+ ## Development
170
+
171
+ ```sh
172
+ bun install # Install dependencies
173
+ bun run build # Build
174
+ bun run test # Run tests
175
+ bun run typecheck # Type check
176
+ ```
177
+
178
+ ## License
179
+
180
+ MIT
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "name": "agent-react-devtools",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-canary-20260210005551",
4
4
  "description": "CLI tool for AI agents to inspect React component trees and profile performance",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agent-react-devtools": "./dist/cli.js"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "CHANGELOG.md"
13
+ ],
9
14
  "scripts": {
10
15
  "build": "tsup",
11
16
  "dev": "tsup --watch",
12
17
  "test": "vitest run",
13
18
  "test:watch": "vitest",
14
19
  "typecheck": "tsc --noEmit",
15
- "lint": "tsc --noEmit"
20
+ "lint": "tsc --noEmit",
21
+ "prepublishOnly": "cp ../../README.md ."
16
22
  },
17
23
  "repository": {
18
24
  "type": "git",
@@ -1,76 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
-
3
- // Inline the parseArgs function for testing (it's not exported from cli.ts)
4
- function parseArgs(argv: string[]): {
5
- command: string[];
6
- flags: Record<string, string | boolean>;
7
- } {
8
- const command: string[] = [];
9
- const flags: Record<string, string | boolean> = {};
10
-
11
- for (let i = 0; i < argv.length; i++) {
12
- const arg = argv[i];
13
- if (arg.startsWith('--')) {
14
- const key = arg.slice(2);
15
- const eqIdx = key.indexOf('=');
16
- if (eqIdx !== -1) {
17
- flags[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
18
- } else {
19
- const next = argv[i + 1];
20
- if (next && !next.startsWith('--')) {
21
- flags[key] = next;
22
- i++;
23
- } else {
24
- flags[key] = true;
25
- }
26
- }
27
- } else {
28
- command.push(arg);
29
- }
30
- }
31
- return { command, flags };
32
- }
33
-
34
- describe('CLI argument parser', () => {
35
- it('should parse simple commands', () => {
36
- const { command, flags } = parseArgs(['start']);
37
- expect(command).toEqual(['start']);
38
- expect(flags).toEqual({});
39
- });
40
-
41
- it('should parse commands with subcommands', () => {
42
- const { command } = parseArgs(['get', 'tree']);
43
- expect(command).toEqual(['get', 'tree']);
44
- });
45
-
46
- it('should parse --flag=value', () => {
47
- const { flags } = parseArgs(['start', '--port=8098']);
48
- expect(flags['port']).toBe('8098');
49
- });
50
-
51
- it('should parse --flag value', () => {
52
- const { flags } = parseArgs(['start', '--port', '8098']);
53
- expect(flags['port']).toBe('8098');
54
- });
55
-
56
- it('should parse boolean flags', () => {
57
- const { flags } = parseArgs(['find', 'User', '--exact']);
58
- expect(flags['exact']).toBe(true);
59
- });
60
-
61
- it('should parse mixed args', () => {
62
- const { command, flags } = parseArgs([
63
- 'profile',
64
- 'slow',
65
- '--limit',
66
- '5',
67
- ]);
68
- expect(command).toEqual(['profile', 'slow']);
69
- expect(flags['limit']).toBe('5');
70
- });
71
-
72
- it('should parse component id as positional arg', () => {
73
- const { command } = parseArgs(['get', 'component', '42']);
74
- expect(command).toEqual(['get', 'component', '42']);
75
- });
76
- });
@@ -1,229 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { ComponentTree } from '../component-tree.js';
3
-
4
- /**
5
- * Operations encoding reference (protocol v2):
6
- * [rendererID, rootFiberID, stringTableSize, ...stringTable, ...ops]
7
- *
8
- * String table: for each string, [length, ...charCodes]. String ID 0 = null.
9
- *
10
- * TREE_OPERATION_ADD (1):
11
- * 1, id, elementType, parentId, ownerID, displayNameStringID, keyStringID
12
- *
13
- * Element types (from react-devtools-shared/src/frontend/types.js):
14
- * CLASS=1, FUNCTION=5, FORWARD_REF=6, HOST=7, MEMO=8, PROFILER=10, ROOT=11, SUSPENSE=12
15
- */
16
-
17
- /** Build a string table and return [tableData, stringIdMap] */
18
- function buildStringTable(strings: string[]): [number[], Map<string, number>] {
19
- const idMap = new Map<string, number>();
20
- const data: number[] = [];
21
- for (const s of strings) {
22
- const id = idMap.size + 1; // 0 is reserved for null
23
- if (!idMap.has(s)) {
24
- idMap.set(s, id);
25
- data.push(s.length, ...Array.from(s).map((c) => c.charCodeAt(0)));
26
- }
27
- }
28
- return [data, idMap];
29
- }
30
-
31
- /** Build a complete operations array with string table */
32
- function buildOps(
33
- rendererID: number,
34
- rootID: number,
35
- strings: string[],
36
- opsFn: (strId: (s: string) => number) => number[],
37
- ): number[] {
38
- const [tableData, idMap] = buildStringTable(strings);
39
- const strId = (s: string) => idMap.get(s) || 0;
40
- const ops = opsFn(strId);
41
- return [rendererID, rootID, tableData.length, ...tableData, ...ops];
42
- }
43
-
44
- function addOp(
45
- id: number,
46
- elementType: number,
47
- parentId: number,
48
- displayNameStrId: number,
49
- keyStrId: number = 0,
50
- ): number[] {
51
- return [1, id, elementType, parentId, 0, displayNameStrId, keyStrId];
52
- }
53
-
54
- function removeOp(...ids: number[]): number[] {
55
- return [2, ids.length, ...ids];
56
- }
57
-
58
- function reorderOp(id: number, children: number[]): number[] {
59
- return [3, id, children.length, ...children];
60
- }
61
-
62
- describe('ComponentTree', () => {
63
- let tree: ComponentTree;
64
-
65
- beforeEach(() => {
66
- tree = new ComponentTree();
67
- });
68
-
69
- it('should add nodes from operations', () => {
70
- const ops = buildOps(1, 100, ['App', 'Header', 'Footer'], (s) => [
71
- ...addOp(1, 5, 0, s('App')), // Function component at root
72
- ...addOp(2, 8, 1, s('Header')), // Memo child
73
- ...addOp(3, 5, 1, s('Footer')), // Function child
74
- ]);
75
-
76
- tree.applyOperations(ops);
77
-
78
- expect(tree.getComponentCount()).toBe(3);
79
-
80
- const node1 = tree.getNode(1);
81
- expect(node1).toBeDefined();
82
- expect(node1!.displayName).toBe('App');
83
- expect(node1!.type).toBe('function');
84
- expect(node1!.children).toEqual([2, 3]);
85
-
86
- const node2 = tree.getNode(2);
87
- expect(node2!.displayName).toBe('Header');
88
- expect(node2!.type).toBe('memo');
89
- expect(node2!.parentId).toBe(1);
90
- });
91
-
92
- it('should handle keys', () => {
93
- const ops = buildOps(1, 100, ['List', 'Item', 'item-1', 'item-2'], (s) => [
94
- ...addOp(1, 5, 0, s('List')),
95
- ...addOp(2, 5, 1, s('Item'), s('item-1')),
96
- ...addOp(3, 5, 1, s('Item'), s('item-2')),
97
- ]);
98
-
99
- tree.applyOperations(ops);
100
-
101
- expect(tree.getNode(2)!.key).toBe('item-1');
102
- expect(tree.getNode(3)!.key).toBe('item-2');
103
- });
104
-
105
- it('should remove nodes', () => {
106
- const addOps = buildOps(1, 100, ['App', 'Child'], (s) => [
107
- ...addOp(1, 5, 0, s('App')),
108
- ...addOp(2, 5, 1, s('Child')),
109
- ]);
110
- tree.applyOperations(addOps);
111
- expect(tree.getComponentCount()).toBe(2);
112
-
113
- // Remove ops still need a string table (can be empty)
114
- const rmOps = [1, 100, 0, ...removeOp(2)];
115
- tree.applyOperations(rmOps);
116
- expect(tree.getComponentCount()).toBe(1);
117
- expect(tree.getNode(1)!.children).toEqual([]);
118
- });
119
-
120
- it('should reorder children', () => {
121
- const ops = buildOps(1, 100, ['App', 'A', 'B', 'C'], (s) => [
122
- ...addOp(1, 5, 0, s('App')),
123
- ...addOp(2, 5, 1, s('A')),
124
- ...addOp(3, 5, 1, s('B')),
125
- ...addOp(4, 5, 1, s('C')),
126
- ]);
127
- tree.applyOperations(ops);
128
- expect(tree.getNode(1)!.children).toEqual([2, 3, 4]);
129
-
130
- const reorderOps = [1, 100, 0, ...reorderOp(1, [4, 2, 3])];
131
- tree.applyOperations(reorderOps);
132
- expect(tree.getNode(1)!.children).toEqual([4, 2, 3]);
133
- });
134
-
135
- it('should find by name (partial match)', () => {
136
- const ops = buildOps(1, 100, ['App', 'UserProfile', 'UserCard', 'Footer'], (s) => [
137
- ...addOp(1, 5, 0, s('App')),
138
- ...addOp(2, 5, 1, s('UserProfile')),
139
- ...addOp(3, 5, 1, s('UserCard')),
140
- ...addOp(4, 5, 1, s('Footer')),
141
- ]);
142
- tree.applyOperations(ops);
143
-
144
- const results = tree.findByName('user');
145
- expect(results).toHaveLength(2);
146
- expect(results.map((r) => r.displayName).sort()).toEqual([
147
- 'UserCard',
148
- 'UserProfile',
149
- ]);
150
- });
151
-
152
- it('should find by name (exact match)', () => {
153
- const ops = buildOps(1, 100, ['App', 'UserProfile', 'UserCard'], (s) => [
154
- ...addOp(1, 5, 0, s('App')),
155
- ...addOp(2, 5, 1, s('UserProfile')),
156
- ...addOp(3, 5, 1, s('UserCard')),
157
- ]);
158
- tree.applyOperations(ops);
159
-
160
- const results = tree.findByName('UserProfile', true);
161
- expect(results).toHaveLength(1);
162
- expect(results[0].displayName).toBe('UserProfile');
163
- });
164
-
165
- it('should get count by type', () => {
166
- const ops = buildOps(1, 100, ['App', 'MemoComp', 'FuncComp', 'div'], (s) => [
167
- ...addOp(1, 5, 0, s('App')), // function
168
- ...addOp(2, 8, 1, s('MemoComp')), // memo
169
- ...addOp(3, 5, 1, s('FuncComp')), // function
170
- ...addOp(4, 7, 1, s('div')), // host
171
- ]);
172
- tree.applyOperations(ops);
173
-
174
- const counts = tree.getCountByType();
175
- expect(counts['function']).toBe(2);
176
- expect(counts['memo']).toBe(1);
177
- expect(counts['host']).toBe(1);
178
- });
179
-
180
- it('should get tree with depth limit', () => {
181
- const ops = buildOps(1, 100, ['App', 'Level1', 'Level2', 'Level3'], (s) => [
182
- ...addOp(1, 5, 0, s('App')),
183
- ...addOp(2, 5, 1, s('Level1')),
184
- ...addOp(3, 5, 2, s('Level2')),
185
- ...addOp(4, 5, 3, s('Level3')),
186
- ]);
187
- tree.applyOperations(ops);
188
-
189
- const fullTree = tree.getTree();
190
- expect(fullTree).toHaveLength(4);
191
-
192
- const shallow = tree.getTree(1);
193
- expect(shallow).toHaveLength(2);
194
- expect(shallow.map((n) => n.displayName)).toEqual(['App', 'Level1']);
195
- });
196
-
197
- it('should handle empty operations', () => {
198
- tree.applyOperations([]);
199
- expect(tree.getComponentCount()).toBe(0);
200
-
201
- tree.applyOperations([1]);
202
- expect(tree.getComponentCount()).toBe(0);
203
- });
204
-
205
- it('should handle all element types', () => {
206
- const ops = buildOps(
207
- 1, 100,
208
- ['ClassComp', 'FuncComp', 'ForwardRefComp', 'HostComp', 'MemoComp', 'ProfilerComp', 'SuspenseComp'],
209
- (s) => [
210
- ...addOp(1, 1, 0, s('ClassComp')), // class = 1
211
- ...addOp(2, 5, 1, s('FuncComp')), // function = 5
212
- ...addOp(3, 6, 1, s('ForwardRefComp')), // forwardRef = 6
213
- ...addOp(4, 7, 1, s('HostComp')), // host = 7
214
- ...addOp(5, 8, 1, s('MemoComp')), // memo = 8
215
- ...addOp(6, 10, 1, s('ProfilerComp')), // profiler = 10
216
- ...addOp(7, 12, 1, s('SuspenseComp')), // suspense = 12
217
- ],
218
- );
219
- tree.applyOperations(ops);
220
-
221
- expect(tree.getNode(1)!.type).toBe('class');
222
- expect(tree.getNode(2)!.type).toBe('function');
223
- expect(tree.getNode(3)!.type).toBe('forwardRef');
224
- expect(tree.getNode(4)!.type).toBe('host');
225
- expect(tree.getNode(5)!.type).toBe('memo');
226
- expect(tree.getNode(6)!.type).toBe('profiler');
227
- expect(tree.getNode(7)!.type).toBe('suspense');
228
- });
229
- });