react-native-debug-toolkit 2.3.0 → 3.0.0

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.
Files changed (113) hide show
  1. package/bin/debug-toolkit.js +114 -0
  2. package/lib/commonjs/features/network/index.js +28 -2
  3. package/lib/commonjs/features/network/index.js.map +1 -1
  4. package/lib/commonjs/features/network/networkInterceptor.js +14 -6
  5. package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
  6. package/lib/commonjs/index.js +59 -0
  7. package/lib/commonjs/index.js.map +1 -1
  8. package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
  9. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  10. package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
  11. package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
  12. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +529 -0
  13. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
  14. package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
  15. package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
  16. package/lib/commonjs/utils/autoDetectDaemon.js +141 -0
  17. package/lib/commonjs/utils/autoDetectDaemon.js.map +1 -0
  18. package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
  19. package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
  20. package/lib/commonjs/utils/daemonConnection.js +81 -0
  21. package/lib/commonjs/utils/daemonConnection.js.map +1 -0
  22. package/lib/commonjs/utils/daemonSettings.js +110 -0
  23. package/lib/commonjs/utils/daemonSettings.js.map +1 -0
  24. package/lib/commonjs/utils/reportToDaemon.js +112 -0
  25. package/lib/commonjs/utils/reportToDaemon.js.map +1 -0
  26. package/lib/commonjs/utils/sessionReport.js +132 -0
  27. package/lib/commonjs/utils/sessionReport.js.map +1 -0
  28. package/lib/commonjs/utils/streamToDaemon.js +334 -0
  29. package/lib/commonjs/utils/streamToDaemon.js.map +1 -0
  30. package/lib/module/features/network/index.js +25 -1
  31. package/lib/module/features/network/index.js.map +1 -1
  32. package/lib/module/features/network/networkInterceptor.js +14 -6
  33. package/lib/module/features/network/networkInterceptor.js.map +1 -1
  34. package/lib/module/index.js +5 -0
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/ui/panel/DebugPanel.js +26 -1
  37. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  38. package/lib/module/ui/panel/FloatPanelView.js +16 -63
  39. package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
  40. package/lib/module/ui/panel/StreamingSettingsModal.js +524 -0
  41. package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
  42. package/lib/module/ui/panel/useTabAnimation.js +67 -0
  43. package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
  44. package/lib/module/utils/autoDetectDaemon.js +136 -0
  45. package/lib/module/utils/autoDetectDaemon.js.map +1 -0
  46. package/lib/module/utils/createPersistedObservableStore.js +23 -3
  47. package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
  48. package/lib/module/utils/daemonConnection.js +77 -0
  49. package/lib/module/utils/daemonConnection.js.map +1 -0
  50. package/lib/module/utils/daemonSettings.js +102 -0
  51. package/lib/module/utils/daemonSettings.js.map +1 -0
  52. package/lib/module/utils/reportToDaemon.js +105 -0
  53. package/lib/module/utils/reportToDaemon.js.map +1 -0
  54. package/lib/module/utils/sessionReport.js +128 -0
  55. package/lib/module/utils/sessionReport.js.map +1 -0
  56. package/lib/module/utils/streamToDaemon.js +328 -0
  57. package/lib/module/utils/streamToDaemon.js.map +1 -0
  58. package/lib/typescript/src/features/network/index.d.ts +2 -0
  59. package/lib/typescript/src/features/network/index.d.ts.map +1 -1
  60. package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
  61. package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
  62. package/lib/typescript/src/index.d.ts +10 -0
  63. package/lib/typescript/src/index.d.ts.map +1 -1
  64. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  65. package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
  66. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
  67. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
  68. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
  69. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
  70. package/lib/typescript/src/utils/autoDetectDaemon.d.ts +15 -0
  71. package/lib/typescript/src/utils/autoDetectDaemon.d.ts.map +1 -0
  72. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
  73. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
  74. package/lib/typescript/src/utils/daemonConnection.d.ts +18 -0
  75. package/lib/typescript/src/utils/daemonConnection.d.ts.map +1 -0
  76. package/lib/typescript/src/utils/daemonSettings.d.ts +19 -0
  77. package/lib/typescript/src/utils/daemonSettings.d.ts.map +1 -0
  78. package/lib/typescript/src/utils/reportToDaemon.d.ts +34 -0
  79. package/lib/typescript/src/utils/reportToDaemon.d.ts.map +1 -0
  80. package/lib/typescript/src/utils/sessionReport.d.ts +18 -0
  81. package/lib/typescript/src/utils/sessionReport.d.ts.map +1 -0
  82. package/lib/typescript/src/utils/streamToDaemon.d.ts +23 -0
  83. package/lib/typescript/src/utils/streamToDaemon.d.ts.map +1 -0
  84. package/node/daemon/src/cli.js +75 -0
  85. package/node/daemon/src/console/console.html +936 -0
  86. package/node/daemon/src/console/index.js +47 -0
  87. package/node/daemon/src/constants.js +32 -0
  88. package/node/daemon/src/index.js +11 -0
  89. package/node/daemon/src/server.js +365 -0
  90. package/node/daemon/src/store.js +110 -0
  91. package/node/mcp/src/cli.js +31 -0
  92. package/node/mcp/src/constants.js +13 -0
  93. package/node/mcp/src/daemonClient.js +132 -0
  94. package/node/mcp/src/httpClient.js +49 -0
  95. package/node/mcp/src/index.js +15 -0
  96. package/node/mcp/src/logs.js +95 -0
  97. package/node/mcp/src/server.js +144 -0
  98. package/node/mcp/src/tools.js +84 -0
  99. package/package.json +7 -2
  100. package/src/features/network/index.ts +30 -3
  101. package/src/features/network/networkInterceptor.ts +19 -6
  102. package/src/index.ts +14 -0
  103. package/src/ui/panel/DebugPanel.tsx +23 -1
  104. package/src/ui/panel/FloatPanelView.tsx +10 -68
  105. package/src/ui/panel/StreamingSettingsModal.tsx +566 -0
  106. package/src/ui/panel/useTabAnimation.ts +77 -0
  107. package/src/utils/autoDetectDaemon.ts +175 -0
  108. package/src/utils/createPersistedObservableStore.ts +16 -3
  109. package/src/utils/daemonConnection.ts +133 -0
  110. package/src/utils/daemonSettings.ts +134 -0
  111. package/src/utils/reportToDaemon.ts +172 -0
  112. package/src/utils/sessionReport.ts +203 -0
  113. package/src/utils/streamToDaemon.ts +419 -0
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const {
8
+ DEFAULT_DAEMON_ORIGIN,
9
+ EXPECTED_PROTOCOL_VERSION,
10
+ } = require('./constants');
11
+ const { requestJson } = require('./httpClient');
12
+
13
+ function getDaemonOrigin() {
14
+ return process.env.DEBUG_TOOLKIT_DAEMON_ORIGIN || DEFAULT_DAEMON_ORIGIN;
15
+ }
16
+
17
+ async function readHealth(origin = getDaemonOrigin()) {
18
+ const response = await requestJson(origin, '/health', { timeoutMs: 800 });
19
+ if (response.status !== 200 || !response.body?.ok) {
20
+ throw new Error(`Daemon health check failed with status ${response.status}`);
21
+ }
22
+ if (response.body.protocolVersion !== EXPECTED_PROTOCOL_VERSION) {
23
+ throw new Error(
24
+ `Daemon protocol mismatch: expected ${EXPECTED_PROTOCOL_VERSION}, got ${response.body.protocolVersion}`,
25
+ );
26
+ }
27
+ return response.body;
28
+ }
29
+
30
+ function findDaemonBin() {
31
+ if (process.env.DEBUG_TOOLKIT_DAEMON_BIN) {
32
+ return process.env.DEBUG_TOOLKIT_DAEMON_BIN;
33
+ }
34
+
35
+ try {
36
+ return require.resolve('react-native-debug-toolkit/bin/debug-toolkit.js');
37
+ } catch {
38
+ const localPath = path.resolve(__dirname, '../../../bin/debug-toolkit.js');
39
+ return fs.existsSync(localPath) ? localPath : null;
40
+ }
41
+ }
42
+
43
+ function getDaemonBindHost() {
44
+ return process.env.DEBUG_TOOLKIT_DAEMON_HOST || '0.0.0.0';
45
+ }
46
+
47
+ function spawnDaemon(origin = getDaemonOrigin()) {
48
+ const daemonBin = findDaemonBin();
49
+ if (!daemonBin) {
50
+ throw new Error('Cannot find react-native-debug-toolkit debug-toolkit bin');
51
+ }
52
+
53
+ const url = new URL(origin);
54
+ const child = spawn(process.execPath, [
55
+ daemonBin,
56
+ '--daemon-only',
57
+ '--host',
58
+ getDaemonBindHost(),
59
+ '--port',
60
+ url.port || '3799',
61
+ ], {
62
+ detached: true,
63
+ stdio: 'ignore',
64
+ });
65
+ child.unref();
66
+ }
67
+
68
+ function sleep(ms) {
69
+ return new Promise((resolve) => setTimeout(resolve, ms));
70
+ }
71
+
72
+ async function ensureDaemon(options = {}) {
73
+ const origin = options.origin || getDaemonOrigin();
74
+ const timeoutMs = options.timeoutMs || 3000;
75
+ const pollMs = options.pollMs || 150;
76
+ const startedAt = Date.now();
77
+
78
+ try {
79
+ return { ok: true, health: await readHealth(origin), origin, spawned: false };
80
+ } catch (initialError) {
81
+ try {
82
+ spawnDaemon(origin);
83
+ } catch (spawnError) {
84
+ return {
85
+ ok: false,
86
+ origin,
87
+ error: `${initialError.message}; ${spawnError.message}`,
88
+ };
89
+ }
90
+ }
91
+
92
+ while (Date.now() - startedAt < timeoutMs) {
93
+ await sleep(pollMs);
94
+ try {
95
+ return { ok: true, health: await readHealth(origin), origin, spawned: true };
96
+ } catch {
97
+ // Keep polling until timeout; the detached daemon may still be binding.
98
+ }
99
+ }
100
+
101
+ return {
102
+ ok: false,
103
+ origin,
104
+ error: `Daemon did not become healthy within ${timeoutMs}ms at ${origin}`,
105
+ };
106
+ }
107
+
108
+ async function readSession(origin, sessionId) {
109
+ const requestPath = sessionId ? `/sessions/${encodeURIComponent(sessionId)}` : '/latest';
110
+ const response = await requestJson(origin, requestPath, { timeoutMs: 3000 });
111
+ if (response.status !== 200 || !response.body?.ok) {
112
+ throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
113
+ }
114
+ return response.body;
115
+ }
116
+
117
+ async function readSessions(origin) {
118
+ const response = await requestJson(origin, '/sessions', { timeoutMs: 3000 });
119
+ if (response.status !== 200 || !response.body?.ok) {
120
+ throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
121
+ }
122
+ return response.body;
123
+ }
124
+
125
+ module.exports = {
126
+ ensureDaemon,
127
+ findDaemonBin,
128
+ getDaemonOrigin,
129
+ readHealth,
130
+ readSession,
131
+ readSessions,
132
+ };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+
6
+ function requestJson(origin, path, options = {}) {
7
+ const url = new URL(path, origin);
8
+ const transport = url.protocol === 'https:' ? https : http;
9
+
10
+ return new Promise((resolve, reject) => {
11
+ const req = transport.request(url, {
12
+ method: options.method || 'GET',
13
+ headers: options.headers || {},
14
+ timeout: options.timeoutMs || 3000,
15
+ }, (res) => {
16
+ const chunks = [];
17
+ res.on('data', (chunk) => chunks.push(chunk));
18
+ res.on('end', () => {
19
+ const raw = Buffer.concat(chunks).toString('utf8');
20
+ let body = null;
21
+ try {
22
+ body = raw ? JSON.parse(raw) : null;
23
+ } catch {
24
+ reject(new Error(`Daemon returned non-JSON response from ${path}`));
25
+ return;
26
+ }
27
+
28
+ resolve({
29
+ status: res.statusCode || 0,
30
+ body,
31
+ });
32
+ });
33
+ });
34
+
35
+ req.on('timeout', () => {
36
+ req.destroy(new Error(`Daemon request timed out: ${path}`));
37
+ });
38
+ req.on('error', reject);
39
+
40
+ if (options.body) {
41
+ req.write(JSON.stringify(options.body));
42
+ }
43
+ req.end();
44
+ });
45
+ }
46
+
47
+ module.exports = {
48
+ requestJson,
49
+ };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const { ensureDaemon } = require('./daemonClient');
4
+ const { handleMessage, startStdioServer } = require('./server');
5
+ const { callTool, getAppLogsTool, listAppSessionsTool, tools } = require('./tools');
6
+
7
+ module.exports = {
8
+ callTool,
9
+ ensureDaemon,
10
+ getAppLogsTool,
11
+ handleMessage,
12
+ listAppSessionsTool,
13
+ startStdioServer,
14
+ tools,
15
+ };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const KNOWN_LOG_TYPES = ['network', 'console', 'navigation', 'track', 'zustand'];
4
+
5
+ function isFailedLog(entry) {
6
+ return Boolean(
7
+ entry &&
8
+ typeof entry === 'object' &&
9
+ (
10
+ entry.error ||
11
+ entry.level === 'error' ||
12
+ entry.response?.success === false ||
13
+ entry.response?.status >= 400
14
+ ),
15
+ );
16
+ }
17
+
18
+ function stripBodies(value, parentKey) {
19
+ if (Array.isArray(value)) {
20
+ return value.map((item) => stripBodies(item, parentKey));
21
+ }
22
+
23
+ if (!value || typeof value !== 'object') {
24
+ return value;
25
+ }
26
+
27
+ return Object.entries(value).reduce((acc, [key, child]) => {
28
+ const normalizedKey = key.toLowerCase();
29
+ if (normalizedKey === 'body' || (parentKey === 'response' && normalizedKey === 'data')) {
30
+ return acc;
31
+ }
32
+ acc[key] = stripBodies(child, normalizedKey);
33
+ return acc;
34
+ }, {});
35
+ }
36
+
37
+ function selectLogs(report, options = {}) {
38
+ const logs = report?.logs && typeof report.logs === 'object' ? report.logs : {};
39
+ const logType = options.logType;
40
+ const limit = Number.isFinite(options.limit) && options.limit > 0
41
+ ? Math.min(Math.floor(options.limit), 200)
42
+ : 50;
43
+ const includeBodies = options.includeBodies !== false;
44
+ const failedOnly = options.failedOnly === true;
45
+
46
+ let entries;
47
+ if (logType) {
48
+ entries = Array.isArray(logs[logType]) ? logs[logType] : [];
49
+ } else {
50
+ entries = Object.entries(logs).flatMap(([type, typeEntries]) => (
51
+ Array.isArray(typeEntries)
52
+ ? typeEntries.map((entry) => ({ type, entry }))
53
+ : []
54
+ ));
55
+ }
56
+
57
+ if (failedOnly) {
58
+ entries = entries.filter((item) => isFailedLog(item.entry || item));
59
+ }
60
+
61
+ entries = entries.slice(-limit);
62
+
63
+ if (logType) {
64
+ return includeBodies ? entries : entries.map(stripBodies);
65
+ }
66
+
67
+ return entries.map((item) => ({
68
+ type: item.type,
69
+ entry: includeBodies ? item.entry : stripBodies(item.entry),
70
+ }));
71
+ }
72
+
73
+ function createToolPayload(session, options = {}) {
74
+ const report = session.report || { version: 2, logs: {} };
75
+ const logs = selectLogs(report, options);
76
+
77
+ return {
78
+ ok: true,
79
+ sessionId: session.sessionId,
80
+ receivedAt: session.receivedAt,
81
+ logType: options.logType || 'all',
82
+ failedOnly: options.failedOnly === true,
83
+ includeBodies: options.includeBodies !== false,
84
+ count: logs.length,
85
+ logs,
86
+ };
87
+ }
88
+
89
+ module.exports = {
90
+ KNOWN_LOG_TYPES,
91
+ createToolPayload,
92
+ isFailedLog,
93
+ selectLogs,
94
+ stripBodies,
95
+ };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ MCP_SERVER_NAME,
5
+ MCP_SERVER_VERSION,
6
+ } = require('./constants');
7
+ const { callTool, tools } = require('./tools');
8
+
9
+ function writeMessage(output, message) {
10
+ output.write(`${JSON.stringify(message)}\n`);
11
+ }
12
+
13
+ function createError(id, code, message) {
14
+ return {
15
+ jsonrpc: '2.0',
16
+ id,
17
+ error: { code, message },
18
+ };
19
+ }
20
+
21
+ async function handleMessage(message, context) {
22
+ const id = message.id;
23
+ const method = message.method;
24
+
25
+ if (!method) {
26
+ return null;
27
+ }
28
+
29
+ if (method === 'initialize') {
30
+ return {
31
+ jsonrpc: '2.0',
32
+ id,
33
+ result: {
34
+ protocolVersion: message.params?.protocolVersion || '2024-11-05',
35
+ capabilities: { tools: {} },
36
+ serverInfo: {
37
+ name: MCP_SERVER_NAME,
38
+ version: MCP_SERVER_VERSION,
39
+ },
40
+ },
41
+ };
42
+ }
43
+
44
+ if (method === 'notifications/initialized') {
45
+ return null;
46
+ }
47
+
48
+ if (method === 'ping') {
49
+ return {
50
+ jsonrpc: '2.0',
51
+ id,
52
+ result: {},
53
+ };
54
+ }
55
+
56
+ if (method === 'tools/list') {
57
+ return {
58
+ jsonrpc: '2.0',
59
+ id,
60
+ result: {
61
+ tools,
62
+ },
63
+ };
64
+ }
65
+
66
+ if (method === 'tools/call') {
67
+ try {
68
+ const result = await callTool(
69
+ message.params?.name,
70
+ message.params?.arguments || {},
71
+ context,
72
+ );
73
+ return {
74
+ jsonrpc: '2.0',
75
+ id,
76
+ result: {
77
+ content: [
78
+ {
79
+ type: 'text',
80
+ text: JSON.stringify(result, null, 2),
81
+ },
82
+ ],
83
+ },
84
+ };
85
+ } catch (error) {
86
+ return createError(id, -32000, error.message || 'Tool call failed');
87
+ }
88
+ }
89
+
90
+ if (id === undefined) {
91
+ return null;
92
+ }
93
+
94
+ return createError(id, -32601, `Method not found: ${method}`);
95
+ }
96
+
97
+ function startStdioServer(options = {}) {
98
+ const input = options.input || process.stdin;
99
+ const output = options.output || process.stdout;
100
+ const errorOutput = options.errorOutput || process.stderr;
101
+ const context = options.context || {};
102
+ let buffer = '';
103
+
104
+ input.setEncoding('utf8');
105
+ input.on('data', (chunk) => {
106
+ buffer += chunk;
107
+ const lines = buffer.split(/\r?\n/);
108
+ buffer = lines.pop() || '';
109
+
110
+ lines.forEach((line) => {
111
+ const trimmed = line.trim();
112
+ if (!trimmed) {
113
+ return;
114
+ }
115
+
116
+ let message;
117
+ try {
118
+ message = JSON.parse(trimmed);
119
+ } catch (error) {
120
+ errorOutput.write(`Invalid MCP JSON message: ${error.message}\n`);
121
+ return;
122
+ }
123
+
124
+ handleMessage(message, context)
125
+ .then((response) => {
126
+ if (response) {
127
+ writeMessage(output, response);
128
+ }
129
+ })
130
+ .catch((error) => {
131
+ if (message.id !== undefined) {
132
+ writeMessage(output, createError(message.id, -32000, error.message || 'MCP error'));
133
+ } else {
134
+ errorOutput.write(`MCP notification failed: ${error.message}\n`);
135
+ }
136
+ });
137
+ });
138
+ });
139
+ }
140
+
141
+ module.exports = {
142
+ handleMessage,
143
+ startStdioServer,
144
+ };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const { getDaemonOrigin, readSession, readSessions } = require('./daemonClient');
4
+ const { KNOWN_LOG_TYPES, createToolPayload } = require('./logs');
5
+
6
+ const getAppLogsTool = {
7
+ name: 'get_app_logs',
8
+ description: 'Read React Native Debug Toolkit logs from the local daemon. Tip: if you have shell access, curl http://127.0.0.1:3799/sessions/latest is more efficient.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ sessionId: { type: 'string' },
13
+ logType: {
14
+ type: 'string',
15
+ enum: KNOWN_LOG_TYPES,
16
+ },
17
+ limit: { type: 'number', default: 50 },
18
+ failedOnly: { type: 'boolean', default: false },
19
+ includeBodies: { type: 'boolean', default: true },
20
+ },
21
+ },
22
+ };
23
+
24
+ const listAppSessionsTool = {
25
+ name: 'list_app_sessions',
26
+ description: 'List React Native Debug Toolkit sessions available in the local daemon. Tip: if you have shell access, curl http://127.0.0.1:3799/sessions is more efficient.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {},
30
+ },
31
+ };
32
+
33
+ const tools = [getAppLogsTool, listAppSessionsTool];
34
+
35
+ async function callTool(name, args = {}, context = {}) {
36
+ const ensureDaemon = context.ensureDaemon || (async () => ({ ok: true, origin: getDaemonOrigin() }));
37
+ const daemon = await ensureDaemon();
38
+ if (!daemon.ok) {
39
+ return {
40
+ ok: false,
41
+ error: daemon.error || 'Debug toolkit daemon is not available',
42
+ origin: daemon.origin,
43
+ };
44
+ }
45
+
46
+ try {
47
+ if (name === listAppSessionsTool.name) {
48
+ const readSessionsImpl = context.readSessions || readSessions;
49
+ const result = await readSessionsImpl(daemon.origin);
50
+ const sessions = Array.isArray(result.sessions) ? result.sessions : [];
51
+ return {
52
+ ok: true,
53
+ origin: daemon.origin,
54
+ sessions,
55
+ count: sessions.length,
56
+ };
57
+ }
58
+
59
+ if (name !== getAppLogsTool.name) {
60
+ throw new Error(`Unknown tool: ${name}`);
61
+ }
62
+
63
+ const session = await readSession(daemon.origin, args.sessionId);
64
+ return createToolPayload(session, {
65
+ logType: args.logType,
66
+ limit: args.limit,
67
+ failedOnly: args.failedOnly,
68
+ includeBodies: args.includeBodies,
69
+ });
70
+ } catch (error) {
71
+ return {
72
+ ok: false,
73
+ error: error.message || 'Failed to read debug toolkit logs',
74
+ origin: daemon.origin,
75
+ };
76
+ }
77
+ }
78
+
79
+ module.exports = {
80
+ callTool,
81
+ getAppLogsTool,
82
+ listAppSessionsTool,
83
+ tools,
84
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "description": "A dev-only floating debug panel for React Native with network, console, Zustand, navigation, and event logs",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -8,6 +8,8 @@
8
8
  "files": [
9
9
  "src",
10
10
  "lib",
11
+ "bin",
12
+ "node",
11
13
  "README.md",
12
14
  "LICENSE",
13
15
  "!**/__tests__",
@@ -19,9 +21,12 @@
19
21
  "typescript": "npm run typecheck",
20
22
  "test": "jest",
21
23
  "build": "bob build",
22
- "lint": "eslint \"src/**/*.{js,ts,tsx}\"",
24
+ "lint": "eslint \"src/**/*.{js,ts,tsx}\" \"node/**/*.js\" \"bin/**/*.js\"",
23
25
  "prepare": "bob build"
24
26
  },
27
+ "bin": {
28
+ "debug-toolkit": "bin/debug-toolkit.js"
29
+ },
25
30
  "keywords": [
26
31
  "react-native",
27
32
  "debug",
@@ -9,8 +9,7 @@ import {
9
9
  startXMLHttpRequest,
10
10
  resetInterceptors,
11
11
  } from './networkInterceptor';
12
-
13
- type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
12
+ import type { NetworkLogPayload } from './networkInterceptor';
14
13
 
15
14
  // ─── Utilities ────────────────────────────────────────
16
15
 
@@ -37,6 +36,7 @@ function emitNetworkLog(entry: NetworkLogPayload): void {
37
36
  // ─── Feature factory ──────────────────────────────────
38
37
 
39
38
  const DEFAULT_MAX_LOGS = 200;
39
+ const daemonEndpointBlacklist: Array<string | RegExp> = [];
40
40
 
41
41
  export interface NetworkFeatureConfig {
42
42
  /** Maximum number of network logs to keep (default: 200) */
@@ -57,7 +57,7 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
57
57
  let stopXhrFn: (() => void) | null = null;
58
58
 
59
59
  const handleLog = (entry: NetworkLogPayload) => {
60
- if (isUrlBlacklisted(entry.request.url, blacklist)) {
60
+ if (isUrlBlacklisted(entry.request.url, [...blacklist, ...daemonEndpointBlacklist])) {
61
61
  return;
62
62
  }
63
63
  logStore.push({ ...entry, id: logStore.nextId() }, maxLogs);
@@ -95,8 +95,35 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
95
95
  };
96
96
  };
97
97
 
98
+ function normalizeDaemonEndpoint(endpoint: string): string {
99
+ const trimmed = endpoint.trim().replace(/\/+$/, '');
100
+ if (!trimmed) {
101
+ return trimmed;
102
+ }
103
+
104
+ try {
105
+ const url = new URL(trimmed);
106
+ return `${url.origin}${url.pathname === '/' ? '' : url.pathname}`;
107
+ } catch {
108
+ return trimmed;
109
+ }
110
+ }
111
+
112
+ export function _addDaemonEndpointToNetworkBlacklist(endpoint: string): void {
113
+ const normalized = normalizeDaemonEndpoint(endpoint);
114
+ if (!normalized || daemonEndpointBlacklist.includes(normalized)) {
115
+ return;
116
+ }
117
+ daemonEndpointBlacklist.push(normalized);
118
+ }
119
+
120
+ export function _isNetworkUrlBlacklistedForTesting(url: string): boolean {
121
+ return isUrlBlacklisted(url, daemonEndpointBlacklist);
122
+ }
123
+
98
124
  /** Reset module-level state for testing */
99
125
  export function _resetNetworkForTesting(): void {
100
126
  networkChannel = createEventChannel<NetworkLogPayload>();
127
+ daemonEndpointBlacklist.splice(0, daemonEndpointBlacklist.length);
101
128
  resetInterceptors();
102
129
  }
@@ -3,6 +3,8 @@ import { urlRewriter } from '../../utils/urlRewriterRegistry';
3
3
 
4
4
  type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
5
5
 
6
+ export type { NetworkLogPayload };
7
+
6
8
  // Intercepts React Native's XMLHttpRequest transport layer.
7
9
  // RN fetch and axios (default adapter) both go through XHR — one hook captures everything.
8
10
 
@@ -113,6 +115,15 @@ function safeRead<T>(read: () => T): T | undefined {
113
115
  }
114
116
  }
115
117
 
118
+ // URLs matching these patterns are skipped (Metro dev server internals).
119
+ const IGNORED_URL_PATTERNS = [
120
+ /\/symbolicate$/,
121
+ ];
122
+
123
+ function shouldIgnoreUrl(url: string): boolean {
124
+ return IGNORED_URL_PATTERNS.some((p) => p.test(url));
125
+ }
126
+
116
127
  function getGlobalXMLHttpRequest(): XMLHttpRequestConstructorLike | undefined {
117
128
  return (globalThis as { XMLHttpRequest?: XMLHttpRequestConstructorLike }).XMLHttpRequest;
118
129
  }
@@ -183,6 +194,9 @@ export function startXMLHttpRequest(
183
194
  ...args: unknown[]
184
195
  ) {
185
196
  const rewrittenUrl = urlRewriter.get() ? rewriteUrl(url) : url;
197
+ if (shouldIgnoreUrl(rewrittenUrl)) {
198
+ return originalXhrOpen!.call(this, method, rewrittenUrl, ...args);
199
+ }
186
200
  pendingXhrRequests.set(this, {
187
201
  method: (method || 'GET').toUpperCase(),
188
202
  url: rewrittenUrl,
@@ -209,12 +223,11 @@ export function startXMLHttpRequest(
209
223
  body?: unknown,
210
224
  ) {
211
225
  const that = this;
212
- const state = pendingXhrRequests.get(that) ?? {
213
- method: 'GET',
214
- url: '',
215
- headers: {},
216
- timestamp: Date.now(),
217
- };
226
+ const existingState = pendingXhrRequests.get(that);
227
+ if (!existingState) {
228
+ return originalXhrSend!.call(that, body);
229
+ }
230
+ const state = existingState;
218
231
  state.body = body;
219
232
  state.timestamp = Date.now();
220
233
  pendingXhrRequests.set(that, state);
package/src/index.ts CHANGED
@@ -28,6 +28,20 @@ export { useNavigationLogger } from './features/navigation/useNavigationLogger';
28
28
  export { safeStringify } from './utils/safeStringify';
29
29
  export { copyToComputer, logToComputer, fmt } from './utils/copyToComputer';
30
30
  export type { CopyResult, CopyOptions, CopyMethod } from './utils/copyToComputer';
31
+ export { createDebugSessionReport } from './utils/sessionReport';
32
+ export type { DebugSessionReport, DebugSessionReportOptions } from './utils/sessionReport';
33
+ export { getDefaultDaemonEndpoint, reportDebugSessionToDaemon } from './utils/reportToDaemon';
34
+ export type { ReportResult, ReportToDaemonOptions } from './utils/reportToDaemon';
35
+ export { checkDaemonConnection } from './utils/daemonConnection';
36
+ export type {
37
+ DaemonConnectionFailureReason,
38
+ DaemonConnectionOptions,
39
+ DaemonConnectionResult,
40
+ } from './utils/daemonConnection';
41
+ export { startStreaming, stopStreaming, isStreaming } from './utils/streamToDaemon';
42
+ export type { StreamStatus, StreamToDaemonOptions } from './utils/streamToDaemon';
43
+ export { autoDetectDaemonIp, getMetroHost } from './utils/autoDetectDaemon';
44
+ export type { AutoDetectOptions, AutoDetectResult } from './utils/autoDetectDaemon';
31
45
 
32
46
  // Types
33
47
  export type {