praxis-agent 0.57.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 +1 -1
- 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/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
|
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));
|
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 src/evals/lsp-diagnostics-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"
|