praxis-agent 0.56.0 → 0.58.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 +4 -3
- package/dist/tools/claude-lsp-tool.d.ts +3 -0
- package/dist/tools/claude-lsp-tool.js +251 -26
- package/dist/tools/glob.d.ts +30 -2
- package/dist/tools/glob.js +84 -75
- package/dist/tools/local-tools.d.ts +4 -0
- package/dist/tools/local-tools.js +34 -34
- package/dist/tools/lsp-diagnostics.d.ts +20 -0
- package/dist/tools/lsp-diagnostics.js +173 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ sessions, configuration, or compatibility directories.
|
|
|
27
27
|
|
|
28
28
|
- macOS or Linux
|
|
29
29
|
- Node.js 24 or newer
|
|
30
|
-
- [`ripgrep`](https://github.com/BurntSushi/ripgrep) (`rg`) for the Grep
|
|
30
|
+
- [`ripgrep`](https://github.com/BurntSushi/ripgrep) (`rg`) for the Grep and Glob tools
|
|
31
31
|
- an API key and model ID for an Anthropic, OpenAI-compatible, or OpenAI
|
|
32
32
|
Responses provider (the stable setup), or the explicitly enabled experimental
|
|
33
33
|
ChatGPT-backed Codex subscription integration
|
|
@@ -173,8 +173,9 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
173
173
|
context budgets; print mode,
|
|
174
174
|
structured JSON/JSONL, context compaction, tool loops, and bounded execution.
|
|
175
175
|
- **Built-in tools** — read, write, edit, `ApplyPatch` for bounded ordered exact
|
|
176
|
-
multi-file replacements,
|
|
177
|
-
|
|
176
|
+
multi-file replacements, configured plugin LSP navigation with fresh bounded
|
|
177
|
+
diagnostics after successful edits, glob, search, shell, notebook, PDF,
|
|
178
|
+
image, web, scheduled prompts, workflows, and worktrees.
|
|
178
179
|
- **Shell lifecycle** — foreground Bash allows up to 10 minutes and carries a
|
|
179
180
|
validated final working directory across calls in the same session without
|
|
180
181
|
leaking state across sessions or overriding an explicit `/cd`.
|
|
@@ -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
|
package/dist/tools/glob.d.ts
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ProcessResult } from '../platform/bounded-process-runner.js';
|
|
2
|
+
export interface GlobSearchRequest {
|
|
2
3
|
root: string;
|
|
3
4
|
displayRoot: string;
|
|
4
5
|
absoluteRoot: string;
|
|
5
6
|
pattern: string;
|
|
6
7
|
signal?: AbortSignal;
|
|
7
8
|
}
|
|
8
|
-
export
|
|
9
|
+
export interface GlobSearchResult {
|
|
10
|
+
content: string;
|
|
11
|
+
isError: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface GlobSearch {
|
|
14
|
+
search(request: GlobSearchRequest): Promise<GlobSearchResult>;
|
|
15
|
+
}
|
|
16
|
+
export interface RipgrepGlobSearchOptions {
|
|
17
|
+
cwd: string;
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
environment?: Readonly<Record<string, string>>;
|
|
20
|
+
runner?: {
|
|
21
|
+
run(options: {
|
|
22
|
+
command: string;
|
|
23
|
+
args: readonly string[];
|
|
24
|
+
cwd?: string;
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
env?: Readonly<Record<string, string>>;
|
|
28
|
+
}): Promise<ProcessResult>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare class RipgrepGlobSearch implements GlobSearch {
|
|
32
|
+
private readonly options;
|
|
33
|
+
private readonly runner;
|
|
34
|
+
constructor(options: RipgrepGlobSearchOptions);
|
|
35
|
+
search(request: GlobSearchRequest): Promise<GlobSearchResult>;
|
|
36
|
+
}
|
|
9
37
|
//# sourceMappingURL=glob.d.ts.map
|
package/dist/tools/glob.js
CHANGED
|
@@ -1,88 +1,97 @@
|
|
|
1
|
-
import { lstat, opendir } from 'node:fs/promises';
|
|
2
1
|
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
2
|
import { minimatch } from 'minimatch';
|
|
3
|
+
import { BoundedProcessRunner, joinedProcessOutput, } from '../platform/bounded-process-runner.js';
|
|
4
4
|
const MAX_RESULTS = 100;
|
|
5
|
+
const MAX_ENUMERATION_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
function portable(value) {
|
|
7
|
+
return sep === '/' ? value : value.split(sep).join('/');
|
|
8
|
+
}
|
|
5
9
|
function abortError() {
|
|
6
10
|
return new DOMException('Tool execution aborted', 'AbortError');
|
|
7
11
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const current = matches[middle];
|
|
20
|
-
if (current && compareMatches(current, candidate) <= 0)
|
|
21
|
-
low = middle + 1;
|
|
22
|
-
else
|
|
23
|
-
high = middle;
|
|
12
|
+
export class RipgrepGlobSearch {
|
|
13
|
+
options;
|
|
14
|
+
runner;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.runner =
|
|
18
|
+
options.runner ??
|
|
19
|
+
new BoundedProcessRunner({
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
maxOutputBytes: MAX_ENUMERATION_BYTES,
|
|
22
|
+
});
|
|
24
23
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
matches.pop();
|
|
28
|
-
}
|
|
29
|
-
export async function globFiles(options) {
|
|
30
|
-
const pattern = portablePath(options.pattern);
|
|
31
|
-
const absolutePattern = isAbsolute(options.pattern);
|
|
32
|
-
const matchBase = !absolutePattern && !pattern.includes('/');
|
|
33
|
-
const matches = [];
|
|
34
|
-
const directories = [''];
|
|
35
|
-
let matchCount = 0;
|
|
36
|
-
let order = 0;
|
|
37
|
-
while (directories.length > 0) {
|
|
38
|
-
if (options.signal?.aborted)
|
|
24
|
+
async search(request) {
|
|
25
|
+
if (request.signal?.aborted)
|
|
39
26
|
throw abortError();
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let metadata;
|
|
59
|
-
try {
|
|
60
|
-
metadata = await lstat(join(options.root, relativePath));
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
if (error.code === 'ENOENT')
|
|
64
|
-
continue;
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
if (!metadata.isFile())
|
|
68
|
-
continue;
|
|
69
|
-
const displayPath = absolutePattern
|
|
70
|
-
? absolutePath
|
|
71
|
-
: portablePath(join(options.displayRoot, relativePath));
|
|
72
|
-
insertOldest(matches, {
|
|
73
|
-
path: displayPath,
|
|
74
|
-
mtimeMs: metadata.mtimeMs,
|
|
75
|
-
order,
|
|
27
|
+
const hidden = this.options.environment?.CLAUDE_CODE_GLOB_HIDDEN !== 'false';
|
|
28
|
+
const noIgnore = this.options.environment?.CLAUDE_CODE_GLOB_NO_IGNORE !== 'false';
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
result = await this.runner.run({
|
|
32
|
+
command: 'rg',
|
|
33
|
+
args: [
|
|
34
|
+
'--files',
|
|
35
|
+
'--null',
|
|
36
|
+
'--sort',
|
|
37
|
+
'modified',
|
|
38
|
+
...(hidden ? ['--hidden'] : []),
|
|
39
|
+
...(noIgnore ? ['--no-ignore'] : []),
|
|
40
|
+
],
|
|
41
|
+
cwd: request.root,
|
|
42
|
+
timeoutMs: this.options.timeoutMs,
|
|
43
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
44
|
+
...(this.options.environment ? { env: this.options.environment } : {}),
|
|
76
45
|
});
|
|
77
|
-
matchCount += 1;
|
|
78
|
-
order += 1;
|
|
79
46
|
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
49
|
+
throw error;
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
throw new Error(`Glob enumeration failed: ${message}`);
|
|
52
|
+
}
|
|
53
|
+
if (result.timedOut)
|
|
54
|
+
return {
|
|
55
|
+
content: `Search timed out after ${this.options.timeoutMs}ms`,
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
if (result.truncated)
|
|
59
|
+
throw new Error('Glob enumeration failed: output truncated');
|
|
60
|
+
if (result.code !== 0) {
|
|
61
|
+
if (result.code === 1 && !result.stdout && !result.stderr)
|
|
62
|
+
return { content: 'No files found', isError: false };
|
|
63
|
+
const output = joinedProcessOutput(result);
|
|
64
|
+
throw new Error(`Glob enumeration failed with exit code ${result.code}${output ? `: ${output}` : ''}`);
|
|
65
|
+
}
|
|
66
|
+
const pattern = portable(request.pattern);
|
|
67
|
+
const absolutePattern = isAbsolute(request.pattern);
|
|
68
|
+
const matchBase = !absolutePattern && !pattern.includes('/');
|
|
69
|
+
const paths = result.stdout.split('\0').filter(Boolean);
|
|
70
|
+
const matched = paths.filter((path) => {
|
|
71
|
+
const relativePath = portable(path);
|
|
72
|
+
const absolutePath = portable(resolve(request.absoluteRoot, relativePath));
|
|
73
|
+
return (pattern === '' ||
|
|
74
|
+
minimatch(absolutePattern ? absolutePath : relativePath, pattern, {
|
|
75
|
+
dot: true,
|
|
76
|
+
matchBase,
|
|
77
|
+
noext: true,
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
if (matched.length === 0)
|
|
81
|
+
return { content: 'No files found', isError: false };
|
|
82
|
+
const display = matched.slice(0, MAX_RESULTS).map((path) => {
|
|
83
|
+
const relativePath = portable(path);
|
|
84
|
+
if (absolutePattern)
|
|
85
|
+
return portable(resolve(request.absoluteRoot, relativePath));
|
|
86
|
+
return portable(join(request.displayRoot, relativePath));
|
|
87
|
+
});
|
|
88
|
+
const content = display.join('\n');
|
|
89
|
+
return {
|
|
90
|
+
content: matched.length > MAX_RESULTS
|
|
91
|
+
? `${content}\n(Showing 100 of ${matched.length} matching files; ${matched.length - 100} more are not listed. Narrow the pattern or path to see the rest.)`
|
|
92
|
+
: content,
|
|
93
|
+
isError: false,
|
|
94
|
+
};
|
|
80
95
|
}
|
|
81
|
-
if (matchCount === 0)
|
|
82
|
-
return 'No files found';
|
|
83
|
-
const content = matches.map((match) => match.path).join('\n');
|
|
84
|
-
return matchCount > MAX_RESULTS
|
|
85
|
-
? `${content}\n(Showing ${MAX_RESULTS} of ${matchCount} matching files; ${matchCount - MAX_RESULTS} more are not listed. Narrow the pattern or path to see the rest.)`
|
|
86
|
-
: content;
|
|
87
96
|
}
|
|
88
97
|
//# sourceMappingURL=glob.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelToolCall, ModelToolDefinition, ToolExecutionContext, ToolExecutionResult, ToolRegistry } from '../core/runtime.js';
|
|
2
|
+
import { type GlobSearch } from './glob.js';
|
|
2
3
|
import type { DataPlane } from '../persistence/data-plane.js';
|
|
3
4
|
export interface LocalToolRegistryOptions {
|
|
4
5
|
cwd: string;
|
|
@@ -17,6 +18,7 @@ export interface LocalToolRegistryOptions {
|
|
|
17
18
|
sandbox?: BashSandboxRuntime;
|
|
18
19
|
homeDirectory?: string;
|
|
19
20
|
configRoot?: string;
|
|
21
|
+
globSearch?: GlobSearch;
|
|
20
22
|
}
|
|
21
23
|
export interface BashSandboxRuntime {
|
|
22
24
|
shouldUseSandbox(input: {
|
|
@@ -46,6 +48,7 @@ export declare class LocalToolRegistry implements ToolRegistry {
|
|
|
46
48
|
private readonly maxShellTimeoutMs;
|
|
47
49
|
private readonly maxSearchTimeoutMs;
|
|
48
50
|
private readonly processRunner;
|
|
51
|
+
private readonly globSearch;
|
|
49
52
|
private readonly enableReportFindings;
|
|
50
53
|
private readonly environment;
|
|
51
54
|
private readonly sessionEnvironment;
|
|
@@ -55,6 +58,7 @@ export declare class LocalToolRegistry implements ToolRegistry {
|
|
|
55
58
|
private readonly sessionCwds;
|
|
56
59
|
private readonly protectedWriteReason;
|
|
57
60
|
private readonly mutationTargetExisted;
|
|
61
|
+
private readonly preparedGlobRoots;
|
|
58
62
|
constructor(options: LocalToolRegistryOptions);
|
|
59
63
|
private assertProtectedWritePath;
|
|
60
64
|
private assertProtectedBashCommand;
|
|
@@ -7,7 +7,7 @@ import sharp from 'sharp';
|
|
|
7
7
|
import { countTokens } from '@anthropic-ai/tokenizer';
|
|
8
8
|
import { commandShell, commandShellArguments, } from '../platform/command-shell.js';
|
|
9
9
|
import { BoundedProcessRunner, joinedProcessOutput, } from '../platform/bounded-process-runner.js';
|
|
10
|
-
import {
|
|
10
|
+
import { RipgrepGlobSearch } from './glob.js';
|
|
11
11
|
import { countLineChanges } from './line-changes.js';
|
|
12
12
|
import { editNotebook, formatNotebookForRead } from './notebook.js';
|
|
13
13
|
import { openPdf } from './pdf.js';
|
|
@@ -537,6 +537,7 @@ export class LocalToolRegistry {
|
|
|
537
537
|
maxShellTimeoutMs;
|
|
538
538
|
maxSearchTimeoutMs;
|
|
539
539
|
processRunner;
|
|
540
|
+
globSearch;
|
|
540
541
|
enableReportFindings;
|
|
541
542
|
environment;
|
|
542
543
|
sessionEnvironment;
|
|
@@ -546,6 +547,7 @@ export class LocalToolRegistry {
|
|
|
546
547
|
sessionCwds = new Map();
|
|
547
548
|
protectedWriteReason;
|
|
548
549
|
mutationTargetExisted = new WeakMap();
|
|
550
|
+
preparedGlobRoots = new WeakMap();
|
|
549
551
|
constructor(options) {
|
|
550
552
|
this.cwd = resolve(options.cwd);
|
|
551
553
|
this.cwdProvider = options.cwdProvider;
|
|
@@ -576,6 +578,13 @@ export class LocalToolRegistry {
|
|
|
576
578
|
cwd: this.cwd,
|
|
577
579
|
maxOutputBytes: this.maxOutputBytes,
|
|
578
580
|
});
|
|
581
|
+
this.globSearch =
|
|
582
|
+
options.globSearch ??
|
|
583
|
+
new RipgrepGlobSearch({
|
|
584
|
+
cwd: this.cwd,
|
|
585
|
+
timeoutMs: this.maxSearchTimeoutMs,
|
|
586
|
+
...(this.environment ? { environment: this.environment } : {}),
|
|
587
|
+
});
|
|
579
588
|
}
|
|
580
589
|
assertProtectedWritePath(filePath) {
|
|
581
590
|
const reason = this.protectedWriteReason(filePath);
|
|
@@ -805,14 +814,16 @@ export class LocalToolRegistry {
|
|
|
805
814
|
case 'Glob': {
|
|
806
815
|
const pathInput = call.input.path;
|
|
807
816
|
const requestedPath = pathInput === undefined ? '.' : stringInput(call.input, 'path');
|
|
808
|
-
await this.globRoot(requestedPath, context);
|
|
809
|
-
|
|
817
|
+
const root = await this.globRoot(requestedPath, context);
|
|
818
|
+
const prepared = {
|
|
810
819
|
...call,
|
|
811
820
|
input: {
|
|
812
821
|
pattern: stringInput(call.input, 'pattern', true),
|
|
813
822
|
...(pathInput === undefined ? {} : { path: requestedPath }),
|
|
814
823
|
},
|
|
815
824
|
};
|
|
825
|
+
this.preparedGlobRoots.set(prepared, root);
|
|
826
|
+
return prepared;
|
|
816
827
|
}
|
|
817
828
|
case 'Grep': {
|
|
818
829
|
const glob = optionalString(call.input, 'glob');
|
|
@@ -857,6 +868,11 @@ export class LocalToolRegistry {
|
|
|
857
868
|
if (JSON.stringify(prepared.input) !== JSON.stringify(call.input)) {
|
|
858
869
|
throw new Error('Tool input changed after permission approval');
|
|
859
870
|
}
|
|
871
|
+
const approvedGlobRoot = this.preparedGlobRoots.get(call);
|
|
872
|
+
if (approvedGlobRoot !== undefined &&
|
|
873
|
+
this.preparedGlobRoots.get(prepared) !== approvedGlobRoot) {
|
|
874
|
+
throw new Error('Tool input changed after permission approval');
|
|
875
|
+
}
|
|
860
876
|
const executionTargetExisted = this.mutationTargetExisted.get(prepared);
|
|
861
877
|
if (approvedTargetExisted !== undefined &&
|
|
862
878
|
executionTargetExisted !== approvedTargetExisted) {
|
|
@@ -1466,37 +1482,21 @@ export class LocalToolRegistry {
|
|
|
1466
1482
|
}
|
|
1467
1483
|
async glob(call, context) {
|
|
1468
1484
|
const requestedPath = call.input.path === undefined ? '.' : stringInput(call.input, 'path');
|
|
1469
|
-
const root =
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
:
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
return {
|
|
1485
|
-
content: truncateOutput(content, this.maxOutputBytes),
|
|
1486
|
-
isError: false,
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
catch (error) {
|
|
1490
|
-
if (context.signal?.aborted)
|
|
1491
|
-
throw abortError();
|
|
1492
|
-
if (timeoutSignal.aborted) {
|
|
1493
|
-
return {
|
|
1494
|
-
content: `Search timed out after ${this.maxSearchTimeoutMs}ms`,
|
|
1495
|
-
isError: true,
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
throw error;
|
|
1499
|
-
}
|
|
1485
|
+
const root = this.preparedGlobRoots.get(call) ??
|
|
1486
|
+
(await this.globRoot(requestedPath, context));
|
|
1487
|
+
const result = await this.globSearch.search({
|
|
1488
|
+
root,
|
|
1489
|
+
displayRoot: call.input.path === undefined ? '.' : requestedPath,
|
|
1490
|
+
absoluteRoot: isAbsolute(requestedPath)
|
|
1491
|
+
? resolve(requestedPath)
|
|
1492
|
+
: resolve(this.currentCwd(context), requestedPath),
|
|
1493
|
+
pattern: stringInput(call.input, 'pattern', true),
|
|
1494
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
1495
|
+
});
|
|
1496
|
+
return {
|
|
1497
|
+
content: truncateOutput(result.content, this.maxOutputBytes),
|
|
1498
|
+
isError: result.isError,
|
|
1499
|
+
};
|
|
1500
1500
|
}
|
|
1501
1501
|
async grep(call, context) {
|
|
1502
1502
|
const workspaceRoot = await realpath(this.currentCwd(context));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DiagnosticSeverity = 'error' | 'warning' | 'information' | 'hint';
|
|
2
|
+
export interface LspDiagnosticRecord {
|
|
3
|
+
canonicalPath: string;
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
severity: DiagnosticSeverity;
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LspDiagnosticsPublication {
|
|
11
|
+
uri: string;
|
|
12
|
+
filePath: string;
|
|
13
|
+
version?: number;
|
|
14
|
+
diagnostics: readonly LspDiagnosticRecord[];
|
|
15
|
+
}
|
|
16
|
+
/** Parse one strict, id-less LSP publishDiagnostics notification. */
|
|
17
|
+
export declare function parsePublishDiagnostics(value: unknown): LspDiagnosticsPublication | null;
|
|
18
|
+
/** Format diagnostics as one bounded provider-visible block. */
|
|
19
|
+
export declare function formatDiagnostics(diagnostics: readonly LspDiagnosticRecord[], cwd: string, sensitiveValues?: readonly string[]): string | null;
|
|
20
|
+
//# sourceMappingURL=lsp-diagnostics.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { redactSensitiveText } from '../platform/sensitive-data.js';
|
|
4
|
+
const severityNames = {
|
|
5
|
+
1: 'error',
|
|
6
|
+
2: 'warning',
|
|
7
|
+
3: 'information',
|
|
8
|
+
4: 'hint',
|
|
9
|
+
};
|
|
10
|
+
const severityRank = {
|
|
11
|
+
error: 0,
|
|
12
|
+
warning: 1,
|
|
13
|
+
information: 2,
|
|
14
|
+
hint: 3,
|
|
15
|
+
};
|
|
16
|
+
function safeInteger(value) {
|
|
17
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
18
|
+
}
|
|
19
|
+
function recordObject(value) {
|
|
20
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function localFilePath(uri) {
|
|
23
|
+
if (typeof uri !== 'string')
|
|
24
|
+
return null;
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(uri);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (parsed.protocol !== 'file:' || parsed.search || parsed.hash)
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
return fileURLToPath(parsed);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Parse one strict, id-less LSP publishDiagnostics notification. */
|
|
42
|
+
export function parsePublishDiagnostics(value) {
|
|
43
|
+
try {
|
|
44
|
+
if (!recordObject(value) || value.jsonrpc !== '2.0')
|
|
45
|
+
return null;
|
|
46
|
+
if (value.id !== undefined ||
|
|
47
|
+
value.method !== 'textDocument/publishDiagnostics')
|
|
48
|
+
return null;
|
|
49
|
+
if (!recordObject(value.params))
|
|
50
|
+
return null;
|
|
51
|
+
const params = value.params;
|
|
52
|
+
const uri = params.uri;
|
|
53
|
+
const rawPath = localFilePath(uri);
|
|
54
|
+
if (!rawPath || typeof uri !== 'string')
|
|
55
|
+
return null;
|
|
56
|
+
const filePath = rawPath;
|
|
57
|
+
const version = params.version;
|
|
58
|
+
if (version !== undefined && !safeInteger(version))
|
|
59
|
+
return null;
|
|
60
|
+
if (!Array.isArray(params.diagnostics))
|
|
61
|
+
return null;
|
|
62
|
+
const diagnostics = [];
|
|
63
|
+
for (const value of params.diagnostics) {
|
|
64
|
+
if (!recordObject(value) || typeof value.message !== 'string')
|
|
65
|
+
return null;
|
|
66
|
+
if (!recordObject(value.range))
|
|
67
|
+
return null;
|
|
68
|
+
const range = value.range;
|
|
69
|
+
if (!recordObject(range.start) || !recordObject(range.end))
|
|
70
|
+
return null;
|
|
71
|
+
const start = range.start;
|
|
72
|
+
const end = range.end;
|
|
73
|
+
if (!safeInteger(start.line) ||
|
|
74
|
+
!safeInteger(start.character) ||
|
|
75
|
+
!safeInteger(end.line) ||
|
|
76
|
+
!safeInteger(end.character) ||
|
|
77
|
+
end.line < start.line ||
|
|
78
|
+
(end.line === start.line && end.character < start.character))
|
|
79
|
+
return null;
|
|
80
|
+
const severity = value.severity;
|
|
81
|
+
if (severity !== undefined &&
|
|
82
|
+
(!safeInteger(severity) || severity < 1 || severity > 4))
|
|
83
|
+
return null;
|
|
84
|
+
const code = value.code;
|
|
85
|
+
if (code !== undefined &&
|
|
86
|
+
!(typeof code === 'string' ||
|
|
87
|
+
(typeof code === 'number' && Number.isFinite(code))))
|
|
88
|
+
return null;
|
|
89
|
+
diagnostics.push({
|
|
90
|
+
canonicalPath: filePath,
|
|
91
|
+
line: start.line + 1,
|
|
92
|
+
column: start.character + 1,
|
|
93
|
+
severity: severityNames[severity ?? 1] ?? 'error',
|
|
94
|
+
code: code === undefined ? '' : String(code),
|
|
95
|
+
message: value.message,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
uri,
|
|
100
|
+
filePath,
|
|
101
|
+
...(version === undefined ? {} : { version }),
|
|
102
|
+
diagnostics,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function compareText(a, b) {
|
|
110
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
function sanitize(value, sensitiveValues) {
|
|
113
|
+
const redacted = redactSensitiveText(value, sensitiveValues);
|
|
114
|
+
let controlsCollapsed = '';
|
|
115
|
+
let inControls = false;
|
|
116
|
+
for (const character of redacted) {
|
|
117
|
+
const code = character.codePointAt(0) ?? 0;
|
|
118
|
+
const control = code <= 0x1f ||
|
|
119
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
120
|
+
code === 0x2028 ||
|
|
121
|
+
code === 0x2029;
|
|
122
|
+
if (control) {
|
|
123
|
+
if (!inControls)
|
|
124
|
+
controlsCollapsed += ' ';
|
|
125
|
+
inControls = true;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
controlsCollapsed += character;
|
|
129
|
+
inControls = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return controlsCollapsed.replaceAll('<', '<').replaceAll('>', '>');
|
|
133
|
+
}
|
|
134
|
+
function relativePath(path, cwd) {
|
|
135
|
+
return relative(cwd, path).replaceAll('\\', '/').replace(/^\.\//u, '');
|
|
136
|
+
}
|
|
137
|
+
function compareRecords(a, b, cwd) {
|
|
138
|
+
const aPath = relativePath(a.canonicalPath, cwd);
|
|
139
|
+
const bPath = relativePath(b.canonicalPath, cwd);
|
|
140
|
+
return (compareText(aPath, bPath) ||
|
|
141
|
+
a.line - b.line ||
|
|
142
|
+
a.column - b.column ||
|
|
143
|
+
severityRank[a.severity] - severityRank[b.severity] ||
|
|
144
|
+
compareText(a.code, b.code) ||
|
|
145
|
+
compareText(a.message, b.message));
|
|
146
|
+
}
|
|
147
|
+
/** Format diagnostics as one bounded provider-visible block. */
|
|
148
|
+
export function formatDiagnostics(diagnostics, cwd, sensitiveValues = []) {
|
|
149
|
+
if (diagnostics.length === 0)
|
|
150
|
+
return null;
|
|
151
|
+
const sorted = [...diagnostics].sort((a, b) => compareRecords(a, b, cwd));
|
|
152
|
+
const marker = '… diagnostics truncated';
|
|
153
|
+
const lines = [];
|
|
154
|
+
let truncated = sorted.length > 8;
|
|
155
|
+
for (const diagnostic of sorted.slice(0, 8)) {
|
|
156
|
+
const code = sanitize(diagnostic.code, sensitiveValues);
|
|
157
|
+
const message = sanitize(diagnostic.message, sensitiveValues);
|
|
158
|
+
const line = `${sanitize(relativePath(diagnostic.canonicalPath, cwd), sensitiveValues)}:${diagnostic.line}:${diagnostic.column} ${sanitize(diagnostic.severity, sensitiveValues)}${code ? ` ${code}` : ''} ${message}`;
|
|
159
|
+
const candidate = `<diagnostics>\n${[...lines, line].join('\n')}\n</diagnostics>`;
|
|
160
|
+
if (Buffer.byteLength(candidate, 'utf8') > 4096) {
|
|
161
|
+
truncated = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
lines.push(line);
|
|
165
|
+
}
|
|
166
|
+
if (truncated) {
|
|
167
|
+
while (Buffer.byteLength(`<diagnostics>\n${[...lines, marker].join('\n')}\n</diagnostics>`, 'utf8') > 4096)
|
|
168
|
+
lines.pop();
|
|
169
|
+
lines.push(marker);
|
|
170
|
+
}
|
|
171
|
+
return `<diagnostics>\n${lines.join('\n')}\n</diagnostics>`;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=lsp-diagnostics.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "praxis-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Local-first, single-user general agent for the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wuqisen",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"verify:ci-coverage": "node scripts/verify-ci-coverage.mjs",
|
|
63
63
|
"verify:fixture-contracts": "node scripts/verify-fixture-contracts.mjs",
|
|
64
64
|
"test:fixtures": "node scripts/run-fixture-contracts.mjs",
|
|
65
|
-
"test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts"
|
|
65
|
+
"test:eval:baseline": "vitest run src/evals/coding-baseline.test.ts src/evals/project-eval-comparison.test.ts src/evals/apply-patch-admission.test.ts src/evals/lsp-diagnostics-admission.test.ts src/evals/glob-ripgrep-admission.test.ts"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
|
68
68
|
"node": ">=24"
|