praxis-agent 0.55.3 → 0.57.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 +3 -1
- package/dist/application/session-service.js +53 -14
- package/dist/application/subagent-service.js +1 -0
- package/dist/cli/interactive.js +2 -2
- package/dist/cli/process-signal.d.ts +3 -0
- package/dist/cli/process-signal.js +5 -0
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +21 -2
- package/dist/cli-runtime.js +11 -1
- package/dist/cli.js +4 -2
- package/dist/evals/eval-contract.js +9 -1
- package/dist/mcp/praxis-mcp-server.js +13 -1
- package/dist/permissions/claude-permission-resolver.js +147 -4
- package/dist/platform/atomic-write.js +2 -0
- package/dist/tools/apply-patch.d.ts +25 -0
- package/dist/tools/apply-patch.js +128 -0
- package/dist/tools/claude-interactive-tools.js +8 -0
- package/dist/tools/claude-lsp-tool.d.ts +3 -0
- package/dist/tools/claude-lsp-tool.js +251 -26
- package/dist/tools/local-tools.d.ts +1 -0
- package/dist/tools/local-tools.js +102 -1
- package/dist/tools/lsp-diagnostics.d.ts +20 -0
- package/dist/tools/lsp-diagnostics.js +173 -0
- package/dist/tools/team-member-tools.js +5 -1
- package/package.json +2 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { countLineChanges } from './line-changes.js';
|
|
2
|
+
export const APPLY_PATCH_MAX_EDITS = 32;
|
|
3
|
+
export const APPLY_PATCH_MAX_FILES = 8;
|
|
4
|
+
export const APPLY_PATCH_MAX_INPUT_BYTES = 256 * 1024;
|
|
5
|
+
export const APPLY_PATCH_DEFINITION = {
|
|
6
|
+
name: 'ApplyPatch',
|
|
7
|
+
description: 'Applies a bounded batch of exact, unique string replacements to existing files.',
|
|
8
|
+
inputSchema: {
|
|
9
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
edits: {
|
|
13
|
+
type: 'array',
|
|
14
|
+
minItems: 1,
|
|
15
|
+
maxItems: APPLY_PATCH_MAX_EDITS,
|
|
16
|
+
items: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
file_path: { type: 'string', minLength: 1 },
|
|
20
|
+
old_string: { type: 'string', minLength: 1 },
|
|
21
|
+
new_string: { type: 'string' },
|
|
22
|
+
},
|
|
23
|
+
required: ['file_path', 'old_string', 'new_string'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ['edits'],
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
function objectInput(input, label) {
|
|
33
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
34
|
+
throw new Error(`${label} must be an object`);
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
function exactKeys(input, keys) {
|
|
38
|
+
const unexpected = Object.keys(input).find((key) => !keys.includes(key));
|
|
39
|
+
if (unexpected)
|
|
40
|
+
throw new Error(`Unexpected ApplyPatch field: ${unexpected}`);
|
|
41
|
+
}
|
|
42
|
+
function stringInput(input, key, nonEmpty = false) {
|
|
43
|
+
const value = input[key];
|
|
44
|
+
if (typeof value !== 'string' || (nonEmpty && value.length === 0))
|
|
45
|
+
throw new Error(`${key} must be ${nonEmpty ? 'a non-empty ' : 'a '}string`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
export function parseApplyPatchInput(input) {
|
|
49
|
+
let encoded;
|
|
50
|
+
try {
|
|
51
|
+
encoded = JSON.stringify(input);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error('ApplyPatch input must be JSON-serializable');
|
|
55
|
+
}
|
|
56
|
+
if (encoded === undefined)
|
|
57
|
+
throw new Error('ApplyPatch input must be JSON-serializable');
|
|
58
|
+
if (Buffer.byteLength(encoded, 'utf8') > APPLY_PATCH_MAX_INPUT_BYTES)
|
|
59
|
+
throw new Error('ApplyPatch input exceeds 256 KiB');
|
|
60
|
+
const object = objectInput(input, 'ApplyPatch input');
|
|
61
|
+
exactKeys(object, ['edits']);
|
|
62
|
+
const rawEdits = object.edits;
|
|
63
|
+
if (!Array.isArray(rawEdits) ||
|
|
64
|
+
rawEdits.length < 1 ||
|
|
65
|
+
rawEdits.length > APPLY_PATCH_MAX_EDITS)
|
|
66
|
+
throw new Error('ApplyPatch edits must contain 1 to 32 operations');
|
|
67
|
+
return rawEdits.map((raw, index) => {
|
|
68
|
+
const item = objectInput(raw, `ApplyPatch edits[${index}]`);
|
|
69
|
+
exactKeys(item, ['file_path', 'old_string', 'new_string']);
|
|
70
|
+
const edit = {
|
|
71
|
+
file_path: stringInput(item, 'file_path', true),
|
|
72
|
+
old_string: stringInput(item, 'old_string', true),
|
|
73
|
+
new_string: stringInput(item, 'new_string'),
|
|
74
|
+
};
|
|
75
|
+
if (edit.old_string === edit.new_string)
|
|
76
|
+
throw new Error('new_string must differ from old_string');
|
|
77
|
+
return edit;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export function planApplyPatch(edits, sources, maxFileBytes) {
|
|
81
|
+
if (edits.length < 1 || edits.length > APPLY_PATCH_MAX_EDITS)
|
|
82
|
+
throw new Error('ApplyPatch edits must contain 1 to 32 operations');
|
|
83
|
+
if (new Set(edits.map((edit) => edit.file_path)).size > APPLY_PATCH_MAX_FILES)
|
|
84
|
+
throw new Error('ApplyPatch may touch at most 8 files');
|
|
85
|
+
const snapshots = new Map();
|
|
86
|
+
for (const edit of edits) {
|
|
87
|
+
if (!edit.old_string)
|
|
88
|
+
throw new Error('old_string must be non-empty');
|
|
89
|
+
if (edit.old_string === edit.new_string)
|
|
90
|
+
throw new Error('new_string must differ from old_string');
|
|
91
|
+
if (!snapshots.has(edit.file_path)) {
|
|
92
|
+
const source = sources.get(edit.file_path);
|
|
93
|
+
if (source === undefined)
|
|
94
|
+
throw new Error(`ApplyPatch source is missing: ${edit.file_path}`);
|
|
95
|
+
snapshots.set(edit.file_path, source);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const edit of edits) {
|
|
99
|
+
const source = snapshots.get(edit.file_path);
|
|
100
|
+
if (source === undefined)
|
|
101
|
+
throw new Error(`ApplyPatch source is missing: ${edit.file_path}`);
|
|
102
|
+
const first = source.indexOf(edit.old_string);
|
|
103
|
+
if (first < 0)
|
|
104
|
+
throw new Error('old_string was not found');
|
|
105
|
+
const second = source.indexOf(edit.old_string, first + edit.old_string.length);
|
|
106
|
+
if (second >= 0)
|
|
107
|
+
throw new Error('old_string must match exactly once');
|
|
108
|
+
const after = source.slice(0, first) +
|
|
109
|
+
edit.new_string +
|
|
110
|
+
source.slice(first + edit.old_string.length);
|
|
111
|
+
if (Buffer.byteLength(after, 'utf8') > maxFileBytes)
|
|
112
|
+
throw new Error(`Edited content exceeds ${maxFileBytes} bytes`);
|
|
113
|
+
snapshots.set(edit.file_path, after);
|
|
114
|
+
}
|
|
115
|
+
const files = [...snapshots].map(([filePath, after]) => {
|
|
116
|
+
const before = sources.get(filePath);
|
|
117
|
+
if (before === undefined)
|
|
118
|
+
throw new Error(`ApplyPatch source is missing: ${filePath}`);
|
|
119
|
+
const changes = countLineChanges(before, after);
|
|
120
|
+
return { filePath, before, after, ...changes };
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
files,
|
|
124
|
+
linesAdded: files.reduce((total, file) => total + file.linesAdded, 0),
|
|
125
|
+
linesRemoved: files.reduce((total, file) => total + file.linesRemoved, 0),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=apply-patch.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
4
|
+
import { parseApplyPatchInput } from './apply-patch.js';
|
|
4
5
|
const ASK_USER_QUESTION = {
|
|
5
6
|
name: 'AskUserQuestion',
|
|
6
7
|
description: "Use this tool only when you are blocked on a decision that is genuinely the user's to make: one you cannot resolve from the request, the code, or sensible defaults. Users can always provide custom text. Use multiSelect for non-exclusive choices. In plan mode, use this tool to clarify requirements before ExitPlanMode; do not use it to request plan approval.",
|
|
@@ -354,6 +355,13 @@ Use AskUserQuestion for decisions that genuinely require the user. Write a compl
|
|
|
354
355
|
typeof call.input.file_path === 'string') {
|
|
355
356
|
return this.resolvePlanFile(state, call, context);
|
|
356
357
|
}
|
|
358
|
+
if (state.mode === 'plan' && call.name === 'ApplyPatch') {
|
|
359
|
+
const edits = parseApplyPatchInput(call.input);
|
|
360
|
+
if ((await Promise.all(edits.map((edit) => this.isPlanFileForState(state, edit.file_path)))).every(Boolean)) {
|
|
361
|
+
return { behavior: 'allow' };
|
|
362
|
+
}
|
|
363
|
+
return this.resolver(state.mode).resolve(call, context);
|
|
364
|
+
}
|
|
357
365
|
const decision = await this.resolver(this.planPermissionMode(sessionId)).resolve(call, context);
|
|
358
366
|
return this.applyRetainedPrompts(state, call, decision);
|
|
359
367
|
}
|
|
@@ -12,11 +12,14 @@ export declare class ClaudeLspToolManager {
|
|
|
12
12
|
environment?: Readonly<Record<string, string | undefined>>;
|
|
13
13
|
});
|
|
14
14
|
registry(base: ToolRegistry): ToolRegistry;
|
|
15
|
+
private discardConnectionsOutside;
|
|
16
|
+
private runtimeCwd;
|
|
15
17
|
private validatedFile;
|
|
16
18
|
private candidates;
|
|
17
19
|
private languageId;
|
|
18
20
|
private connection;
|
|
19
21
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
22
|
+
enrichMutationResult(call: ModelToolCall, context: ToolExecutionContext, result: ToolExecutionResult): Promise<ToolExecutionResult>;
|
|
20
23
|
close(): Promise<void>;
|
|
21
24
|
}
|
|
22
25
|
//# sourceMappingURL=claude-lsp-tool.d.ts.map
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { execFile, spawn, } from 'node:child_process';
|
|
2
2
|
import { readFile, realpath, stat } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { extname, resolve, sep } from 'node:path';
|
|
4
|
+
import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
8
8
|
import { redactSensitiveText, sanitizeChildEnvironment, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
9
9
|
import { formatClaudeLspResult } from './claude-lsp-formatters.js';
|
|
10
|
+
import { formatDiagnostics, parsePublishDiagnostics, } from './lsp-diagnostics.js';
|
|
10
11
|
const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
11
12
|
const MAX_RESULT_CHARS = 512 * 1024;
|
|
12
13
|
const MAX_FILE_BYTES = 10_000_000;
|
|
13
14
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
15
|
+
const DIAGNOSTICS_TIMEOUT_MS = 1_000;
|
|
14
16
|
const execFileAsync = promisify(execFile);
|
|
15
17
|
const NO_CALL_HIERARCHY = Symbol('no-call-hierarchy');
|
|
16
18
|
function lspSensitiveValues(definition) {
|
|
@@ -224,6 +226,9 @@ class LspConnection {
|
|
|
224
226
|
buffer = Buffer.alloc(0);
|
|
225
227
|
pending = new Map();
|
|
226
228
|
documents = new Map();
|
|
229
|
+
diagnosticSnapshots = new Map();
|
|
230
|
+
diagnosticWaiters = new Set();
|
|
231
|
+
diagnosticGeneration = 0;
|
|
227
232
|
stderr = '';
|
|
228
233
|
exited = false;
|
|
229
234
|
initialized = false;
|
|
@@ -253,6 +258,20 @@ class LspConnection {
|
|
|
253
258
|
this.cleanupPending(id, pending);
|
|
254
259
|
pending.reject(error);
|
|
255
260
|
}
|
|
261
|
+
for (const waiter of [...this.diagnosticWaiters])
|
|
262
|
+
this.settleDiagnosticWaiter(waiter, undefined, error);
|
|
263
|
+
this.diagnosticSnapshots.clear();
|
|
264
|
+
}
|
|
265
|
+
settleDiagnosticWaiter(waiter, diagnostics, error) {
|
|
266
|
+
if (!this.diagnosticWaiters.delete(waiter))
|
|
267
|
+
return;
|
|
268
|
+
clearTimeout(waiter.timer);
|
|
269
|
+
if (waiter.signal && waiter.abort)
|
|
270
|
+
waiter.signal.removeEventListener('abort', waiter.abort);
|
|
271
|
+
if (error)
|
|
272
|
+
waiter.reject(error);
|
|
273
|
+
else
|
|
274
|
+
waiter.resolve(diagnostics ?? []);
|
|
256
275
|
}
|
|
257
276
|
cleanupPending(id, pending) {
|
|
258
277
|
clearTimeout(pending.timer);
|
|
@@ -306,6 +325,29 @@ class LspConnection {
|
|
|
306
325
|
if (!message || typeof message !== 'object' || Array.isArray(message))
|
|
307
326
|
return;
|
|
308
327
|
const value = message;
|
|
328
|
+
const publication = parsePublishDiagnostics(message);
|
|
329
|
+
if (publication) {
|
|
330
|
+
const document = [...this.documents.values()].find((candidate) => candidate.uri === publication.uri);
|
|
331
|
+
if (!document)
|
|
332
|
+
return;
|
|
333
|
+
if (publication.version !== undefined &&
|
|
334
|
+
publication.version !== document.version)
|
|
335
|
+
return;
|
|
336
|
+
const generation = ++this.diagnosticGeneration;
|
|
337
|
+
this.diagnosticSnapshots.set(publication.uri, {
|
|
338
|
+
generation,
|
|
339
|
+
diagnostics: publication.diagnostics,
|
|
340
|
+
});
|
|
341
|
+
const snapshot = this.diagnosticSnapshots.get(publication.uri);
|
|
342
|
+
for (const waiter of [...this.diagnosticWaiters]) {
|
|
343
|
+
if (waiter.uri !== publication.uri ||
|
|
344
|
+
!snapshot ||
|
|
345
|
+
snapshot.generation <= waiter.afterGeneration)
|
|
346
|
+
continue;
|
|
347
|
+
this.settleDiagnosticWaiter(waiter, snapshot.diagnostics);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
309
351
|
if (value.id !== undefined && typeof value.method === 'string') {
|
|
310
352
|
const result = value.method === 'workspace/configuration' &&
|
|
311
353
|
value.params &&
|
|
@@ -444,7 +486,7 @@ class LspConnection {
|
|
|
444
486
|
publishDiagnostics: {
|
|
445
487
|
relatedInformation: true,
|
|
446
488
|
tagSupport: { valueSet: [1, 2] },
|
|
447
|
-
versionSupport:
|
|
489
|
+
versionSupport: true,
|
|
448
490
|
codeDescriptionSupport: true,
|
|
449
491
|
dataSupport: false,
|
|
450
492
|
},
|
|
@@ -472,25 +514,82 @@ class LspConnection {
|
|
|
472
514
|
}
|
|
473
515
|
this.initialized = true;
|
|
474
516
|
}
|
|
475
|
-
|
|
476
|
-
const text = await readFile(filePath, 'utf8');
|
|
517
|
+
synchronizeDocument(filePath, languageId, text) {
|
|
477
518
|
const current = this.documents.get(filePath);
|
|
478
519
|
const uri = pathToFileURL(filePath).href;
|
|
520
|
+
if (current?.text === text)
|
|
521
|
+
return false;
|
|
479
522
|
if (!current) {
|
|
480
|
-
this.documents.set(filePath, { text, version: 1 });
|
|
523
|
+
this.documents.set(filePath, { text, version: 1, uri });
|
|
481
524
|
this.notify('textDocument/didOpen', {
|
|
482
525
|
textDocument: { uri, languageId, version: 1, text },
|
|
483
526
|
});
|
|
484
|
-
return;
|
|
485
527
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
528
|
+
else {
|
|
529
|
+
const version = current.version + 1;
|
|
530
|
+
this.documents.set(filePath, { text, version, uri });
|
|
531
|
+
this.notify('textDocument/didChange', {
|
|
532
|
+
textDocument: { uri, version },
|
|
533
|
+
contentChanges: [{ text }],
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
async openDocument(filePath, languageId) {
|
|
539
|
+
const text = await readFile(filePath, 'utf8');
|
|
540
|
+
this.synchronizeDocument(filePath, languageId, text);
|
|
541
|
+
}
|
|
542
|
+
async refreshDiagnostics(filePath, languageId, signal) {
|
|
543
|
+
if (signal?.aborted)
|
|
544
|
+
return [];
|
|
545
|
+
const text = await readFile(filePath, 'utf8');
|
|
546
|
+
if (signal?.aborted)
|
|
547
|
+
return [];
|
|
548
|
+
const uri = pathToFileURL(filePath).href;
|
|
549
|
+
const current = this.documents.get(filePath);
|
|
550
|
+
if (current && current.text === text)
|
|
551
|
+
return [];
|
|
552
|
+
const afterGeneration = this.diagnosticSnapshots.get(uri)?.generation ?? 0;
|
|
553
|
+
let waiter;
|
|
554
|
+
const result = new Promise((resolveResult, rejectResult) => {
|
|
555
|
+
const entry = {
|
|
556
|
+
uri,
|
|
557
|
+
afterGeneration,
|
|
558
|
+
resolve: resolveResult,
|
|
559
|
+
reject: rejectResult,
|
|
560
|
+
timer: setTimeout(() => {
|
|
561
|
+
this.settleDiagnosticWaiter(entry);
|
|
562
|
+
}, 1_000),
|
|
563
|
+
...(signal ? { signal } : {}),
|
|
564
|
+
};
|
|
565
|
+
waiter = entry;
|
|
566
|
+
if (signal) {
|
|
567
|
+
entry.abort = () => {
|
|
568
|
+
this.settleDiagnosticWaiter(entry, undefined, new Error('LSP diagnostics refresh cancelled'));
|
|
569
|
+
};
|
|
570
|
+
if (signal.aborted) {
|
|
571
|
+
clearTimeout(entry.timer);
|
|
572
|
+
resolveResult([]);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
signal.addEventListener('abort', entry.abort, { once: true });
|
|
576
|
+
}
|
|
577
|
+
this.diagnosticWaiters.add(entry);
|
|
493
578
|
});
|
|
579
|
+
if (signal?.aborted)
|
|
580
|
+
return result;
|
|
581
|
+
try {
|
|
582
|
+
this.synchronizeDocument(filePath, languageId, text);
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
if (waiter) {
|
|
586
|
+
// The method throws the send failure directly. Resolve the detached
|
|
587
|
+
// waiter while cleaning it so it cannot become an unhandled rejection.
|
|
588
|
+
this.settleDiagnosticWaiter(waiter);
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
return result.catch(() => []);
|
|
494
593
|
}
|
|
495
594
|
async execute(input, filePath, signal) {
|
|
496
595
|
const uri = pathToFileURL(filePath).href;
|
|
@@ -527,6 +626,7 @@ class LspConnection {
|
|
|
527
626
|
async close() {
|
|
528
627
|
if (this.exited)
|
|
529
628
|
return;
|
|
629
|
+
this.fail(new Error(`LSP server ${this.definition.name} is closing`));
|
|
530
630
|
const exited = new Promise((resolveExit) => {
|
|
531
631
|
this.child.once('close', () => resolveExit());
|
|
532
632
|
});
|
|
@@ -567,6 +667,33 @@ export class ClaudeLspToolManager {
|
|
|
567
667
|
registry(base) {
|
|
568
668
|
return new ClaudeLspToolRegistry(base, this);
|
|
569
669
|
}
|
|
670
|
+
async discardConnectionsOutside(runtimeCwd) {
|
|
671
|
+
const prefix = runtimeCwd === undefined ? undefined : `${runtimeCwd}\0`;
|
|
672
|
+
const staleKeys = new Set([
|
|
673
|
+
...this.connections.keys(),
|
|
674
|
+
...this.initializing.keys(),
|
|
675
|
+
...this.restartCounts.keys(),
|
|
676
|
+
].filter((key) => prefix === undefined || !key.startsWith(prefix)));
|
|
677
|
+
const staleConnections = [];
|
|
678
|
+
for (const key of staleKeys) {
|
|
679
|
+
const connection = this.connections.get(key);
|
|
680
|
+
if (connection)
|
|
681
|
+
staleConnections.push(connection);
|
|
682
|
+
this.connections.delete(key);
|
|
683
|
+
this.initializing.delete(key);
|
|
684
|
+
this.restartCounts.delete(key);
|
|
685
|
+
}
|
|
686
|
+
await Promise.allSettled(staleConnections.map((connection) => connection.close()));
|
|
687
|
+
}
|
|
688
|
+
async runtimeCwd() {
|
|
689
|
+
try {
|
|
690
|
+
return await realpath(this.options.cwdProvider());
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
await this.discardConnectionsOutside();
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
570
697
|
async validatedFile(inputPath) {
|
|
571
698
|
const cwd = this.options.cwdProvider();
|
|
572
699
|
const expanded = inputPath === '~'
|
|
@@ -601,16 +728,23 @@ export class ClaudeLspToolManager {
|
|
|
601
728
|
const extension = extname(filePath).toLowerCase();
|
|
602
729
|
return (Object.entries(definition.extensionToLanguage).find(([candidate]) => candidate.toLowerCase() === extension)?.[1] ?? '');
|
|
603
730
|
}
|
|
604
|
-
async connection(definition, signal) {
|
|
605
|
-
const runtimeCwd = await
|
|
731
|
+
async connection(definition, signal, expectedCwd) {
|
|
732
|
+
const runtimeCwd = await this.runtimeCwd();
|
|
733
|
+
if (expectedCwd !== undefined && runtimeCwd !== expectedCwd) {
|
|
734
|
+
await this.discardConnectionsOutside(runtimeCwd);
|
|
735
|
+
throw new Error('LSP diagnostics cwd changed during collection');
|
|
736
|
+
}
|
|
737
|
+
if (signal?.aborted)
|
|
738
|
+
throw new Error('LSP connection cancelled');
|
|
606
739
|
const serverCwd = await realpath(definition.workspaceFolder ?? runtimeCwd);
|
|
607
740
|
const key = `${runtimeCwd}\0${definition.pluginName}\0${definition.name}`;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
await
|
|
741
|
+
await this.discardConnectionsOutside(runtimeCwd);
|
|
742
|
+
if (signal?.aborted)
|
|
743
|
+
throw new Error('LSP connection cancelled');
|
|
744
|
+
const confirmedCwd = await this.runtimeCwd();
|
|
745
|
+
if (expectedCwd !== undefined && confirmedCwd !== expectedCwd) {
|
|
746
|
+
await this.discardConnectionsOutside(confirmedCwd);
|
|
747
|
+
throw new Error('LSP diagnostics cwd changed during collection');
|
|
614
748
|
}
|
|
615
749
|
const initializing = this.initializing.get(key);
|
|
616
750
|
if (initializing)
|
|
@@ -657,7 +791,7 @@ export class ClaudeLspToolManager {
|
|
|
657
791
|
async execute(call, context) {
|
|
658
792
|
try {
|
|
659
793
|
const input = inputFrom(call.input);
|
|
660
|
-
const cwd = await
|
|
794
|
+
const cwd = await this.runtimeCwd();
|
|
661
795
|
const { filePath, canonicalPath, size } = await this.validatedFile(input.filePath);
|
|
662
796
|
if (size > MAX_FILE_BYTES) {
|
|
663
797
|
return {
|
|
@@ -710,6 +844,96 @@ export class ClaudeLspToolManager {
|
|
|
710
844
|
};
|
|
711
845
|
}
|
|
712
846
|
}
|
|
847
|
+
async enrichMutationResult(call, context, result) {
|
|
848
|
+
if (result.isError || (call.name !== 'Edit' && call.name !== 'ApplyPatch'))
|
|
849
|
+
return result;
|
|
850
|
+
if (context.signal?.aborted)
|
|
851
|
+
return result;
|
|
852
|
+
const deadline = new AbortController();
|
|
853
|
+
let resolveInterrupted;
|
|
854
|
+
const interrupted = new Promise((resolveResult) => {
|
|
855
|
+
resolveInterrupted = resolveResult;
|
|
856
|
+
});
|
|
857
|
+
const abort = () => {
|
|
858
|
+
deadline.abort();
|
|
859
|
+
resolveInterrupted?.(result);
|
|
860
|
+
};
|
|
861
|
+
context.signal?.addEventListener('abort', abort, { once: true });
|
|
862
|
+
const timer = setTimeout(() => abort(), DIAGNOSTICS_TIMEOUT_MS);
|
|
863
|
+
const attempt = (async () => {
|
|
864
|
+
const cwd = await this.runtimeCwd();
|
|
865
|
+
await this.discardConnectionsOutside(cwd);
|
|
866
|
+
const sameCwd = async () => {
|
|
867
|
+
try {
|
|
868
|
+
const runtimeCwd = await this.runtimeCwd();
|
|
869
|
+
if (runtimeCwd === cwd)
|
|
870
|
+
return true;
|
|
871
|
+
await this.discardConnectionsOutside(runtimeCwd);
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
// runtimeCwd() already discarded connection-local state.
|
|
875
|
+
}
|
|
876
|
+
return false;
|
|
877
|
+
};
|
|
878
|
+
if (deadline.signal.aborted || !(await sameCwd()))
|
|
879
|
+
return result;
|
|
880
|
+
const requested = call.name === 'Edit'
|
|
881
|
+
? [call.input.file_path]
|
|
882
|
+
: Array.isArray(call.input.edits)
|
|
883
|
+
? call.input.edits.map((edit) => edit?.file_path)
|
|
884
|
+
: [];
|
|
885
|
+
const targets = new Set();
|
|
886
|
+
for (const value of requested) {
|
|
887
|
+
if (typeof value !== 'string' || deadline.signal.aborted)
|
|
888
|
+
continue;
|
|
889
|
+
try {
|
|
890
|
+
const canonical = await realpath(resolve(cwd, value));
|
|
891
|
+
if (deadline.signal.aborted || !(await sameCwd()))
|
|
892
|
+
continue;
|
|
893
|
+
const relativePath = relative(cwd, canonical);
|
|
894
|
+
if (relativePath === '..' ||
|
|
895
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
896
|
+
isAbsolute(relativePath) ||
|
|
897
|
+
!(await stat(canonical)).isFile())
|
|
898
|
+
continue;
|
|
899
|
+
targets.add(canonical);
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// Successful mutations must remain successful when enrichment cannot inspect a target.
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const records = [];
|
|
906
|
+
await Promise.all([...targets].flatMap((filePath) => this.candidates(filePath).map(async (definition) => {
|
|
907
|
+
if (deadline.signal.aborted)
|
|
908
|
+
return;
|
|
909
|
+
try {
|
|
910
|
+
const connection = await this.connection(definition, deadline.signal, cwd);
|
|
911
|
+
if (deadline.signal.aborted || !(await sameCwd()))
|
|
912
|
+
return;
|
|
913
|
+
const current = await connection.refreshDiagnostics(filePath, this.languageId(definition, filePath), deadline.signal);
|
|
914
|
+
if (deadline.signal.aborted || !(await sameCwd()))
|
|
915
|
+
return;
|
|
916
|
+
records.push(...current);
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
// Diagnostics are best effort and never change mutation semantics.
|
|
920
|
+
}
|
|
921
|
+
})));
|
|
922
|
+
if (records.length === 0 || deadline.signal.aborted || !(await sameCwd()))
|
|
923
|
+
return result;
|
|
924
|
+
const block = formatDiagnostics(records, cwd, this.options.servers.flatMap((definition) => lspSensitiveValues(definition)));
|
|
925
|
+
return block
|
|
926
|
+
? { ...result, content: `${result.content}\n${block}` }
|
|
927
|
+
: result;
|
|
928
|
+
})().catch(() => result);
|
|
929
|
+
try {
|
|
930
|
+
return await Promise.race([attempt, interrupted]);
|
|
931
|
+
}
|
|
932
|
+
finally {
|
|
933
|
+
clearTimeout(timer);
|
|
934
|
+
context.signal?.removeEventListener('abort', abort);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
713
937
|
async close() {
|
|
714
938
|
const connections = [...this.connections.values()];
|
|
715
939
|
this.connections.clear();
|
|
@@ -742,10 +966,11 @@ class ClaudeLspToolRegistry {
|
|
|
742
966
|
? Promise.resolve(call)
|
|
743
967
|
: this.base.prepare(call, context);
|
|
744
968
|
}
|
|
745
|
-
execute(call, context) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
969
|
+
async execute(call, context) {
|
|
970
|
+
if (call.name === 'LSP')
|
|
971
|
+
return this.manager.execute(call, context);
|
|
972
|
+
const result = await this.base.execute(call, context);
|
|
973
|
+
return this.manager.enrichMutationResult(call, context, result);
|
|
749
974
|
}
|
|
750
975
|
}
|
|
751
976
|
//# sourceMappingURL=claude-lsp-tool.js.map
|