react-native-debug-toolkit 2.2.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.
- package/README.md +5 -8
- package/README.zh-CN.md +5 -8
- package/bin/debug-toolkit.js +114 -0
- package/lib/commonjs/features/network/index.js +32 -12
- package/lib/commonjs/features/network/index.js.map +1 -1
- package/lib/commonjs/features/network/networkInterceptor.js +164 -201
- package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
- package/lib/commonjs/index.js +59 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
- package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +529 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/commonjs/utils/autoDetectDaemon.js +141 -0
- package/lib/commonjs/utils/autoDetectDaemon.js.map +1 -0
- package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
- package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/commonjs/utils/daemonConnection.js +81 -0
- package/lib/commonjs/utils/daemonConnection.js.map +1 -0
- package/lib/commonjs/utils/daemonSettings.js +110 -0
- package/lib/commonjs/utils/daemonSettings.js.map +1 -0
- package/lib/commonjs/utils/reportToDaemon.js +112 -0
- package/lib/commonjs/utils/reportToDaemon.js.map +1 -0
- package/lib/commonjs/utils/sessionReport.js +132 -0
- package/lib/commonjs/utils/sessionReport.js.map +1 -0
- package/lib/commonjs/utils/streamToDaemon.js +334 -0
- package/lib/commonjs/utils/streamToDaemon.js.map +1 -0
- package/lib/module/features/network/index.js +30 -12
- package/lib/module/features/network/index.js.map +1 -1
- package/lib/module/features/network/networkInterceptor.js +163 -200
- package/lib/module/features/network/networkInterceptor.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +26 -1
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/ui/panel/FloatPanelView.js +16 -63
- package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +524 -0
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/module/ui/panel/useTabAnimation.js +67 -0
- package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/module/utils/autoDetectDaemon.js +136 -0
- package/lib/module/utils/autoDetectDaemon.js.map +1 -0
- package/lib/module/utils/createPersistedObservableStore.js +23 -3
- package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/module/utils/daemonConnection.js +77 -0
- package/lib/module/utils/daemonConnection.js.map +1 -0
- package/lib/module/utils/daemonSettings.js +102 -0
- package/lib/module/utils/daemonSettings.js.map +1 -0
- package/lib/module/utils/reportToDaemon.js +105 -0
- package/lib/module/utils/reportToDaemon.js.map +1 -0
- package/lib/module/utils/sessionReport.js +128 -0
- package/lib/module/utils/sessionReport.js.map +1 -0
- package/lib/module/utils/streamToDaemon.js +328 -0
- package/lib/module/utils/streamToDaemon.js.map +1 -0
- package/lib/typescript/src/features/network/index.d.ts +2 -4
- package/lib/typescript/src/features/network/index.d.ts.map +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts +2 -15
- package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +11 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
- package/lib/typescript/src/utils/autoDetectDaemon.d.ts +15 -0
- package/lib/typescript/src/utils/autoDetectDaemon.d.ts.map +1 -0
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
- package/lib/typescript/src/utils/daemonConnection.d.ts +18 -0
- package/lib/typescript/src/utils/daemonConnection.d.ts.map +1 -0
- package/lib/typescript/src/utils/daemonSettings.d.ts +19 -0
- package/lib/typescript/src/utils/daemonSettings.d.ts.map +1 -0
- package/lib/typescript/src/utils/reportToDaemon.d.ts +34 -0
- package/lib/typescript/src/utils/reportToDaemon.d.ts.map +1 -0
- package/lib/typescript/src/utils/sessionReport.d.ts +18 -0
- package/lib/typescript/src/utils/sessionReport.d.ts.map +1 -0
- package/lib/typescript/src/utils/streamToDaemon.d.ts +23 -0
- package/lib/typescript/src/utils/streamToDaemon.d.ts.map +1 -0
- package/node/daemon/src/cli.js +75 -0
- package/node/daemon/src/console/console.html +936 -0
- package/node/daemon/src/console/index.js +47 -0
- package/node/daemon/src/constants.js +32 -0
- package/node/daemon/src/index.js +11 -0
- package/node/daemon/src/server.js +365 -0
- package/node/daemon/src/store.js +110 -0
- package/node/mcp/src/cli.js +31 -0
- package/node/mcp/src/constants.js +13 -0
- package/node/mcp/src/daemonClient.js +132 -0
- package/node/mcp/src/httpClient.js +49 -0
- package/node/mcp/src/index.js +15 -0
- package/node/mcp/src/logs.js +95 -0
- package/node/mcp/src/server.js +144 -0
- package/node/mcp/src/tools.js +84 -0
- package/package.json +10 -5
- package/src/features/network/index.ts +35 -19
- package/src/features/network/networkInterceptor.ts +224 -236
- package/src/index.ts +15 -1
- package/src/ui/panel/DebugPanel.tsx +23 -1
- package/src/ui/panel/FloatPanelView.tsx +10 -68
- package/src/ui/panel/StreamingSettingsModal.tsx +566 -0
- package/src/ui/panel/useTabAnimation.ts +77 -0
- package/src/utils/autoDetectDaemon.ts +175 -0
- package/src/utils/createPersistedObservableStore.ts +16 -3
- package/src/utils/daemonConnection.ts +133 -0
- package/src/utils/daemonSettings.ts +134 -0
- package/src/utils/reportToDaemon.ts +172 -0
- package/src/utils/sessionReport.ts +203 -0
- 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": "
|
|
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",
|
|
@@ -34,11 +39,11 @@
|
|
|
34
39
|
"license": "MIT",
|
|
35
40
|
"repository": {
|
|
36
41
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/
|
|
42
|
+
"url": "https://github.com/superzcj/react-native-debug-toolkit"
|
|
38
43
|
},
|
|
39
|
-
"homepage": "https://github.com/
|
|
44
|
+
"homepage": "https://github.com/superzcj/react-native-debug-toolkit#readme",
|
|
40
45
|
"bugs": {
|
|
41
|
-
"url": "https://github.com/
|
|
46
|
+
"url": "https://github.com/superzcj/react-native-debug-toolkit/issues"
|
|
42
47
|
},
|
|
43
48
|
"peerDependencies": {
|
|
44
49
|
"@react-native-clipboard/clipboard": ">=1.0.0",
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { NetworkLogTab } from './NetworkLogTab';
|
|
2
2
|
|
|
3
|
-
export type { AxiosInstanceLike } from './networkInterceptor';
|
|
4
3
|
import type { DebugFeature, NetworkLogEntry } from '../../types';
|
|
5
4
|
import { createEventChannel } from '../../utils/createEventChannel';
|
|
6
5
|
import { createPersistedObservableStore } from '../../utils/createPersistedObservableStore';
|
|
7
6
|
import { KEYS } from '../../utils/debugPreferences';
|
|
8
7
|
import { urlRewriter } from '../../utils/urlRewriterRegistry';
|
|
9
|
-
import type { AxiosInstanceLike } from './networkInterceptor';
|
|
10
8
|
import {
|
|
11
|
-
|
|
12
|
-
startAxios,
|
|
9
|
+
startXMLHttpRequest,
|
|
13
10
|
resetInterceptors,
|
|
14
11
|
} from './networkInterceptor';
|
|
15
|
-
|
|
16
|
-
type NetworkLogPayload = Omit<NetworkLogEntry, 'id'>;
|
|
12
|
+
import type { NetworkLogPayload } from './networkInterceptor';
|
|
17
13
|
|
|
18
14
|
// ─── Utilities ────────────────────────────────────────
|
|
19
15
|
|
|
@@ -40,14 +36,13 @@ function emitNetworkLog(entry: NetworkLogPayload): void {
|
|
|
40
36
|
// ─── Feature factory ──────────────────────────────────
|
|
41
37
|
|
|
42
38
|
const DEFAULT_MAX_LOGS = 200;
|
|
39
|
+
const daemonEndpointBlacklist: Array<string | RegExp> = [];
|
|
43
40
|
|
|
44
41
|
export interface NetworkFeatureConfig {
|
|
45
42
|
/** Maximum number of network logs to keep (default: 200) */
|
|
46
43
|
maxLogs?: number;
|
|
47
44
|
/** URLs to filter out from logging */
|
|
48
45
|
blacklist?: Array<string | RegExp>;
|
|
49
|
-
/** Axios instance to intercept. Pass your axios.create() instance to capture axios requests. */
|
|
50
|
-
axiosInstance?: AxiosInstanceLike;
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeature<NetworkLogEntry[]> => {
|
|
@@ -59,11 +54,10 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
|
|
|
59
54
|
});
|
|
60
55
|
let initialized = false;
|
|
61
56
|
let unsubscribeLogs: (() => void) | null = null;
|
|
62
|
-
let
|
|
63
|
-
let stopAxiosFn: (() => void) | null = null;
|
|
57
|
+
let stopXhrFn: (() => void) | null = null;
|
|
64
58
|
|
|
65
59
|
const handleLog = (entry: NetworkLogPayload) => {
|
|
66
|
-
if (isUrlBlacklisted(entry.request.url, blacklist)) {
|
|
60
|
+
if (isUrlBlacklisted(entry.request.url, [...blacklist, ...daemonEndpointBlacklist])) {
|
|
67
61
|
return;
|
|
68
62
|
}
|
|
69
63
|
logStore.push({ ...entry, id: logStore.nextId() }, maxLogs);
|
|
@@ -78,10 +72,7 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
|
|
|
78
72
|
return;
|
|
79
73
|
}
|
|
80
74
|
unsubscribeLogs = networkChannel.subscribe(handleLog);
|
|
81
|
-
|
|
82
|
-
if (config?.axiosInstance) {
|
|
83
|
-
stopAxiosFn = startAxios(config.axiosInstance, emitNetworkLog);
|
|
84
|
-
}
|
|
75
|
+
stopXhrFn = startXMLHttpRequest(emitNetworkLog);
|
|
85
76
|
initialized = true;
|
|
86
77
|
},
|
|
87
78
|
getSnapshot: () => logStore.getData(),
|
|
@@ -95,10 +86,8 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
|
|
|
95
86
|
urlRewriter.set(null);
|
|
96
87
|
unsubscribeLogs?.();
|
|
97
88
|
unsubscribeLogs = null;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
stopAxiosFn?.();
|
|
101
|
-
stopAxiosFn = null;
|
|
89
|
+
stopXhrFn?.();
|
|
90
|
+
stopXhrFn = null;
|
|
102
91
|
logStore.clear();
|
|
103
92
|
initialized = false;
|
|
104
93
|
},
|
|
@@ -106,8 +95,35 @@ export const createNetworkFeature = (config?: NetworkFeatureConfig): DebugFeatur
|
|
|
106
95
|
};
|
|
107
96
|
};
|
|
108
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
|
+
|
|
109
124
|
/** Reset module-level state for testing */
|
|
110
125
|
export function _resetNetworkForTesting(): void {
|
|
111
126
|
networkChannel = createEventChannel<NetworkLogPayload>();
|
|
127
|
+
daemonEndpointBlacklist.splice(0, daemonEndpointBlacklist.length);
|
|
112
128
|
resetInterceptors();
|
|
113
129
|
}
|