@jonathanleelx/meddle 0.5.0-beta.23 → 0.5.0-beta.25
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/CONFIG_STRUCTURE.md +1 -0
- package/README.md +16 -0
- package/bin/commands/route/active.js +14 -1
- package/bin/commands/route/add.js +112 -103
- package/bin/commands/route/delete.js +73 -49
- package/bin/commands/route/index.js +20 -2
- package/bin/commands/route/list.js +2 -2
- package/bin/commands/route/show.js +10 -1
- package/bin/commands/route/update.js +134 -101
- package/bin/index.js +3 -1
- package/bin/lib/api-client.js +14 -2
- package/bin/lib/file-access.js +39 -1
- package/bin/lib/route-write-args.js +150 -0
- package/bin/lib/rule-file-edit.js +263 -0
- package/bin/lib/ruleset-readonly.js +61 -0
- package/dist/core/mock-handler.d.ts.map +1 -1
- package/dist/core/mock-handler.js +34 -5
- package/dist/core/mock-handler.js.map +1 -1
- package/dist/core/rule-file-edit.d.ts +81 -0
- package/dist/core/rule-file-edit.d.ts.map +1 -0
- package/dist/core/rule-file-edit.js +243 -0
- package/dist/core/rule-file-edit.js.map +1 -0
- package/dist/core/static-server.d.ts.map +1 -1
- package/dist/core/static-server.js +15 -13
- package/dist/core/static-server.js.map +1 -1
- package/dist/server/agent/tools.d.ts.map +1 -1
- package/dist/server/agent/tools.js +20 -18
- package/dist/server/agent/tools.js.map +1 -1
- package/dist/server/app-info.d.ts +14 -0
- package/dist/server/app-info.d.ts.map +1 -1
- package/dist/server/app-info.js +33 -1
- package/dist/server/app-info.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +51 -9
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/rule-files.d.ts +23 -0
- package/dist/server/rule-files.d.ts.map +1 -1
- package/dist/server/rule-files.js +169 -81
- package/dist/server/rule-files.js.map +1 -1
- package/dist/server/ruleset-lock.d.ts +62 -0
- package/dist/server/ruleset-lock.d.ts.map +1 -0
- package/dist/server/ruleset-lock.js +234 -0
- package/dist/server/ruleset-lock.js.map +1 -0
- package/mcp-server.js +87 -88
- package/package.json +1 -1
- package/web/dist/assets/{detail-panel-D4JSaFH3.js → detail-panel-CyOc7gn8.js} +3 -3
- package/web/dist/assets/dist-BQN0rzUS.js +1 -0
- package/web/dist/assets/{global-panel-shell-DXvcxmbd.js → global-panel-shell-jXQTb9nw.js} +3 -3
- package/web/dist/assets/{health-panel-Bo0eD8_R.js → health-panel-CSV7OqBv.js} +1 -1
- package/web/dist/assets/index-C6dPxxT3.css +2 -0
- package/web/dist/assets/index-CSXxaYkG.js +21 -0
- package/web/dist/assets/{mobile-proxy-panel-FQwFQFmS.js → mobile-proxy-panel-a09S-R79.js} +2 -2
- package/web/dist/assets/mock-config-D_E5YOE_.js +1 -0
- package/web/dist/assets/{mock-editor-panel-kx0bzGv4.js → mock-editor-panel-t4TDXeaR.js} +3 -3
- package/web/dist/assets/{mocks-file-missing-note-BvL7-uzS.js → mocks-file-missing-note-Ddy1TdMX.js} +1 -1
- package/web/dist/assets/{monaco-editor-BLHN8gCU.js → monaco-editor-GpPLOKA5.js} +2 -2
- package/web/dist/assets/pane-bar-zJmE3xI5.js +1 -0
- package/web/dist/assets/{pane-split-DfQcgd1v.js → pane-split-CJ1VXJDy.js} +1 -1
- package/web/dist/assets/panel-heading-DlFWoYAR.js +1 -0
- package/web/dist/assets/plugin-code-editor-CtaamKWJ.js +2 -0
- package/web/dist/assets/plugin-config-Dg_FCb-L.js +2 -0
- package/web/dist/assets/{plugin-generator-B5FRaaKc.js → plugin-generator-CVREuZs7.js} +2 -2
- package/web/dist/assets/plugin-test-dialog-B-iV8ocm.js +7 -0
- package/web/dist/assets/route-preview-CLJeg_9v.js +1 -0
- package/web/dist/assets/{rule-ai-assistant-panel-BEmXPzli.js → rule-ai-assistant-panel-BvTqm4zV.js} +2 -2
- package/web/dist/assets/rule-config-DmNPaKxc.js +15 -0
- package/web/dist/assets/{rule-overview-panel-DsNEU8L1.js → rule-overview-panel-CB9b-b0t.js} +1 -1
- package/web/dist/assets/settings-panel-DLdeJ8ox.js +1 -0
- package/web/dist/assets/toast-9yeTf7d9.js +1 -0
- package/web/dist/assets/use-mocks-file-status-BUIUJ4oQ.js +1 -0
- package/web/dist/index.html +11 -9
- package/web/dist/assets/index-Cbis1y9h.js +0 -18
- package/web/dist/assets/index-dKaIrLee.css +0 -2
- package/web/dist/assets/mock-config-CL9nXIkx.js +0 -1
- package/web/dist/assets/pane-bar-BSzN6D93.js +0 -1
- package/web/dist/assets/panel-heading-Dev_soZA.js +0 -1
- package/web/dist/assets/plugin-code-editor-uI_-iAXY.js +0 -2
- package/web/dist/assets/plugin-config-BpfIX4Uk.js +0 -2
- package/web/dist/assets/plugin-test-dialog-Cptd2bs0.js +0 -7
- package/web/dist/assets/route-preview-DEc50Y_R.js +0 -1
- package/web/dist/assets/rule-config-DQkdF3tr.js +0 -15
- package/web/dist/assets/select-CFTzQ8Uy.js +0 -1
- package/web/dist/assets/settings-panel-C1v0UmZQ.js +0 -1
- /package/web/dist/assets/{alert-DFypnYT1.js → alert-B514Frte.js} +0 -0
- /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
- /package/web/dist/assets/{body-diff-view-CCwbvDYx.js → body-diff-view-SfLCLzs9.js} +0 -0
- /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
- /package/web/dist/assets/{dist-Dk9a2sJl.js → dist-DLQKRjuW.js} +0 -0
- /package/web/dist/assets/{editor-pane-actions-BGttNlkS.js → editor-pane-actions-_ckwcCII.js} +0 -0
- /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
- /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
- /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
- /package/web/dist/assets/{standalone-Bj8SSd8Q.js → standalone-DsnFnsqM.js} +0 -0
- /package/web/dist/assets/{syntax-highlight-Dg_CTbXd.js → syntax-highlight-BFgN_C0Y.js} +0 -0
- /package/web/dist/assets/{table-B10JkPhg.js → table-DaXD6jJv.js} +0 -0
- /package/web/dist/assets/{textarea-BjWF0pk4.js → textarea-9wB8Hsz-.js} +0 -0
- /package/web/dist/assets/{wand-sparkles-DDGjzjnX.js → wand-sparkles-Bmt4dDOf.js} +0 -0
- /package/web/dist/assets/{wifi-QkTcTrED.js → wifi-Cjj_Br1u.js} +0 -0
- /package/web/dist/assets/{zap-CEVoVkAg.js → zap-BuOtogAv.js} +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RulesetReadonlyError = exports.UI_TOKEN_META_NAME = exports.UI_TOKEN_HEADER = exports.READONLY_SETTINGS_KEY = exports.RULESET_READONLY_STATUS = exports.RULESET_READONLY_MESSAGE = exports.RULESET_READONLY = void 0;
|
|
37
|
+
exports.isRulesetReadonlyError = isRulesetReadonlyError;
|
|
38
|
+
exports.rulesetReadonlyBody = rulesetReadonlyBody;
|
|
39
|
+
exports.normalizeReadonlyNames = normalizeReadonlyNames;
|
|
40
|
+
exports.getReadonlyRuleFileNames = getReadonlyRuleFileNames;
|
|
41
|
+
exports.isRuleFileReadonly = isRuleFileReadonly;
|
|
42
|
+
exports.assertRuleFilesWritable = assertRuleFilesWritable;
|
|
43
|
+
exports.assertRuleFileWritable = assertRuleFileWritable;
|
|
44
|
+
exports.setRuleFileReadonly = setRuleFileReadonly;
|
|
45
|
+
exports.getUiToken = getUiToken;
|
|
46
|
+
exports.injectUiToken = injectUiToken;
|
|
47
|
+
exports.isSameOriginRequest = isSameOriginRequest;
|
|
48
|
+
exports.hasValidUiToken = hasValidUiToken;
|
|
49
|
+
const crypto = __importStar(require("crypto"));
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
/**
|
|
53
|
+
* Issue #137: per-ruleset (rule file) read-only lock.
|
|
54
|
+
*
|
|
55
|
+
* - Stored in settings.json under `readonlyRuleFiles` (file names without
|
|
56
|
+
* `.txt`), never inside the rule file. A missing field means every ruleset
|
|
57
|
+
* is read-write, so old settings need no migration.
|
|
58
|
+
* - Keyed by file name. A rename done outside Meddle makes a new, read-write
|
|
59
|
+
* ruleset; lock entries whose file no longer exists are dropped silently.
|
|
60
|
+
* - Every ruleset write goes through `assertRuleFilesWritable`, which throws
|
|
61
|
+
* `RulesetReadonlyError` (code `RULESET_READONLY`).
|
|
62
|
+
* - Only the web UI can lock / unlock: the request must carry the token the
|
|
63
|
+
* server put into the page (index.html meta / same-origin bootstrap) and pass
|
|
64
|
+
* a same-origin check. This guards against accidents, not against an attacker:
|
|
65
|
+
* anyone with a shell can GET the page, read the token, forge `Origin`, or
|
|
66
|
+
* simply edit the files on disk.
|
|
67
|
+
*/
|
|
68
|
+
exports.RULESET_READONLY = 'RULESET_READONLY';
|
|
69
|
+
exports.RULESET_READONLY_MESSAGE = '该规则集已设为只读,只能在 Meddle 界面中解锁';
|
|
70
|
+
/** HTTP status for a write against a locked ruleset (423 Locked). */
|
|
71
|
+
exports.RULESET_READONLY_STATUS = 423;
|
|
72
|
+
exports.READONLY_SETTINGS_KEY = 'readonlyRuleFiles';
|
|
73
|
+
exports.UI_TOKEN_HEADER = 'x-meddle-ui-token';
|
|
74
|
+
exports.UI_TOKEN_META_NAME = 'meddle-ui-token';
|
|
75
|
+
class RulesetReadonlyError extends Error {
|
|
76
|
+
constructor(ruleFile) {
|
|
77
|
+
super(exports.RULESET_READONLY_MESSAGE);
|
|
78
|
+
this.ruleFile = ruleFile;
|
|
79
|
+
this.code = exports.RULESET_READONLY;
|
|
80
|
+
this.status = exports.RULESET_READONLY_STATUS;
|
|
81
|
+
this.name = 'RulesetReadonlyError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.RulesetReadonlyError = RulesetReadonlyError;
|
|
85
|
+
function isRulesetReadonlyError(err) {
|
|
86
|
+
return err instanceof RulesetReadonlyError;
|
|
87
|
+
}
|
|
88
|
+
function rulesetReadonlyBody(err) {
|
|
89
|
+
return { error: err.message, code: err.code, ruleFile: err.ruleFile };
|
|
90
|
+
}
|
|
91
|
+
function readSettings(ctx) {
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(ctx.settingsPath)) {
|
|
94
|
+
return JSON.parse(fs.readFileSync(ctx.settingsPath, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch { /* ignore */ }
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
function writeSettings(ctx, settings) {
|
|
101
|
+
const dir = path.dirname(ctx.settingsPath);
|
|
102
|
+
if (!fs.existsSync(dir))
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(ctx.settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
105
|
+
}
|
|
106
|
+
/** Normalize the raw settings value; anything that is not a string array counts as empty. */
|
|
107
|
+
function normalizeReadonlyNames(value) {
|
|
108
|
+
if (!Array.isArray(value))
|
|
109
|
+
return [];
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const names = [];
|
|
112
|
+
for (const item of value) {
|
|
113
|
+
if (typeof item !== 'string' || !item || seen.has(item))
|
|
114
|
+
continue;
|
|
115
|
+
seen.add(item);
|
|
116
|
+
names.push(item);
|
|
117
|
+
}
|
|
118
|
+
return names;
|
|
119
|
+
}
|
|
120
|
+
function ruleFileExists(ctx, name) {
|
|
121
|
+
return fs.existsSync(path.join(ctx.meddleDir, 'route-rules', `${name}.txt`));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Locked rule file names. Entries whose file is gone (deleted or renamed
|
|
125
|
+
* outside Meddle) are removed from settings.json here, silently.
|
|
126
|
+
*/
|
|
127
|
+
function getReadonlyRuleFileNames(ctx) {
|
|
128
|
+
const settings = readSettings(ctx);
|
|
129
|
+
if (!(exports.READONLY_SETTINGS_KEY in settings))
|
|
130
|
+
return [];
|
|
131
|
+
const stored = normalizeReadonlyNames(settings[exports.READONLY_SETTINGS_KEY]);
|
|
132
|
+
const live = stored.filter((name) => ruleFileExists(ctx, name));
|
|
133
|
+
const raw = settings[exports.READONLY_SETTINGS_KEY];
|
|
134
|
+
const changed = !Array.isArray(raw) || live.length !== raw.length;
|
|
135
|
+
if (changed) {
|
|
136
|
+
settings[exports.READONLY_SETTINGS_KEY] = live;
|
|
137
|
+
writeSettings(ctx, settings);
|
|
138
|
+
}
|
|
139
|
+
return live;
|
|
140
|
+
}
|
|
141
|
+
function isRuleFileReadonly(ctx, name) {
|
|
142
|
+
return getReadonlyRuleFileNames(ctx).some((item) => fold(item) === fold(name));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* File names compare case-insensitively on macOS / Windows, where `Foo.txt`
|
|
146
|
+
* and `foo.txt` are the same file; the lock must not be dodged by changing case.
|
|
147
|
+
*/
|
|
148
|
+
const CASE_INSENSITIVE_FS = process.platform === 'darwin' || process.platform === 'win32';
|
|
149
|
+
const fold = (name) => (CASE_INSENSITIVE_FS ? name.toLowerCase() : name);
|
|
150
|
+
/** The common write gate: throws RulesetReadonlyError for the first locked name. */
|
|
151
|
+
function assertRuleFilesWritable(ctx, names) {
|
|
152
|
+
const locked = getReadonlyRuleFileNames(ctx);
|
|
153
|
+
if (locked.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
const byKey = new Map(locked.map((name) => [fold(name), name]));
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const hit = byKey.get(fold(name));
|
|
158
|
+
if (hit !== undefined)
|
|
159
|
+
throw new RulesetReadonlyError(hit);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function assertRuleFileWritable(ctx, name) {
|
|
163
|
+
assertRuleFilesWritable(ctx, [name]);
|
|
164
|
+
}
|
|
165
|
+
/** Lock / unlock. Only the token-protected UI route calls this. */
|
|
166
|
+
function setRuleFileReadonly(ctx, name, readonly) {
|
|
167
|
+
if (!ruleFileExists(ctx, name)) {
|
|
168
|
+
throw new Error('规则文件不存在');
|
|
169
|
+
}
|
|
170
|
+
const current = getReadonlyRuleFileNames(ctx);
|
|
171
|
+
const next = readonly
|
|
172
|
+
? (current.includes(name) ? current : [...current, name])
|
|
173
|
+
: current.filter((item) => item !== name);
|
|
174
|
+
const settings = readSettings(ctx);
|
|
175
|
+
settings[exports.READONLY_SETTINGS_KEY] = next;
|
|
176
|
+
writeSettings(ctx, settings);
|
|
177
|
+
return next;
|
|
178
|
+
}
|
|
179
|
+
// ---------- UI token + same-origin check ----------
|
|
180
|
+
const uiTokens = new WeakMap();
|
|
181
|
+
/** One random token per server instance; handed to the page, never to CLI/MCP. */
|
|
182
|
+
function getUiToken(ctx) {
|
|
183
|
+
let token = uiTokens.get(ctx);
|
|
184
|
+
if (!token) {
|
|
185
|
+
token = crypto.randomBytes(32).toString('hex');
|
|
186
|
+
uiTokens.set(ctx, token);
|
|
187
|
+
}
|
|
188
|
+
return token;
|
|
189
|
+
}
|
|
190
|
+
/** Put the token into index.html as `<meta name="meddle-ui-token">`. */
|
|
191
|
+
function injectUiToken(html, token) {
|
|
192
|
+
const meta = `<meta name="${exports.UI_TOKEN_META_NAME}" content="${token}">`;
|
|
193
|
+
if (html.includes('</head>'))
|
|
194
|
+
return html.replace('</head>', `${meta}</head>`);
|
|
195
|
+
return meta + html;
|
|
196
|
+
}
|
|
197
|
+
function headerValue(req, name) {
|
|
198
|
+
const value = req.headers[name];
|
|
199
|
+
if (Array.isArray(value))
|
|
200
|
+
return value[0] || '';
|
|
201
|
+
return typeof value === 'string' ? value : '';
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Same-origin check. Browsers send `Sec-Fetch-Site`; when present it must be
|
|
205
|
+
* `same-origin`. When `Origin` is present its host must equal `Host`. A request
|
|
206
|
+
* with neither header (plain curl / CLI / MCP fetch) is rejected.
|
|
207
|
+
*/
|
|
208
|
+
function isSameOriginRequest(req) {
|
|
209
|
+
const site = headerValue(req, 'sec-fetch-site');
|
|
210
|
+
const origin = headerValue(req, 'origin');
|
|
211
|
+
if (!site && !origin)
|
|
212
|
+
return false;
|
|
213
|
+
if (site && site !== 'same-origin')
|
|
214
|
+
return false;
|
|
215
|
+
if (origin) {
|
|
216
|
+
const host = headerValue(req, 'host');
|
|
217
|
+
try {
|
|
218
|
+
if (!host || new URL(origin).host !== host)
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
function hasValidUiToken(req, ctx) {
|
|
228
|
+
const provided = headerValue(req, exports.UI_TOKEN_HEADER);
|
|
229
|
+
const expected = getUiToken(ctx);
|
|
230
|
+
if (!provided || provided.length !== expected.length)
|
|
231
|
+
return false;
|
|
232
|
+
return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=ruleset-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruleset-lock.js","sourceRoot":"","sources":["../../server/ruleset-lock.ts"],"names":[],"mappings":"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"}
|
package/mcp-server.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
} = require('./bin/lib/sessions')
|
|
19
19
|
const { GLOBAL_DEFAULT_PORT } = require('./bin/lib/session-args')
|
|
20
20
|
const { buildProxySpawn, getReentryArgv } = require('./bin/lib/spawn-proxy')
|
|
21
|
+
const { addRouteRule, updateRouteRule, deleteRouteRule } = require('./bin/lib/rule-file-edit')
|
|
21
22
|
|
|
22
23
|
const meddleDir = resolveMeddleHome()
|
|
23
24
|
const mcpFile = path.join(meddleDir, 'mcp-proxy-url.json')
|
|
@@ -53,11 +54,55 @@ function proxyApiForSession(sessionId, method, pathname, body) {
|
|
|
53
54
|
const opts = { method, headers: { 'Content-Type': 'application/json' } }
|
|
54
55
|
if (body !== undefined) opts.body = typeof body === 'string' ? body : JSON.stringify(body)
|
|
55
56
|
return fetch(url, opts).then((res) => {
|
|
56
|
-
if (!res.ok) return res.text().then((t) => { throw
|
|
57
|
-
return res.headers.get('content-type')?.includes('application/json') ? res.json() : res
|
|
57
|
+
if (!res.ok) return res.text().then((t) => { throw apiError(res, t) })
|
|
58
|
+
return res.headers.get('content-type')?.includes('application/json') ? res.json() : readTextKeepBom(res)
|
|
58
59
|
})
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Issue #145: `Response.text()` strips a leading UTF-8 BOM, so a rule file read
|
|
64
|
+
* and written back through the API would silently lose it. Decode keeping it.
|
|
65
|
+
*/
|
|
66
|
+
function readTextKeepBom(res) {
|
|
67
|
+
return res.arrayBuffer().then((buf) => new TextDecoder('utf-8', { ignoreBOM: true }).decode(buf))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------- Issue #137: 只读规则集 ----------
|
|
71
|
+
// 锁只存在 settings.json,只能在 Meddle Web 界面中加锁 / 解锁;MCP 没有对应工具,只读取状态并在写入被拒时如实返回。
|
|
72
|
+
const RULESET_READONLY = 'RULESET_READONLY'
|
|
73
|
+
const RULESET_READONLY_MESSAGE = '该规则集已设为只读,只能在 Meddle 界面中解锁'
|
|
74
|
+
const READONLY_NOTE = '只读规则集会被拒绝(错误码 RULESET_READONLY,只能在 Meddle 界面中解锁)。'
|
|
75
|
+
|
|
76
|
+
/** 把代理 API 的错误响应转成 Error,保留结构化的 code / ruleFile。 */
|
|
77
|
+
function apiError(res, text) {
|
|
78
|
+
const err = new Error(text || res.statusText)
|
|
79
|
+
err.status = res.status
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(text)
|
|
82
|
+
if (data && typeof data === 'object') {
|
|
83
|
+
if (typeof data.code === 'string') err.code = data.code
|
|
84
|
+
if (typeof data.ruleFile === 'string') err.ruleFile = data.ruleFile
|
|
85
|
+
}
|
|
86
|
+
} catch (_) {}
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rulesetReadonlyResult(ruleFile) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: JSON.stringify({ error: { code: RULESET_READONLY, message: RULESET_READONLY_MESSAGE, ruleFile: ruleFile || null } }, null, 2)
|
|
95
|
+
}],
|
|
96
|
+
isError: true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 统一的错误返回:只读拒绝给出结构化结果,其它错误保持原样。 */
|
|
101
|
+
function toolError(e, ruleFile) {
|
|
102
|
+
if (e && e.code === RULESET_READONLY) return rulesetReadonlyResult(e.ruleFile || ruleFile)
|
|
103
|
+
return { content: [{ type: 'text', text: e.message }], isError: true }
|
|
104
|
+
}
|
|
105
|
+
|
|
61
106
|
const FILE_PATTERN = /^file:\/\//
|
|
62
107
|
const LOCAL_FILE_PATTERN = /^[A-Za-z]:\\|^\/|^\\/
|
|
63
108
|
|
|
@@ -109,37 +154,6 @@ function parseEprc(content) {
|
|
|
109
154
|
return parseEprcWithExclusions(content).ruleMap
|
|
110
155
|
}
|
|
111
156
|
|
|
112
|
-
/** 将 pattern -> target 对象转回规则文件文本(与 helpers.ruleMapToEprcText 一致,保留 exclusions) */
|
|
113
|
-
function ruleMapToEprcText(ruleMap, excludeMap) {
|
|
114
|
-
const entries = Object.entries(ruleMap)
|
|
115
|
-
if (entries.length === 0) return ''
|
|
116
|
-
const byTargetAndExclusions = {}
|
|
117
|
-
entries.forEach(([rule, target]) => {
|
|
118
|
-
const bm = target.match(/\[([^\]]+)\]/)
|
|
119
|
-
const groupKey = bm ? target.replace(bm[0], '') : target
|
|
120
|
-
const displayRule = bm ? rule.replace(bm[1], bm[0]) : rule
|
|
121
|
-
const exclusions = excludeMap?.[rule] || []
|
|
122
|
-
const exclusionsKey = exclusions.join(',')
|
|
123
|
-
// Create a compound key that includes both target and exclusions
|
|
124
|
-
const compoundKey = `${groupKey}|||${exclusionsKey}`
|
|
125
|
-
if (!byTargetAndExclusions[compoundKey]) {
|
|
126
|
-
byTargetAndExclusions[compoundKey] = { target: groupKey, rules: [], exclusions }
|
|
127
|
-
}
|
|
128
|
-
byTargetAndExclusions[compoundKey].rules.push(displayRule)
|
|
129
|
-
})
|
|
130
|
-
return Object.entries(byTargetAndExclusions)
|
|
131
|
-
.map(([, { target, rules, exclusions }]) => {
|
|
132
|
-
let displayTarget = target
|
|
133
|
-
if (FILE_PATTERN.test(target)) {
|
|
134
|
-
displayTarget = target.replace(/^file:\/\//, '').replace(/\//g, path.sep)
|
|
135
|
-
}
|
|
136
|
-
const exclusionStr = exclusions.map(e => `!${e}`).join(' ')
|
|
137
|
-
const rulesStr = rules.join(' ')
|
|
138
|
-
return exclusionStr ? `${rulesStr} ${exclusionStr} ${displayTarget}` : `${rulesStr} ${displayTarget}`
|
|
139
|
-
})
|
|
140
|
-
.join('\n')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
157
|
/** 请求代理 API */
|
|
144
158
|
function proxyApi(method, pathname, body) {
|
|
145
159
|
const base = getProxyBaseUrl()
|
|
@@ -147,9 +161,8 @@ function proxyApi(method, pathname, body) {
|
|
|
147
161
|
const opts = { method, headers: { 'Content-Type': 'application/json' } }
|
|
148
162
|
if (body !== undefined) opts.body = typeof body === 'string' ? body : JSON.stringify(body)
|
|
149
163
|
return fetch(url, opts).then((res) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return res.headers.get('content-type')?.includes('application/json') ? res.json() : text()
|
|
164
|
+
if (!res.ok) return res.text().then((t) => { throw new Error(t || res.statusText) })
|
|
165
|
+
return res.headers.get('content-type')?.includes('application/json') ? res.json() : readTextKeepBom(res)
|
|
153
166
|
})
|
|
154
167
|
}
|
|
155
168
|
|
|
@@ -395,7 +408,7 @@ mcpServer.registerTool('mock_rule_list', {
|
|
|
395
408
|
const rules = await proxyApiForSession(session, 'GET', '/api/mocks')
|
|
396
409
|
return { content: [{ type: 'text', text: JSON.stringify(rules, null, 2) }] }
|
|
397
410
|
} catch (e) {
|
|
398
|
-
return
|
|
411
|
+
return toolError(e)
|
|
399
412
|
}
|
|
400
413
|
})
|
|
401
414
|
|
|
@@ -435,7 +448,7 @@ mcpServer.registerTool('mock_rule_add', {
|
|
|
435
448
|
content: [{ type: 'text', text: `已添加 Mock 规则 id=${rule.id}: ${rule.name} (${rule.urlPattern})` }]
|
|
436
449
|
}
|
|
437
450
|
} catch (e) {
|
|
438
|
-
return
|
|
451
|
+
return toolError(e)
|
|
439
452
|
}
|
|
440
453
|
})
|
|
441
454
|
|
|
@@ -470,7 +483,7 @@ mcpServer.registerTool('mock_rule_update', {
|
|
|
470
483
|
content: [{ type: 'text', text: `已更新 Mock 规则 id=${id}: ${rule.name ?? '(未改)'}` }]
|
|
471
484
|
}
|
|
472
485
|
} catch (e) {
|
|
473
|
-
return
|
|
486
|
+
return toolError(e)
|
|
474
487
|
}
|
|
475
488
|
})
|
|
476
489
|
|
|
@@ -485,14 +498,14 @@ mcpServer.registerTool('mock_rule_delete', {
|
|
|
485
498
|
await proxyApiForSession(session, 'DELETE', `/api/mocks/${id}`)
|
|
486
499
|
return { content: [{ type: 'text', text: `已删除 Mock 规则 id=${id}` }] }
|
|
487
500
|
} catch (e) {
|
|
488
|
-
return
|
|
501
|
+
return toolError(e)
|
|
489
502
|
}
|
|
490
503
|
})
|
|
491
504
|
|
|
492
505
|
// ---------- 路由规则(可选 session 参数) ----------
|
|
493
506
|
|
|
494
507
|
mcpServer.registerTool('route_rule_list', {
|
|
495
|
-
description: '
|
|
508
|
+
description: '列出所有路由规则文件(每项含 readonly: true|false);可选指定 ruleFile 获取该文件下的规则列表(pattern -> target 及 exclusions,含 readonly)。readonly 只能在 Meddle 界面中修改,MCP 没有加锁/解锁工具。可选 session 参数指定操作的 session(不传则操作默认 session)。',
|
|
496
509
|
inputSchema: {
|
|
497
510
|
session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
|
|
498
511
|
ruleFile: z.string().optional().describe('规则文件名(不含 .txt),不传则只返回文件列表')
|
|
@@ -520,11 +533,13 @@ mcpServer.registerTool('route_rule_list', {
|
|
|
520
533
|
for (const exclusions of Object.values(excludeMap)) {
|
|
521
534
|
totalExclusions += exclusions.length
|
|
522
535
|
}
|
|
536
|
+
const info = Array.isArray(files) ? files.find((item) => item && item.name === ruleFile.trim()) : null
|
|
537
|
+
const readonly = Boolean(info && info.readonly)
|
|
523
538
|
return {
|
|
524
|
-
content: [{ type: 'text', text: JSON.stringify({ ruleFile, rules: displayRules, ruleCount: Object.keys(displayRules).length, exclusionCount: totalExclusions }, null, 2) }]
|
|
539
|
+
content: [{ type: 'text', text: JSON.stringify({ ruleFile, readonly, rules: displayRules, ruleCount: Object.keys(displayRules).length, exclusionCount: totalExclusions }, null, 2) }]
|
|
525
540
|
}
|
|
526
541
|
} catch (e) {
|
|
527
|
-
return
|
|
542
|
+
return toolError(e)
|
|
528
543
|
}
|
|
529
544
|
})
|
|
530
545
|
|
|
@@ -549,12 +564,12 @@ mcpServer.registerTool('route_rule_active_get', {
|
|
|
549
564
|
}]
|
|
550
565
|
}
|
|
551
566
|
} catch (e) {
|
|
552
|
-
return
|
|
567
|
+
return toolError(e)
|
|
553
568
|
}
|
|
554
569
|
})
|
|
555
570
|
|
|
556
571
|
mcpServer.registerTool('route_rule_active_set', {
|
|
557
|
-
description: '
|
|
572
|
+
description: '设置当前激活的路由规则文件。会改变启用状态的规则集中若有只读规则集则整体拒绝、不做任何修改:' + READONLY_NOTE + '可选 session 参数指定操作的 session(不传则操作默认 session)。',
|
|
558
573
|
inputSchema: {
|
|
559
574
|
session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
|
|
560
575
|
ruleFile: z.string().describe('要设为当前激活的规则文件名(不含 .txt)')
|
|
@@ -571,6 +586,10 @@ mcpServer.registerTool('route_rule_active_set', {
|
|
|
571
586
|
return { content: [{ type: 'text', text: `未找到规则文件: ${targetName}` }], isError: true }
|
|
572
587
|
}
|
|
573
588
|
|
|
589
|
+
// 先整体检查,避免锁住的规则集让切换只做了一半
|
|
590
|
+
const blocked = files.find((item) => item && item.readonly && !!item.enabled !== (item.name === targetName))
|
|
591
|
+
if (blocked) return rulesetReadonlyResult(blocked.name)
|
|
592
|
+
|
|
574
593
|
for (const item of files) {
|
|
575
594
|
const nextEnabled = item.name === targetName
|
|
576
595
|
if (!!item.enabled === nextEnabled) continue
|
|
@@ -585,12 +604,12 @@ mcpServer.registerTool('route_rule_active_set', {
|
|
|
585
604
|
}]
|
|
586
605
|
}
|
|
587
606
|
} catch (e) {
|
|
588
|
-
return
|
|
607
|
+
return toolError(e)
|
|
589
608
|
}
|
|
590
609
|
})
|
|
591
610
|
|
|
592
611
|
mcpServer.registerTool('route_rule_create_file', {
|
|
593
|
-
description: '
|
|
612
|
+
description: '创建新的路由规则文件(新文件默认可读写)。同名规则文件已存在时失败,不会覆盖已有规则集,因此只读规则集会被拒绝。可选 session 参数指定操作的 session(不传则操作默认 session)。',
|
|
594
613
|
inputSchema: {
|
|
595
614
|
session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
|
|
596
615
|
name: z.string().describe('规则文件名称(不含 .txt),如 dev、staging;非法字符会被替换为下划线'),
|
|
@@ -609,18 +628,18 @@ mcpServer.registerTool('route_rule_create_file', {
|
|
|
609
628
|
}]
|
|
610
629
|
}
|
|
611
630
|
} catch (e) {
|
|
612
|
-
return
|
|
631
|
+
return toolError(e)
|
|
613
632
|
}
|
|
614
633
|
})
|
|
615
634
|
|
|
616
635
|
mcpServer.registerTool('route_rule_add', {
|
|
617
|
-
description: '在指定规则文件中添加一条路由规则(pattern -> target)。若 pattern
|
|
636
|
+
description: '在指定规则文件中添加一条路由规则(pattern -> target)。若 pattern 已存在则覆盖其 target。exclusions 不传时保留该规则原有的排除规则(新规则无排除规则),传 [] 清空。只改这一行,文件中的注释、空行和其他规则保持原样。' + READONLY_NOTE + '可选 session 参数指定操作的 session(不传则操作默认 session)。',
|
|
618
637
|
inputSchema: {
|
|
619
638
|
session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
|
|
620
639
|
ruleFile: z.string().describe('规则文件名(不含 .txt)'),
|
|
621
640
|
pattern: z.string().describe('匹配完整请求 URL。支持:(1) 正则表达式,如 ^https://a\\.com/api; (2) 通配符,如 *.wps.cn 匹配 wps.cn、plus.wps.cn、deep.plus.wps.cn;(3) [marker] 路径重写,如 ^https://cdn.com/[assets] 将 assets 后的路径拼接到 target'),
|
|
622
641
|
target: z.string().describe('转发目标。若仅 host:port 则继承原请求的协议、pathname、query;若完整 URL 则直接使用;若原请求是 websocket 而 target 是 http(s),自动转为 ws(s)'),
|
|
623
|
-
exclusions: z.array(z.string()).optional().describe('排除规则列表,匹配完整 URL。若任一 exclusion 命中则跳过此规则继续匹配下一条。如 ["api/health", "^https://a\\.com/internal"]')
|
|
642
|
+
exclusions: z.array(z.string()).optional().describe('排除规则列表,匹配完整 URL。若任一 exclusion 命中则跳过此规则继续匹配下一条。如 ["api/health", "^https://a\\.com/internal"]。不传时保留该规则原有的排除规则(新规则无排除规则);传 [] 清空')
|
|
624
643
|
}
|
|
625
644
|
}, async ({ session, ruleFile, pattern, target, exclusions }) => {
|
|
626
645
|
try {
|
|
@@ -632,34 +651,25 @@ mcpServer.registerTool('route_rule_add', {
|
|
|
632
651
|
return { content: [{ type: 'text', text: `规则文件不存在或代理未启动: ${err.message}` }], isError: true }
|
|
633
652
|
}
|
|
634
653
|
const text = typeof content === 'string' ? content : String(content)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
639
|
-
if (bm) {
|
|
640
|
-
ruleMap[pat.replace(bm[0], bm[1])] = tgt + bm[0]
|
|
641
|
-
excludeMap[pat.replace(bm[0], bm[1])] = exclusions || []
|
|
642
|
-
} else {
|
|
643
|
-
ruleMap[pat] = tgt
|
|
644
|
-
excludeMap[pat] = exclusions || []
|
|
645
|
-
}
|
|
646
|
-
const newContent = ruleMapToEprcText(ruleMap, excludeMap)
|
|
654
|
+
// Issue #145: edit only this rule's line(s); other lines keep their exact bytes.
|
|
655
|
+
// Without `exclusions` an existing rule keeps its own exclusions.
|
|
656
|
+
const { content: newContent } = addRouteRule(text, { pattern: pattern.trim(), target: target.trim(), exclusions })
|
|
647
657
|
await proxyApiForSession(session, 'PUT', `/api/rule-files/${name}/content`, { content: newContent })
|
|
648
658
|
const exclusionInfo = exclusions && exclusions.length > 0 ? `,排除规则: ${exclusions.join(', ')}` : ''
|
|
649
659
|
return { content: [{ type: 'text', text: `已添加规则: ${pattern} -> ${target}${exclusionInfo}` }] }
|
|
650
660
|
} catch (e) {
|
|
651
|
-
return
|
|
661
|
+
return toolError(e)
|
|
652
662
|
}
|
|
653
663
|
})
|
|
654
664
|
|
|
655
665
|
mcpServer.registerTool('route_rule_update', {
|
|
656
|
-
description: '修改指定规则文件中某条规则的 target 和 exclusions(按 pattern
|
|
666
|
+
description: '修改指定规则文件中某条规则的 target 和 exclusions(按 pattern 查找)。exclusions 不传时保留该规则原有的排除规则,传 [] 清空。只改这一行,文件中的注释、空行和其他规则保持原样。' + READONLY_NOTE + '可选 session 参数指定操作的 session(不传则操作默认 session)。',
|
|
657
667
|
inputSchema: {
|
|
658
668
|
session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
|
|
659
669
|
ruleFile: z.string().describe('规则文件名(不含 .txt)'),
|
|
660
670
|
pattern: z.string().describe('要修改的 pattern(需与现有规则一致,支持带 [marker] 的写法)'),
|
|
661
671
|
newTarget: z.string().optional().describe('新的转发目标。若仅 host:port 则继承原请求的协议、pathname、query;若完整 URL 则直接使用'),
|
|
662
|
-
exclusions: z.array(z.string()).optional().describe('新的排除规则列表,匹配完整 URL
|
|
672
|
+
exclusions: z.array(z.string()).optional().describe('新的排除规则列表,匹配完整 URL。不传时保留该规则原有的排除规则;传 [] 清空')
|
|
663
673
|
}
|
|
664
674
|
}, async ({ session, ruleFile, pattern, newTarget, exclusions }) => {
|
|
665
675
|
try {
|
|
@@ -672,32 +682,24 @@ mcpServer.registerTool('route_rule_update', {
|
|
|
672
682
|
return { content: [{ type: 'text', text: `规则文件不存在或代理未启动: ${err.message}` }], isError: true }
|
|
673
683
|
}
|
|
674
684
|
const text = typeof content === 'string' ? content : String(content)
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
685
|
+
// Issue #145: edit only this rule's line(s); other lines keep their exact bytes.
|
|
686
|
+
const result = updateRouteRule(text, { pattern: pat, target: newTarget ? newTarget.trim() : undefined, exclusions })
|
|
687
|
+
if (!result.found) {
|
|
679
688
|
return { content: [{ type: 'text', text: `未找到 pattern: ${pat}` }], isError: true }
|
|
680
689
|
}
|
|
681
|
-
|
|
682
|
-
const tgt = newTarget.trim()
|
|
683
|
-
ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
|
|
684
|
-
}
|
|
685
|
-
if (exclusions !== undefined) {
|
|
686
|
-
excludeMap[internalKey] = exclusions
|
|
687
|
-
}
|
|
688
|
-
const newContent = ruleMapToEprcText(ruleMap, excludeMap)
|
|
690
|
+
const newContent = result.content
|
|
689
691
|
await proxyApiForSession(session, 'PUT', `/api/rule-files/${name}/content`, { content: newContent })
|
|
690
692
|
const targetInfo = newTarget ? `目标: ${newTarget}` : ''
|
|
691
693
|
const exclusionInfo = exclusions !== undefined ? (exclusions.length > 0 ? `排除规则: ${exclusions.join(', ')}` : '已清除排除规则') : ''
|
|
692
694
|
const parts = [targetInfo, exclusionInfo].filter(Boolean).join(', ')
|
|
693
695
|
return { content: [{ type: 'text', text: `已更新规则: ${pat}${parts ? ` (${parts})` : ''}` }] }
|
|
694
696
|
} catch (e) {
|
|
695
|
-
return
|
|
697
|
+
return toolError(e)
|
|
696
698
|
}
|
|
697
699
|
})
|
|
698
700
|
|
|
699
701
|
mcpServer.registerTool('route_rule_delete', {
|
|
700
|
-
description: '从指定规则文件中删除一条路由规则(按 pattern
|
|
702
|
+
description: '从指定规则文件中删除一条路由规则(按 pattern)。' + READONLY_NOTE + '可选 session 参数指定操作的 session(不传则操作默认 session)。',
|
|
701
703
|
inputSchema: {
|
|
702
704
|
session: z.string().optional().describe('目标 session id,不传则操作默认 session'),
|
|
703
705
|
ruleFile: z.string().describe('规则文件名(不含 .txt)'),
|
|
@@ -714,19 +716,16 @@ mcpServer.registerTool('route_rule_delete', {
|
|
|
714
716
|
return { content: [{ type: 'text', text: `规则文件不存在或代理未启动: ${err.message}` }], isError: true }
|
|
715
717
|
}
|
|
716
718
|
const text = typeof content === 'string' ? content : String(content)
|
|
717
|
-
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
719
|
+
// Issue #145: remove only this rule; other lines keep their exact bytes.
|
|
720
|
+
const result = deleteRouteRule(text, { pattern: pat })
|
|
721
|
+
if (!result.found) {
|
|
721
722
|
return { content: [{ type: 'text', text: `未找到 pattern: ${pat}` }], isError: true }
|
|
722
723
|
}
|
|
723
|
-
|
|
724
|
-
delete excludeMap[internalKey]
|
|
725
|
-
const newContent = ruleMapToEprcText(ruleMap, excludeMap)
|
|
724
|
+
const newContent = result.content
|
|
726
725
|
await proxyApiForSession(session, 'PUT', `/api/rule-files/${name}/content`, { content: newContent })
|
|
727
726
|
return { content: [{ type: 'text', text: `已删除规则: ${pat}` }] }
|
|
728
727
|
} catch (e) {
|
|
729
|
-
return
|
|
728
|
+
return toolError(e)
|
|
730
729
|
}
|
|
731
730
|
})
|
|
732
731
|
|
|
@@ -789,7 +788,7 @@ mcpServer.registerTool('route_preview', {
|
|
|
789
788
|
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
790
789
|
}
|
|
791
790
|
} catch (e) {
|
|
792
|
-
return
|
|
791
|
+
return toolError(e)
|
|
793
792
|
}
|
|
794
793
|
})
|
|
795
794
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonathanleelx/meddle",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.25",
|
|
4
4
|
"description": "A simple development proxy server with custom routing rules, mock responses, HTTPS interception, and a plugin system",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|