@adeu/mcp-server 1.18.0 → 1.18.2
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/dist/index.js +136 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/verify-reasoning-order.mjs +113 -0
- package/src/index.ts +170 -11
- package/src/mcp.bugs.test.ts +10 -1
- package/src/mcp.cloud.test.ts +4 -3
- package/src/mcp.schema-gaps.test.ts +4 -0
- package/src/parity_live.test.ts +181 -95
- package/src/repro.feedback.test.ts +24 -7
- package/src/response-builders.ts +21 -5
package/src/parity_live.test.ts
CHANGED
|
@@ -6,7 +6,10 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
6
6
|
|
|
7
7
|
describe("Parity Live Server Integration Verification", () => {
|
|
8
8
|
let serverProc: ChildProcess;
|
|
9
|
-
const fixturePath = resolve(
|
|
9
|
+
const fixturePath = resolve(
|
|
10
|
+
__dirname,
|
|
11
|
+
"../tests/fixtures/gap2_minimal_repro.docx",
|
|
12
|
+
);
|
|
10
13
|
|
|
11
14
|
beforeAll(async () => {
|
|
12
15
|
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
@@ -76,86 +79,117 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
it("exposes the build stamp via serverInfo.version in initialize", async () => {
|
|
79
|
-
const initRes = await sendRpc(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
const initRes = await sendRpc(
|
|
83
|
+
"initialize",
|
|
84
|
+
{
|
|
85
|
+
protocolVersion: "2024-11-05",
|
|
86
|
+
capabilities: {},
|
|
87
|
+
clientInfo: { name: "test-client", version: "1.0.0" },
|
|
88
|
+
},
|
|
89
|
+
202,
|
|
90
|
+
);
|
|
84
91
|
expect(initRes.result).toBeDefined();
|
|
85
92
|
expect(initRes.result.serverInfo).toBeDefined();
|
|
86
93
|
expect(initRes.result.serverInfo.version).not.toBe("1.0.0");
|
|
87
94
|
expect(initRes.result.serverInfo.version).not.toContain("unknown");
|
|
88
95
|
});
|
|
89
96
|
|
|
90
|
-
|
|
91
97
|
it("read_docx appends [Debug] build stamp footer to mode=outline and mode=full", async () => {
|
|
92
|
-
const res = await sendRpc(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const res = await sendRpc(
|
|
99
|
+
"tools/call",
|
|
100
|
+
{
|
|
101
|
+
name: "read_docx",
|
|
102
|
+
arguments: {
|
|
103
|
+
reasoning: "test",
|
|
104
|
+
file_path: fixturePath,
|
|
105
|
+
mode: "outline",
|
|
106
|
+
},
|
|
97
107
|
},
|
|
98
|
-
|
|
108
|
+
202,
|
|
109
|
+
);
|
|
99
110
|
|
|
100
111
|
expect(res.result).toBeDefined();
|
|
101
112
|
expect(res.result.content[0].text).not.toContain("[Debug] build=");
|
|
102
113
|
});
|
|
103
114
|
|
|
104
115
|
it("GAP 2 (Live): process_document_batch modify straddling deleted text returns actionable deletion error", async () => {
|
|
105
|
-
const outPath = join(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
const outPath = join(
|
|
117
|
+
resolve(__dirname, "../../../../tmp"),
|
|
118
|
+
`live_gap2_out_${Date.now()}.docx`,
|
|
119
|
+
);
|
|
120
|
+
const res = await sendRpc(
|
|
121
|
+
"tools/call",
|
|
122
|
+
{
|
|
123
|
+
name: "process_document_batch",
|
|
124
|
+
arguments: {
|
|
125
|
+
reasoning: "test",
|
|
126
|
+
original_docx_path: fixturePath,
|
|
127
|
+
author_name: "Ed",
|
|
128
|
+
changes: [
|
|
129
|
+
{
|
|
130
|
+
type: "modify",
|
|
131
|
+
target_text: "Foo bar old phrase here.",
|
|
132
|
+
new_text: "X",
|
|
133
|
+
comment: "c",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
output_path: outPath,
|
|
137
|
+
dry_run: true,
|
|
138
|
+
},
|
|
121
139
|
},
|
|
122
|
-
|
|
140
|
+
203,
|
|
141
|
+
);
|
|
123
142
|
|
|
124
143
|
expect(res.result).toBeDefined();
|
|
125
144
|
const text = res.result.content[0].text;
|
|
126
|
-
expect(text).toContain(
|
|
127
|
-
|
|
145
|
+
expect(text).toContain(
|
|
146
|
+
"Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.",
|
|
147
|
+
);
|
|
148
|
+
expect(text).toContain(
|
|
149
|
+
"Reject/accept that change first or target the active replacement text instead.",
|
|
150
|
+
);
|
|
128
151
|
expect(text).not.toContain("Target text not found in document.");
|
|
129
152
|
|
|
130
153
|
// Run Python engine to assert cross-engine exact error parity
|
|
131
154
|
const { execSync } = await import("node:child_process");
|
|
132
155
|
const projectRoot = resolve(__dirname, "../../../..");
|
|
133
|
-
const pythonCli =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
const pythonCli =
|
|
157
|
+
process.platform === "win32"
|
|
158
|
+
? join(projectRoot, "python/.venv/Scripts/adeu.exe")
|
|
159
|
+
: join(projectRoot, "python/.venv/bin/adeu");
|
|
160
|
+
|
|
137
161
|
// Create temporary changes JSON for Python CLI
|
|
138
162
|
const fs = await import("node:fs");
|
|
139
163
|
const tempJsonPath = join(projectRoot, "tmp", `changes_${Date.now()}.json`);
|
|
140
164
|
fs.mkdirSync(join(projectRoot, "tmp"), { recursive: true });
|
|
141
|
-
fs.writeFileSync(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
165
|
+
fs.writeFileSync(
|
|
166
|
+
tempJsonPath,
|
|
167
|
+
JSON.stringify([
|
|
168
|
+
{
|
|
169
|
+
type: "modify",
|
|
170
|
+
target_text: "Foo bar old phrase here.",
|
|
171
|
+
new_text: "X",
|
|
172
|
+
comment: "c",
|
|
173
|
+
},
|
|
174
|
+
]),
|
|
175
|
+
);
|
|
149
176
|
|
|
150
177
|
try {
|
|
151
|
-
const pythonOut = execSync(
|
|
152
|
-
|
|
153
|
-
|
|
178
|
+
const pythonOut = execSync(
|
|
179
|
+
`"${pythonCli}" apply "${fixturePath}" "${tempJsonPath}" --dry-run --author Ed`,
|
|
180
|
+
{
|
|
181
|
+
encoding: "utf-8",
|
|
182
|
+
},
|
|
183
|
+
);
|
|
154
184
|
// Should not succeed normally since dry run exits with code 1 on errors, so we catch in the try block
|
|
155
185
|
} catch (err: any) {
|
|
156
186
|
const pyErrorOutput = err.stdout || err.stderr || "";
|
|
157
|
-
expect(pyErrorOutput).toContain(
|
|
158
|
-
|
|
187
|
+
expect(pyErrorOutput).toContain(
|
|
188
|
+
"Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.",
|
|
189
|
+
);
|
|
190
|
+
expect(pyErrorOutput).toContain(
|
|
191
|
+
"Reject/accept that change first or target the active replacement text instead.",
|
|
192
|
+
);
|
|
159
193
|
} finally {
|
|
160
194
|
if (fs.existsSync(tempJsonPath)) {
|
|
161
195
|
fs.unlinkSync(tempJsonPath);
|
|
@@ -164,34 +198,50 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
164
198
|
});
|
|
165
199
|
|
|
166
200
|
it("GAP 1 (Live): read_docx mode=outline heading count and content parity with Python", async () => {
|
|
167
|
-
const gap1FixturePath = resolve(
|
|
201
|
+
const gap1FixturePath = resolve(
|
|
202
|
+
__dirname,
|
|
203
|
+
"../tests/fixtures/gap1_deleted_row_repro.docx",
|
|
204
|
+
);
|
|
168
205
|
const { execSync } = await import("node:child_process");
|
|
169
206
|
const projectRoot = resolve(__dirname, "../../../..");
|
|
170
|
-
const pythonCli =
|
|
171
|
-
|
|
172
|
-
|
|
207
|
+
const pythonCli =
|
|
208
|
+
process.platform === "win32"
|
|
209
|
+
? join(projectRoot, "python/.venv/Scripts/adeu.exe")
|
|
210
|
+
: join(projectRoot, "python/.venv/bin/adeu");
|
|
173
211
|
|
|
174
212
|
// 1. clean_view = true
|
|
175
|
-
const nodeResClean = await sendRpc(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
213
|
+
const nodeResClean = await sendRpc(
|
|
214
|
+
"tools/call",
|
|
215
|
+
{
|
|
216
|
+
name: "read_docx",
|
|
217
|
+
arguments: {
|
|
218
|
+
reasoning: "test",
|
|
219
|
+
file_path: gap1FixturePath,
|
|
220
|
+
mode: "outline",
|
|
221
|
+
clean_view: true,
|
|
222
|
+
},
|
|
181
223
|
},
|
|
182
|
-
|
|
224
|
+
204,
|
|
225
|
+
);
|
|
183
226
|
|
|
184
227
|
expect(nodeResClean.result).toBeDefined();
|
|
185
228
|
const nodeTextClean = nodeResClean.result.content[0].text;
|
|
186
|
-
|
|
229
|
+
|
|
187
230
|
// Get Python output for clean_view = true
|
|
188
|
-
const pythonOutClean = execSync(
|
|
189
|
-
|
|
190
|
-
|
|
231
|
+
const pythonOutClean = execSync(
|
|
232
|
+
`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`,
|
|
233
|
+
{
|
|
234
|
+
encoding: "utf-8",
|
|
235
|
+
},
|
|
236
|
+
);
|
|
191
237
|
|
|
192
238
|
// Extract raw markdown headings (lines starting with #) to assert perfect parity
|
|
193
|
-
const getHeadings = (text: string) =>
|
|
194
|
-
|
|
239
|
+
const getHeadings = (text: string) =>
|
|
240
|
+
text
|
|
241
|
+
.split("\n")
|
|
242
|
+
.filter((line) => line.startsWith("#"))
|
|
243
|
+
.map((l) => l.trim());
|
|
244
|
+
|
|
195
245
|
const nodeHeadingsClean = getHeadings(nodeTextClean);
|
|
196
246
|
const pythonHeadingsClean = getHeadings(pythonOutClean);
|
|
197
247
|
|
|
@@ -200,56 +250,86 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
200
250
|
expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
|
|
201
251
|
|
|
202
252
|
// 2. clean_view = false
|
|
203
|
-
const nodeResDirty = await sendRpc(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
253
|
+
const nodeResDirty = await sendRpc(
|
|
254
|
+
"tools/call",
|
|
255
|
+
{
|
|
256
|
+
name: "read_docx",
|
|
257
|
+
arguments: {
|
|
258
|
+
reasoning: "test",
|
|
259
|
+
file_path: gap1FixturePath,
|
|
260
|
+
mode: "outline",
|
|
261
|
+
clean_view: false,
|
|
262
|
+
},
|
|
209
263
|
},
|
|
210
|
-
|
|
264
|
+
205,
|
|
265
|
+
);
|
|
211
266
|
|
|
212
267
|
expect(nodeResDirty.result).toBeDefined();
|
|
213
268
|
const nodeTextDirty = nodeResDirty.result.content[0].text;
|
|
214
269
|
|
|
215
|
-
const pythonOutDirty = execSync(
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
const pythonOutDirty = execSync(
|
|
271
|
+
`"${pythonCli}" extract "${gap1FixturePath}" --mode outline`,
|
|
272
|
+
{
|
|
273
|
+
encoding: "utf-8",
|
|
274
|
+
},
|
|
275
|
+
);
|
|
218
276
|
|
|
219
277
|
const nodeHeadingsDirty = getHeadings(nodeTextDirty);
|
|
220
278
|
const pythonHeadingsDirty = getHeadings(pythonOutDirty);
|
|
221
279
|
|
|
222
|
-
expect(nodeHeadingsDirty).toEqual([
|
|
223
|
-
|
|
280
|
+
expect(nodeHeadingsDirty).toEqual([
|
|
281
|
+
"# Active Heading (p1)",
|
|
282
|
+
"# Deleted Heading (p1)",
|
|
283
|
+
]);
|
|
284
|
+
expect(pythonHeadingsDirty).toEqual([
|
|
285
|
+
"# Active Heading (p1)",
|
|
286
|
+
"# Deleted Heading (p1)",
|
|
287
|
+
]);
|
|
224
288
|
expect(nodeHeadingsDirty).toEqual(pythonHeadingsDirty);
|
|
225
289
|
});
|
|
226
290
|
|
|
227
291
|
it("GAP 1 - Style Def (Live): style-definition outlineLvl on non-heading style classification parity", async () => {
|
|
228
|
-
const gap1FixturePath = resolve(
|
|
292
|
+
const gap1FixturePath = resolve(
|
|
293
|
+
__dirname,
|
|
294
|
+
"../tests/fixtures/gap1_minimal_repro.docx",
|
|
295
|
+
);
|
|
229
296
|
const { execSync } = await import("node:child_process");
|
|
230
297
|
const projectRoot = resolve(__dirname, "../../../..");
|
|
231
|
-
const pythonCli =
|
|
232
|
-
|
|
233
|
-
|
|
298
|
+
const pythonCli =
|
|
299
|
+
process.platform === "win32"
|
|
300
|
+
? join(projectRoot, "python/.venv/Scripts/adeu.exe")
|
|
301
|
+
: join(projectRoot, "python/.venv/bin/adeu");
|
|
234
302
|
|
|
235
303
|
// 1. clean_view = true (both engines must return exactly 2 headings, excluding Subtitle)
|
|
236
|
-
const nodeResClean = await sendRpc(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
304
|
+
const nodeResClean = await sendRpc(
|
|
305
|
+
"tools/call",
|
|
306
|
+
{
|
|
307
|
+
name: "read_docx",
|
|
308
|
+
arguments: {
|
|
309
|
+
reasoning: "test",
|
|
310
|
+
file_path: gap1FixturePath,
|
|
311
|
+
mode: "outline",
|
|
312
|
+
clean_view: true,
|
|
313
|
+
},
|
|
242
314
|
},
|
|
243
|
-
|
|
315
|
+
206,
|
|
316
|
+
);
|
|
244
317
|
|
|
245
318
|
expect(nodeResClean.result).toBeDefined();
|
|
246
319
|
const nodeTextClean = nodeResClean.result.content[0].text;
|
|
247
320
|
|
|
248
|
-
const pythonOutClean = execSync(
|
|
249
|
-
|
|
250
|
-
|
|
321
|
+
const pythonOutClean = execSync(
|
|
322
|
+
`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`,
|
|
323
|
+
{
|
|
324
|
+
encoding: "utf-8",
|
|
325
|
+
},
|
|
326
|
+
);
|
|
251
327
|
|
|
252
|
-
const getHeadings = (text: string) =>
|
|
328
|
+
const getHeadings = (text: string) =>
|
|
329
|
+
text
|
|
330
|
+
.split("\n")
|
|
331
|
+
.filter((line) => line.startsWith("#"))
|
|
332
|
+
.map((l) => l.trim());
|
|
253
333
|
|
|
254
334
|
const nodeHeadingsClean = getHeadings(nodeTextClean);
|
|
255
335
|
const pythonHeadingsClean = getHeadings(pythonOutClean);
|
|
@@ -258,8 +338,14 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
258
338
|
expect(nodeHeadingsClean.length).toBe(2);
|
|
259
339
|
expect(pythonHeadingsClean.length).toBe(2);
|
|
260
340
|
|
|
261
|
-
expect(nodeHeadingsClean).toEqual([
|
|
262
|
-
|
|
341
|
+
expect(nodeHeadingsClean).toEqual([
|
|
342
|
+
"# Real Heading One (p1)",
|
|
343
|
+
"# Real Heading Two (p1)",
|
|
344
|
+
]);
|
|
345
|
+
expect(pythonHeadingsClean).toEqual([
|
|
346
|
+
"# Real Heading One (p1)",
|
|
347
|
+
"# Real Heading Two (p1)",
|
|
348
|
+
]);
|
|
263
349
|
expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
|
|
264
350
|
});
|
|
265
351
|
});
|
|
@@ -57,14 +57,17 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
57
57
|
|
|
58
58
|
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
59
59
|
if (!existsSync(serverPath)) {
|
|
60
|
-
throw new Error(
|
|
60
|
+
throw new Error(
|
|
61
|
+
"MCP server not built. Run 'npm run build' before tests.",
|
|
62
|
+
);
|
|
61
63
|
}
|
|
62
64
|
serverProc = spawn("node", [serverPath]);
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
afterAll(() => {
|
|
66
68
|
if (serverProc && !serverProc.killed) serverProc.kill();
|
|
67
|
-
if (workDir && existsSync(workDir))
|
|
69
|
+
if (workDir && existsSync(workDir))
|
|
70
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
68
71
|
});
|
|
69
72
|
|
|
70
73
|
function sendRpc(method: string, params: any, id: number): Promise<any> {
|
|
@@ -105,9 +108,12 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
105
108
|
const r1 = await callTool(
|
|
106
109
|
"process_document_batch",
|
|
107
110
|
{
|
|
111
|
+
reasoning: "test",
|
|
108
112
|
original_docx_path: contractPath,
|
|
109
113
|
author_name: "Agent",
|
|
110
|
-
changes: [
|
|
114
|
+
changes: [
|
|
115
|
+
{ type: "modify", target_text: "Provider", new_text: "Supplier" },
|
|
116
|
+
],
|
|
111
117
|
},
|
|
112
118
|
201,
|
|
113
119
|
);
|
|
@@ -120,7 +126,10 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
120
126
|
// NOT back onto contract.docx / contract_processed.docx.
|
|
121
127
|
const r2 = await callTool(
|
|
122
128
|
"accept_all_changes",
|
|
123
|
-
{
|
|
129
|
+
{
|
|
130
|
+
reasoning: "test",
|
|
131
|
+
docx_path: join(workDir, "contract_processed.docx"),
|
|
132
|
+
},
|
|
124
133
|
202,
|
|
125
134
|
);
|
|
126
135
|
const msg2 = r2.result.content[0].text as string;
|
|
@@ -145,15 +154,20 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
145
154
|
const r = await callTool(
|
|
146
155
|
"process_document_batch",
|
|
147
156
|
{
|
|
157
|
+
reasoning: "test",
|
|
148
158
|
original_docx_path: join(workDir, "contract_processed.docx"),
|
|
149
159
|
author_name: "Agent",
|
|
150
|
-
changes: [
|
|
160
|
+
changes: [
|
|
161
|
+
{ type: "modify", target_text: "goods", new_text: "products" },
|
|
162
|
+
],
|
|
151
163
|
},
|
|
152
164
|
203,
|
|
153
165
|
);
|
|
154
166
|
const msg = r.result.content[0].text as string;
|
|
155
167
|
expect(savedFile(msg)).toBe("contract_processed.docx");
|
|
156
|
-
expect(existsSync(join(workDir, "contract_processed_processed.docx"))).toBe(
|
|
168
|
+
expect(existsSync(join(workDir, "contract_processed_processed.docx"))).toBe(
|
|
169
|
+
false,
|
|
170
|
+
);
|
|
157
171
|
});
|
|
158
172
|
|
|
159
173
|
it("Issue 1 (end-to-end via shipped server): accept + modify the same foreign insertion in one batch succeeds", async () => {
|
|
@@ -162,7 +176,9 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
162
176
|
const foreign = join(workDir, "lease.docx");
|
|
163
177
|
{
|
|
164
178
|
const doc = await DocumentObject.load(
|
|
165
|
-
readFileSync(
|
|
179
|
+
readFileSync(
|
|
180
|
+
resolve(__dirname, "../../../../shared/fixtures/initial.docx"),
|
|
181
|
+
),
|
|
166
182
|
);
|
|
167
183
|
const body = doc.element;
|
|
168
184
|
while (body.firstChild) body.removeChild(body.firstChild);
|
|
@@ -191,6 +207,7 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
191
207
|
const res = await callTool(
|
|
192
208
|
"process_document_batch",
|
|
193
209
|
{
|
|
210
|
+
reasoning: "test",
|
|
194
211
|
original_docx_path: foreign,
|
|
195
212
|
author_name: "Acme's Counsel",
|
|
196
213
|
output_path: join(workDir, "lease_out.docx"),
|
package/src/response-builders.ts
CHANGED
|
@@ -220,13 +220,27 @@ export function build_search_response(
|
|
|
220
220
|
const [body] = split_structural_appendix(text);
|
|
221
221
|
const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
222
222
|
const flags = search_case_sensitive ? "g" : "gi";
|
|
223
|
-
const patternStr = search_regex ? search_query : escapeRegExp(search_query);
|
|
224
223
|
|
|
224
|
+
// When the caller asked for a regex but supplied something the engine can't
|
|
225
|
+
// compile (e.g. an unterminated character class `\[`, or an inline-flag group
|
|
226
|
+
// `(?i)...` that JS RegExp rejects), do NOT hard-error and burn the turn.
|
|
227
|
+
// Downgrade to a literal search of the raw string and tell the model, so it
|
|
228
|
+
// can either accept the literal hits or fix its pattern — instead of retrying
|
|
229
|
+
// the same broken regex.
|
|
230
|
+
let regexDowngradedNote = "";
|
|
225
231
|
let regex: RegExp;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
if (search_regex) {
|
|
233
|
+
try {
|
|
234
|
+
regex = new RegExp(search_query, flags);
|
|
235
|
+
} catch (e: any) {
|
|
236
|
+
regexDowngradedNote =
|
|
237
|
+
`> **Note:** \`${search_query}\` is not a valid regular expression ` +
|
|
238
|
+
`(${e.message}), so it was searched as literal text instead. ` +
|
|
239
|
+
`If you meant a regex, fix the pattern; if you meant literal text, set \`search_regex\` to false.`;
|
|
240
|
+
regex = new RegExp(escapeRegExp(search_query), flags);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
regex = new RegExp(escapeRegExp(search_query), flags);
|
|
230
244
|
}
|
|
231
245
|
|
|
232
246
|
const allMatches = Array.from(body.matchAll(regex));
|
|
@@ -295,6 +309,7 @@ export function build_search_response(
|
|
|
295
309
|
} else {
|
|
296
310
|
body_msg = `> **Search Results** — No matches found for query \`${search_query}\` in \`${basename(file_path)}\`.\n\nVerify your search spelling, or try setting \`search_case_sensitive\` to false or enabling \`search_regex\` if you used pattern wildcards.`;
|
|
297
311
|
}
|
|
312
|
+
if (regexDowngradedNote) body_msg = `${regexDowngradedNote}\n\n${body_msg}`;
|
|
298
313
|
const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${body_msg}`;
|
|
299
314
|
return {
|
|
300
315
|
content: [{ type: "text", text: llm_content }],
|
|
@@ -415,6 +430,7 @@ export function build_search_response(
|
|
|
415
430
|
i++;
|
|
416
431
|
}
|
|
417
432
|
|
|
433
|
+
if (regexDowngradedNote) ui_parts.unshift(regexDowngradedNote);
|
|
418
434
|
const ui_markdown = ui_parts.join("\n\n");
|
|
419
435
|
const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
|
|
420
436
|
|