@arnilo/prism-coding-agent 0.0.96 → 0.1.1
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/CHANGELOG.md +139 -3
- package/README.md +48 -19
- package/dist/ask-user-decision.d.ts +160 -0
- package/dist/ask-user-decision.js +495 -0
- package/dist/atomic-write.d.ts +3 -0
- package/dist/atomic-write.js +24 -0
- package/dist/checks.js +5 -0
- package/dist/coding-checkpoint.js +6 -15
- package/dist/delete.d.ts +29 -0
- package/dist/delete.js +119 -0
- package/dist/edit-diff.js +1 -4
- package/dist/edit.d.ts +5 -1
- package/dist/edit.js +20 -9
- package/dist/effects.d.ts +33 -0
- package/dist/effects.js +89 -0
- package/dist/execution-policy.d.ts +8 -3
- package/dist/execution-policy.js +5 -2
- package/dist/file-mutation-queue.js +1 -2
- package/dist/forge/github.d.ts +2 -0
- package/dist/forge/github.js +554 -0
- package/dist/forge/index.d.ts +3 -0
- package/dist/forge/index.js +3 -0
- package/dist/forge/types.d.ts +150 -0
- package/dist/forge/types.js +19 -0
- package/dist/git-aware-repository.d.ts +25 -0
- package/dist/git-aware-repository.js +268 -0
- package/dist/git-exec.js +1 -1
- package/dist/git-tools.d.ts +4 -1
- package/dist/git-tools.js +15 -7
- package/dist/git.d.ts +3 -3
- package/dist/git.js +14 -14
- package/dist/glob-match.d.ts +6 -0
- package/dist/glob-match.js +81 -0
- package/dist/glob.d.ts +14 -0
- package/dist/glob.js +147 -0
- package/dist/goal-verify.d.ts +66 -0
- package/dist/goal-verify.js +280 -0
- package/dist/index.d.ts +63 -30
- package/dist/index.js +40 -16
- package/dist/language/client.d.ts +44 -0
- package/dist/language/client.js +290 -0
- package/dist/language/framing.d.ts +23 -0
- package/dist/language/framing.js +112 -0
- package/dist/language/index.d.ts +4 -0
- package/dist/language/index.js +4 -0
- package/dist/language/intelligence.d.ts +10 -0
- package/dist/language/intelligence.js +526 -0
- package/dist/language/types.d.ts +106 -0
- package/dist/language/types.js +21 -0
- package/dist/lifecycle.d.ts +75 -0
- package/dist/lifecycle.js +102 -0
- package/dist/limits.d.ts +41 -0
- package/dist/limits.js +41 -0
- package/dist/list.js +6 -10
- package/dist/move.d.ts +24 -0
- package/dist/move.js +150 -0
- package/dist/mutation-path.d.ts +7 -0
- package/dist/mutation-path.js +51 -0
- package/dist/output-accumulator.d.ts +8 -0
- package/dist/output-accumulator.js +45 -1
- package/dist/path-utils.js +1 -1
- package/dist/process/index.d.ts +3 -0
- package/dist/process/index.js +3 -0
- package/dist/process/sessions.d.ts +2 -0
- package/dist/process/sessions.js +592 -0
- package/dist/process/types.d.ts +146 -0
- package/dist/process/types.js +19 -0
- package/dist/read-path-set.d.ts +14 -0
- package/dist/read-path-set.js +26 -0
- package/dist/read.d.ts +3 -0
- package/dist/read.js +11 -17
- package/dist/repository.d.ts +54 -3
- package/dist/repository.js +144 -38
- package/dist/search.d.ts +1 -1
- package/dist/search.js +91 -27
- package/dist/shell.d.ts +3 -0
- package/dist/shell.js +23 -8
- package/dist/truncate.js +1 -1
- package/dist/write.d.ts +5 -1
- package/dist/write.js +19 -6
- package/package.json +6 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { AgentIdentity, ExecutionPolicy, OwnershipScope } from "@arnilo/prism";
|
|
2
|
+
/** Duck-typed long-running sandbox handle (mirrors coding-security SandboxProcessHandle). */
|
|
3
|
+
export interface ProcessSandboxHandle {
|
|
4
|
+
write(data: Uint8Array): Promise<void>;
|
|
5
|
+
signal(name: string): Promise<void>;
|
|
6
|
+
kill(): Promise<void>;
|
|
7
|
+
release(): Promise<void>;
|
|
8
|
+
wait(options?: {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
exitCode: number | null;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export interface ProcessSandboxStartRequest {
|
|
16
|
+
readonly file: string;
|
|
17
|
+
readonly args: readonly string[];
|
|
18
|
+
readonly cwd?: string;
|
|
19
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
20
|
+
readonly onData?: (data: Buffer) => void;
|
|
21
|
+
readonly signal?: AbortSignal;
|
|
22
|
+
readonly timeout?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Optional sandbox backend for ProcessSessions.
|
|
26
|
+
* Presence of `startProcess` = long-running capable; absence fails closed.
|
|
27
|
+
*/
|
|
28
|
+
export interface ProcessSandboxBackend {
|
|
29
|
+
startProcess?(request: ProcessSandboxStartRequest): Promise<ProcessSandboxHandle>;
|
|
30
|
+
status?(): Promise<{
|
|
31
|
+
readonly state: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
export type ProcessSessionState = "starting" | "running" | "exited" | "killed" | "released" | "expired" | "unknown";
|
|
35
|
+
export type ProcessSessionErrorCode = "ERR_PRISM_PROCESS_POLICY" | "ERR_PRISM_PROCESS_OWNERSHIP" | "ERR_PRISM_PROCESS_STATE" | "ERR_PRISM_PROCESS_LIMIT" | "ERR_PRISM_PROCESS_PTY_UNSUPPORTED" | "ERR_PRISM_PROCESS_UNSUPPORTED";
|
|
36
|
+
export declare class ProcessSessionError extends Error {
|
|
37
|
+
readonly code: ProcessSessionErrorCode;
|
|
38
|
+
constructor(code: ProcessSessionErrorCode, message: string);
|
|
39
|
+
}
|
|
40
|
+
export interface ProcessSessionLimits {
|
|
41
|
+
readonly maxSessions?: number;
|
|
42
|
+
readonly maxInputBytes?: number;
|
|
43
|
+
readonly maxLifetimeMs?: number;
|
|
44
|
+
readonly maxOutputChunkBytes?: number;
|
|
45
|
+
readonly maxTotalOutputBytes?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ResolvedProcessSessionLimits {
|
|
48
|
+
readonly maxSessions: number;
|
|
49
|
+
readonly maxInputBytes: number;
|
|
50
|
+
readonly maxLifetimeMs: number;
|
|
51
|
+
readonly maxOutputChunkBytes: number;
|
|
52
|
+
readonly maxTotalOutputBytes: number;
|
|
53
|
+
}
|
|
54
|
+
export declare function resolveProcessSessionLimits(limits?: ProcessSessionLimits): ResolvedProcessSessionLimits;
|
|
55
|
+
export interface ProcessStartRequest {
|
|
56
|
+
readonly command: string;
|
|
57
|
+
readonly args?: readonly string[];
|
|
58
|
+
readonly cwd?: string;
|
|
59
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
60
|
+
/** Default false. true on unsupported platform → ERR_PRISM_PROCESS_PTY_UNSUPPORTED. */
|
|
61
|
+
readonly pty?: boolean;
|
|
62
|
+
readonly lifetimeMs?: number;
|
|
63
|
+
/** Owner string for this session; defaults to registry ownership key. */
|
|
64
|
+
readonly owner?: string;
|
|
65
|
+
/** When true, run cancel releases instead of killing (default kill). */
|
|
66
|
+
readonly releaseOnCancel?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface ProcessOutputChunk {
|
|
69
|
+
readonly data: string;
|
|
70
|
+
/** Next byte cursor for paging. */
|
|
71
|
+
readonly cursor: number;
|
|
72
|
+
readonly eof: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface ProcessExitResult {
|
|
75
|
+
readonly exitCode: number | null;
|
|
76
|
+
readonly state: ProcessSessionState;
|
|
77
|
+
}
|
|
78
|
+
export interface ProcessSessionMetadata {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly commandFingerprint: string;
|
|
81
|
+
readonly owner: string;
|
|
82
|
+
readonly workspace: string;
|
|
83
|
+
readonly policyDecision: string;
|
|
84
|
+
readonly startedAt: string;
|
|
85
|
+
readonly exitedAt?: string;
|
|
86
|
+
readonly state: ProcessSessionState;
|
|
87
|
+
readonly releaseOnCancel: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface ProcessSession {
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly state: ProcessSessionState;
|
|
92
|
+
readonly owner: string;
|
|
93
|
+
metadata(): ProcessSessionMetadata;
|
|
94
|
+
output(request?: {
|
|
95
|
+
cursor?: number;
|
|
96
|
+
maxBytes?: number;
|
|
97
|
+
}): Promise<ProcessOutputChunk>;
|
|
98
|
+
input(data: string | Uint8Array): Promise<void>;
|
|
99
|
+
wait(options?: {
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
}): Promise<ProcessExitResult>;
|
|
103
|
+
signal(name: "SIGTERM" | "SIGINT" | "SIGHUP"): Promise<void>;
|
|
104
|
+
kill(): Promise<void>;
|
|
105
|
+
release(): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
export type CodingProcessEvent = {
|
|
108
|
+
readonly type: "process_started" | "process_exited" | "process_killed" | "process_released" | "process_expired" | "process_unknown";
|
|
109
|
+
readonly sessionId: string;
|
|
110
|
+
readonly processId: string;
|
|
111
|
+
readonly owner: string;
|
|
112
|
+
readonly exitCode?: number | null;
|
|
113
|
+
readonly at: string;
|
|
114
|
+
};
|
|
115
|
+
export interface ProcessSessions {
|
|
116
|
+
start(request: ProcessStartRequest): Promise<ProcessSession>;
|
|
117
|
+
get(sessionId: string, owner?: string): ProcessSession;
|
|
118
|
+
/** Kill (default) or release all sessions for `owner`. */
|
|
119
|
+
cancelOwned(owner: string, options?: {
|
|
120
|
+
release?: boolean;
|
|
121
|
+
}): Promise<void>;
|
|
122
|
+
/** Mark a running session unknown (backend loss); never fabricates exitCode. */
|
|
123
|
+
markUnknown(sessionId: string, owner?: string): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Host resume / sandbox-loss reconciliation: mark every running/starting session unknown.
|
|
126
|
+
* Never fabricates exitCode. O(owned sessions).
|
|
127
|
+
*/
|
|
128
|
+
reconcile(): Promise<{
|
|
129
|
+
readonly markedUnknown: number;
|
|
130
|
+
}>;
|
|
131
|
+
dispose(): Promise<void>;
|
|
132
|
+
}
|
|
133
|
+
export interface CreateProcessSessionsOptions {
|
|
134
|
+
readonly cwd: string;
|
|
135
|
+
readonly policy?: ExecutionPolicy;
|
|
136
|
+
readonly limits?: ProcessSessionLimits;
|
|
137
|
+
readonly onEvent?: (event: CodingProcessEvent) => void;
|
|
138
|
+
readonly ownership?: OwnershipScope;
|
|
139
|
+
/** Host-verified identity; projects onto default owner when ownership omitted. */
|
|
140
|
+
readonly identity?: AgentIdentity;
|
|
141
|
+
/**
|
|
142
|
+
* When set: use `startProcess` if present; otherwise start() fails closed
|
|
143
|
+
* (`ERR_PRISM_PROCESS_UNSUPPORTED`). Native spawn only when sandbox omitted.
|
|
144
|
+
*/
|
|
145
|
+
readonly sandbox?: ProcessSandboxBackend;
|
|
146
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DEFAULT_MAX_PROCESS_INPUT_BYTES, DEFAULT_MAX_PROCESS_LIFETIME_MS, DEFAULT_MAX_PROCESS_OUTPUT_CHUNK_BYTES, DEFAULT_MAX_PROCESS_SESSIONS, DEFAULT_MAX_PROCESS_TOTAL_OUTPUT_BYTES, HARD_MAX_PROCESS_INPUT_BYTES, HARD_MAX_PROCESS_LIFETIME_MS, HARD_MAX_PROCESS_OUTPUT_CHUNK_BYTES, HARD_MAX_PROCESS_SESSIONS, HARD_MAX_PROCESS_TOTAL_OUTPUT_BYTES, validateCodingLimit, } from "../limits.js";
|
|
2
|
+
export class ProcessSessionError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ProcessSessionError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function resolveProcessSessionLimits(limits) {
|
|
11
|
+
return {
|
|
12
|
+
maxSessions: validateCodingLimit("maxSessions", limits?.maxSessions ?? DEFAULT_MAX_PROCESS_SESSIONS, HARD_MAX_PROCESS_SESSIONS),
|
|
13
|
+
maxInputBytes: validateCodingLimit("maxInputBytes", limits?.maxInputBytes ?? DEFAULT_MAX_PROCESS_INPUT_BYTES, HARD_MAX_PROCESS_INPUT_BYTES),
|
|
14
|
+
maxLifetimeMs: validateCodingLimit("maxLifetimeMs", limits?.maxLifetimeMs ?? DEFAULT_MAX_PROCESS_LIFETIME_MS, HARD_MAX_PROCESS_LIFETIME_MS),
|
|
15
|
+
maxOutputChunkBytes: validateCodingLimit("maxOutputChunkBytes", limits?.maxOutputChunkBytes ?? DEFAULT_MAX_PROCESS_OUTPUT_CHUNK_BYTES, HARD_MAX_PROCESS_OUTPUT_CHUNK_BYTES),
|
|
16
|
+
maxTotalOutputBytes: validateCodingLimit("maxTotalOutputBytes", limits?.maxTotalOutputBytes ?? DEFAULT_MAX_PROCESS_TOTAL_OUTPUT_BYTES, HARD_MAX_PROCESS_TOTAL_OUTPUT_BYTES),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Session-scoped paths successfully read via the read tool (host-owned, not checkpointed). */
|
|
2
|
+
export interface ReadPathSet {
|
|
3
|
+
has(path: string): boolean;
|
|
4
|
+
add(path: string): void;
|
|
5
|
+
list(): readonly string[];
|
|
6
|
+
clear(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createReadPathSet(): ReadPathSet;
|
|
9
|
+
export interface ReadBeforeWriteOptions {
|
|
10
|
+
requireReadBeforeWrite?: boolean;
|
|
11
|
+
readPathSet?: ReadPathSet;
|
|
12
|
+
}
|
|
13
|
+
/** Returns refusal message, or null when the mutation may proceed. */
|
|
14
|
+
export declare function refuseReadBeforeWrite(operation: "write" | "edit", displayPath: string, absolutePath: string, options: ReadBeforeWriteOptions | undefined, force: boolean): string | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function createReadPathSet() {
|
|
2
|
+
const paths = new Set();
|
|
3
|
+
return {
|
|
4
|
+
has(path) {
|
|
5
|
+
return paths.has(path);
|
|
6
|
+
},
|
|
7
|
+
add(path) {
|
|
8
|
+
paths.add(path);
|
|
9
|
+
},
|
|
10
|
+
list() {
|
|
11
|
+
return [...paths];
|
|
12
|
+
},
|
|
13
|
+
clear() {
|
|
14
|
+
paths.clear();
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Returns refusal message, or null when the mutation may proceed. */
|
|
19
|
+
export function refuseReadBeforeWrite(operation, displayPath, absolutePath, options, force) {
|
|
20
|
+
if (!options?.requireReadBeforeWrite || force)
|
|
21
|
+
return null;
|
|
22
|
+
if (options.readPathSet?.has(absolutePath))
|
|
23
|
+
return null;
|
|
24
|
+
return `Refusing ${operation} to ${displayPath}: not read in this session. Read first or pass force=true.`;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=read-path-set.js.map
|
package/dist/read.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { Buffer } from "node:buffer";
|
|
23
23
|
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
24
|
+
import type { ReadPathSet } from "./read-path-set.js";
|
|
24
25
|
import { type TruncationResult } from "./truncate.js";
|
|
25
26
|
/** Detect a supported image MIME type from a buffer's leading bytes. Returns null for non-images. */
|
|
26
27
|
export declare function detectSupportedImageMimeType(buffer: Buffer): string | null;
|
|
@@ -100,6 +101,8 @@ export interface ReadToolOptions {
|
|
|
100
101
|
maxLines?: number;
|
|
101
102
|
/** Max bytes kept from the head (default 50KB). */
|
|
102
103
|
maxBytes?: number;
|
|
104
|
+
/** When set, successful reads record the resolved absolute path for read-before-write guards. */
|
|
105
|
+
readPathSet?: ReadPathSet;
|
|
103
106
|
}
|
|
104
107
|
export declare function createReadTool(cwd: string, options?: ReadToolOptions): ToolDefinition;
|
|
105
108
|
/** Re-exported for hosts building custom read tools or analyzing truncation metadata. */
|
package/dist/read.js
CHANGED
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { Buffer } from "node:buffer";
|
|
23
23
|
import { constants } from "node:fs";
|
|
24
|
-
import { access as fsAccess,
|
|
24
|
+
import { access as fsAccess, stat as fsStat, open } from "node:fs/promises";
|
|
25
25
|
import { readFileBounded } from "./bounded-file.js";
|
|
26
|
+
import { CODING_OBSERVATION_EFFECT } from "./effects.js";
|
|
26
27
|
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
27
|
-
import { resolveReadPathAsync } from "./path-utils.js";
|
|
28
28
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_IMAGE_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, HARD_MAX_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, validateCodingLimit, } from "./limits.js";
|
|
29
|
+
import { resolveReadPathAsync } from "./path-utils.js";
|
|
29
30
|
import { formatSize } from "./truncate.js";
|
|
30
31
|
// --- magic-byte image MIME detection (faithful port of pi utils/mime.js, pure JS, no deps) ---
|
|
31
32
|
const IMAGE_TYPE_SNIFF_BYTES = 4100;
|
|
@@ -64,9 +65,7 @@ export async function detectSupportedImageMimeTypeFromFile(filePath) {
|
|
|
64
65
|
}
|
|
65
66
|
function isPng(buffer) {
|
|
66
67
|
// First chunk after the 8-byte signature must be a 13-byte IHDR.
|
|
67
|
-
return
|
|
68
|
-
readUint32BE(buffer, PNG_SIGNATURE.length) === 13 &&
|
|
69
|
-
startsWithAscii(buffer, 12, "IHDR"));
|
|
68
|
+
return buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR");
|
|
70
69
|
}
|
|
71
70
|
function isAnimatedPng(buffer) {
|
|
72
71
|
// Walk PNG chunks; an acTL chunk before the first IDAT marks an animated (APNG) image.
|
|
@@ -118,16 +117,10 @@ function readUint16LE(buffer, offset) {
|
|
|
118
117
|
return (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8);
|
|
119
118
|
}
|
|
120
119
|
function readUint32BE(buffer, offset) {
|
|
121
|
-
return ((buffer[offset] ?? 0) * 0x1000000 +
|
|
122
|
-
((buffer[offset + 1] ?? 0) << 16) +
|
|
123
|
-
((buffer[offset + 2] ?? 0) << 8) +
|
|
124
|
-
(buffer[offset + 3] ?? 0));
|
|
120
|
+
return ((buffer[offset] ?? 0) * 0x1000000 + ((buffer[offset + 1] ?? 0) << 16) + ((buffer[offset + 2] ?? 0) << 8) + (buffer[offset + 3] ?? 0));
|
|
125
121
|
}
|
|
126
122
|
function readUint32LE(buffer, offset) {
|
|
127
|
-
return ((buffer[offset] ?? 0) +
|
|
128
|
-
((buffer[offset + 1] ?? 0) << 8) +
|
|
129
|
-
((buffer[offset + 2] ?? 0) << 16) +
|
|
130
|
-
(buffer[offset + 3] ?? 0) * 0x1000000);
|
|
123
|
+
return ((buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8) + ((buffer[offset + 2] ?? 0) << 16) + (buffer[offset + 3] ?? 0) * 0x1000000);
|
|
131
124
|
}
|
|
132
125
|
function startsWith(buffer, bytes) {
|
|
133
126
|
if (buffer.length < bytes.length)
|
|
@@ -315,7 +308,8 @@ export function createReadTool(cwd, options) {
|
|
|
315
308
|
const maxScanBytes = validateCodingLimit("maxScanBytes", options?.maxScanBytes ?? DEFAULT_MAX_TEXT_SCAN_BYTES, HARD_MAX_TEXT_SCAN_BYTES);
|
|
316
309
|
return {
|
|
317
310
|
name: "read",
|
|
318
|
-
|
|
311
|
+
effect: CODING_OBSERVATION_EFFECT,
|
|
312
|
+
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp); images are returned as image content. For text files, output is truncated to ${maxLines} lines or ${maxBytes / 1024}KB (whichever is hit first). When truncated, continue with the suggested offset until complete. Prefer repo_search to find text across many files.`,
|
|
319
313
|
parameters: {
|
|
320
314
|
type: "object",
|
|
321
315
|
properties: {
|
|
@@ -339,9 +333,7 @@ export function createReadTool(cwd, options) {
|
|
|
339
333
|
}
|
|
340
334
|
try {
|
|
341
335
|
const startLine = validateCodingLimit("offset", offset ?? 1, Number.MAX_SAFE_INTEGER);
|
|
342
|
-
const requestedLines = limit === undefined
|
|
343
|
-
? undefined
|
|
344
|
-
: validateCodingLimit("limit", limit, HARD_MAX_LINES);
|
|
336
|
+
const requestedLines = limit === undefined ? undefined : validateCodingLimit("limit", limit, HARD_MAX_LINES);
|
|
345
337
|
const absolutePath = await resolveReadPathAsync(path, cwd);
|
|
346
338
|
const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
347
339
|
kind: "read",
|
|
@@ -362,6 +354,7 @@ export function createReadTool(cwd, options) {
|
|
|
362
354
|
autoResizeImages: options?.autoResizeImages,
|
|
363
355
|
signal: context.signal,
|
|
364
356
|
});
|
|
357
|
+
options?.readPathSet?.add(allowedPath);
|
|
365
358
|
return {
|
|
366
359
|
toolCallId,
|
|
367
360
|
name: "read",
|
|
@@ -416,6 +409,7 @@ export function createReadTool(cwd, options) {
|
|
|
416
409
|
outputText += `\n\n[Showing lines ${page.startLine}-${endLine}${page.truncatedBy === "bytes" ? ` (${formatSize(maxBytes)} limit)` : ""}. Use offset=${page.nextOffset} to continue.]`;
|
|
417
410
|
}
|
|
418
411
|
}
|
|
412
|
+
options?.readPathSet?.add(allowedPath);
|
|
419
413
|
return {
|
|
420
414
|
toolCallId,
|
|
421
415
|
name: "read",
|
package/dist/repository.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded repository walk primitives for list/search tools.
|
|
3
|
+
*
|
|
4
|
+
* Streams the tree with Node `opendir` / `lstat`; never follows symlink escapes,
|
|
5
|
+
* rejects devices/FIFOs/sockets for descent, and charges finite depth/entry/file
|
|
6
|
+
* limits before retaining the next result. No glob/index/watcher dependency.
|
|
7
|
+
*/
|
|
1
8
|
export type RepoEntryKind = "file" | "directory" | "symlink" | "other";
|
|
2
9
|
export interface RepoListEntry {
|
|
3
10
|
readonly path: string;
|
|
@@ -13,6 +20,16 @@ export interface RepositoryListResult {
|
|
|
13
20
|
readonly nextOffset?: number;
|
|
14
21
|
readonly offset: number;
|
|
15
22
|
}
|
|
23
|
+
export interface RepositoryGlobResult {
|
|
24
|
+
readonly paths: readonly string[];
|
|
25
|
+
readonly truncated: boolean;
|
|
26
|
+
readonly truncatedBy: RepositoryListResult["truncatedBy"];
|
|
27
|
+
readonly scannedEntries: number;
|
|
28
|
+
readonly scannedFiles: number;
|
|
29
|
+
readonly nextOffset?: number;
|
|
30
|
+
readonly offset: number;
|
|
31
|
+
}
|
|
32
|
+
export type RepoSearchOutputMode = "content" | "files_with_matches" | "count";
|
|
16
33
|
export interface RepositorySearchMatch {
|
|
17
34
|
readonly path: string;
|
|
18
35
|
readonly line: number;
|
|
@@ -78,7 +95,8 @@ export interface RepositorySearchRequest {
|
|
|
78
95
|
readonly root: string;
|
|
79
96
|
readonly query: string;
|
|
80
97
|
readonly path?: string;
|
|
81
|
-
readonly mode?: "literal"
|
|
98
|
+
readonly mode?: "literal";
|
|
99
|
+
readonly outputMode?: RepoSearchOutputMode;
|
|
82
100
|
readonly caseSensitive?: boolean;
|
|
83
101
|
readonly includeHidden?: boolean;
|
|
84
102
|
readonly exclude?: readonly string[];
|
|
@@ -87,9 +105,22 @@ export interface RepositorySearchRequest {
|
|
|
87
105
|
readonly signal?: AbortSignal;
|
|
88
106
|
readonly deadlineMs?: number;
|
|
89
107
|
}
|
|
108
|
+
export interface RepositoryGlobRequest {
|
|
109
|
+
readonly root: string;
|
|
110
|
+
readonly pattern: string;
|
|
111
|
+
readonly path?: string;
|
|
112
|
+
readonly includeHidden?: boolean;
|
|
113
|
+
readonly exclude?: readonly string[];
|
|
114
|
+
readonly maxDepth?: number;
|
|
115
|
+
readonly maxResults?: number;
|
|
116
|
+
readonly offset?: number;
|
|
117
|
+
readonly signal?: AbortSignal;
|
|
118
|
+
readonly deadlineMs?: number;
|
|
119
|
+
}
|
|
90
120
|
export interface RepositoryOperations {
|
|
91
121
|
list(request: RepositoryListRequest): Promise<RepositoryListResult>;
|
|
92
122
|
search(request: RepositorySearchRequest): Promise<RepositorySearchResult>;
|
|
123
|
+
glob(request: RepositoryGlobRequest): Promise<RepositoryGlobResult>;
|
|
93
124
|
}
|
|
94
125
|
export declare const DEFAULT_REPO_EXCLUDE: readonly string[];
|
|
95
126
|
export declare class RepositoryError extends Error {
|
|
@@ -109,11 +140,31 @@ export declare function resolveRepoPath(root: string, inputPath: string | undefi
|
|
|
109
140
|
rootReal: string;
|
|
110
141
|
}>;
|
|
111
142
|
export declare function isBinaryBuffer(buffer: Buffer): boolean;
|
|
112
|
-
export declare function compileSearchPattern(query: string,
|
|
143
|
+
export declare function compileSearchPattern(query: string, caseSensitive: boolean, maxPatternBytes: number): {
|
|
113
144
|
testLine: (line: string) => {
|
|
114
145
|
column: number;
|
|
115
146
|
} | null;
|
|
116
147
|
patternBytes: number;
|
|
117
148
|
};
|
|
149
|
+
export interface RepositoryWalkLimits {
|
|
150
|
+
maxDepth: number;
|
|
151
|
+
maxEntries: number;
|
|
152
|
+
maxFiles: number;
|
|
153
|
+
exclude: ReadonlySet<string>;
|
|
154
|
+
includeHidden: boolean;
|
|
155
|
+
signal?: AbortSignal;
|
|
156
|
+
deadlineAt?: number;
|
|
157
|
+
}
|
|
158
|
+
export type RepositoryWalkEvent = {
|
|
159
|
+
type: "entry";
|
|
160
|
+
entry: RepoListEntry;
|
|
161
|
+
absolutePath: string;
|
|
162
|
+
depth: number;
|
|
163
|
+
} | {
|
|
164
|
+
type: "limit";
|
|
165
|
+
truncatedBy: "entries" | "files" | "depth";
|
|
166
|
+
};
|
|
167
|
+
/** Injectable enumerator for list/search/glob. Default is the native opendir walker. */
|
|
168
|
+
export type RepositoryWalk = (rootReal: string, startAbsolute: string, limits: RepositoryWalkLimits) => AsyncGenerator<RepositoryWalkEvent>;
|
|
118
169
|
/** Local filesystem repository operations (default backend). */
|
|
119
|
-
export declare function createLocalRepositoryOperations(limits?: RepositoryLimitOptions): RepositoryOperations;
|
|
170
|
+
export declare function createLocalRepositoryOperations(limits?: RepositoryLimitOptions, walk?: RepositoryWalk): RepositoryOperations;
|
package/dist/repository.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* rejects devices/FIFOs/sockets for descent, and charges finite depth/entry/file
|
|
6
6
|
* limits before retaining the next result. No glob/index/watcher dependency.
|
|
7
7
|
*/
|
|
8
|
-
import { open, opendir,
|
|
8
|
+
import { lstat, open, opendir, realpath } from "node:fs/promises";
|
|
9
9
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { DEFAULT_BINARY_SNIFF_BYTES, DEFAULT_MAX_REPO_CONCURRENCY, DEFAULT_MAX_REPO_DEPTH, DEFAULT_MAX_REPO_ENTRIES, DEFAULT_MAX_REPO_FILES, DEFAULT_MAX_REPO_RESULTS, DEFAULT_MAX_SEARCH_CONTEXT_LINES, DEFAULT_MAX_SEARCH_FILE_BYTES, DEFAULT_MAX_SEARCH_LINE_BYTES, DEFAULT_MAX_SEARCH_MATCHES, DEFAULT_MAX_SEARCH_PATTERN_BYTES, DEFAULT_MAX_SEARCH_SCAN_BYTES, DEFAULT_MAX_SEARCH_TIME_MS, HARD_MAX_REPO_CONCURRENCY, HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_ENTRIES, HARD_MAX_REPO_FILES, HARD_MAX_REPO_RESULTS, HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_FILE_BYTES, HARD_MAX_SEARCH_LINE_BYTES, HARD_MAX_SEARCH_MATCHES, HARD_MAX_SEARCH_PATTERN_BYTES, HARD_MAX_SEARCH_SCAN_BYTES, HARD_MAX_SEARCH_TIME_MS, validateCodingLimit, validateCodingLimitAllowZero, } from "./limits.js";
|
|
11
|
+
import { matchGlobPattern, validateGlobPattern } from "./glob-match.js";
|
|
11
12
|
import { resolveToCwd } from "./path-utils.js";
|
|
12
13
|
export const DEFAULT_REPO_EXCLUDE = Object.freeze([".git", "node_modules", "dist"]);
|
|
13
14
|
export class RepositoryError extends Error {
|
|
@@ -119,46 +120,28 @@ export function isBinaryBuffer(buffer) {
|
|
|
119
120
|
}
|
|
120
121
|
return false;
|
|
121
122
|
}
|
|
122
|
-
export function compileSearchPattern(query,
|
|
123
|
+
export function compileSearchPattern(query, caseSensitive, maxPatternBytes) {
|
|
123
124
|
const patternBytes = Buffer.byteLength(query, "utf8");
|
|
124
125
|
if (patternBytes < 1)
|
|
125
126
|
throw new RepositoryError("query must be non-empty");
|
|
126
127
|
if (patternBytes > maxPatternBytes) {
|
|
127
128
|
throw new RepositoryError(`query exceeds ${maxPatternBytes} byte pattern limit`);
|
|
128
129
|
}
|
|
129
|
-
if (
|
|
130
|
-
if (caseSensitive) {
|
|
131
|
-
return {
|
|
132
|
-
patternBytes,
|
|
133
|
-
testLine: (line) => {
|
|
134
|
-
const column = line.indexOf(query);
|
|
135
|
-
return column >= 0 ? { column: column + 1 } : null;
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
const needle = query.toLowerCase();
|
|
130
|
+
if (caseSensitive) {
|
|
140
131
|
return {
|
|
141
132
|
patternBytes,
|
|
142
133
|
testLine: (line) => {
|
|
143
|
-
const column = line.
|
|
134
|
+
const column = line.indexOf(query);
|
|
144
135
|
return column >= 0 ? { column: column + 1 } : null;
|
|
145
136
|
},
|
|
146
137
|
};
|
|
147
138
|
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
regex = new RegExp(query, caseSensitive ? "u" : "iu");
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
-
throw new RepositoryError(`invalid regular expression: ${message}`);
|
|
155
|
-
}
|
|
139
|
+
const needle = query.toLowerCase();
|
|
156
140
|
return {
|
|
157
141
|
patternBytes,
|
|
158
142
|
testLine: (line) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return match && match.index !== undefined ? { column: match.index + 1 } : null;
|
|
143
|
+
const column = line.toLowerCase().indexOf(needle);
|
|
144
|
+
return column >= 0 ? { column: column + 1 } : null;
|
|
162
145
|
},
|
|
163
146
|
};
|
|
164
147
|
}
|
|
@@ -256,7 +239,7 @@ async function* walkRepository(rootReal, startAbsolute, limits) {
|
|
|
256
239
|
}
|
|
257
240
|
}
|
|
258
241
|
}
|
|
259
|
-
async function listLocal(request, defaults) {
|
|
242
|
+
async function listLocal(request, defaults, walk) {
|
|
260
243
|
const resolved = await resolveRepoPath(request.root, request.path);
|
|
261
244
|
const maxResults = validateCodingLimit("maxResults", request.maxResults ?? defaults.maxResults, HARD_MAX_REPO_RESULTS);
|
|
262
245
|
const offset = validateCodingLimitAllowZero("offset", request.offset ?? 0, HARD_MAX_REPO_ENTRIES);
|
|
@@ -278,9 +261,7 @@ async function listLocal(request, defaults) {
|
|
|
278
261
|
kind = "symlink";
|
|
279
262
|
else if (startStat.isFile())
|
|
280
263
|
kind = "file";
|
|
281
|
-
const entry = kind === "file"
|
|
282
|
-
? { path: resolved.relative, kind, size: startStat.size }
|
|
283
|
-
: { path: resolved.relative, kind };
|
|
264
|
+
const entry = kind === "file" ? { path: resolved.relative, kind, size: startStat.size } : { path: resolved.relative, kind };
|
|
284
265
|
scannedEntries = 1;
|
|
285
266
|
scannedFiles = kind === "file" ? 1 : 0;
|
|
286
267
|
if (offset === 0 && maxResults > 0)
|
|
@@ -305,7 +286,7 @@ async function listLocal(request, defaults) {
|
|
|
305
286
|
throw new RepositoryError(`cannot open path: ${message}`);
|
|
306
287
|
}
|
|
307
288
|
try {
|
|
308
|
-
for await (const event of
|
|
289
|
+
for await (const event of walk(resolved.rootReal, resolved.absolute, {
|
|
309
290
|
maxDepth,
|
|
310
291
|
maxEntries: defaults.maxEntries,
|
|
311
292
|
maxFiles: defaults.maxFiles,
|
|
@@ -477,13 +458,13 @@ async function searchFileLines(absolutePath, relativePath, testLine, options) {
|
|
|
477
458
|
await handle.close();
|
|
478
459
|
}
|
|
479
460
|
}
|
|
480
|
-
async function searchLocal(request, defaults) {
|
|
461
|
+
async function searchLocal(request, defaults, walk) {
|
|
481
462
|
const mode = request.mode ?? "literal";
|
|
482
|
-
if (mode !== "literal"
|
|
483
|
-
throw new RepositoryError(`unsupported search mode: ${String(mode)}`);
|
|
463
|
+
if (mode !== "literal") {
|
|
464
|
+
throw new RepositoryError(`unsupported search mode: ${String(mode)} (literal only)`);
|
|
484
465
|
}
|
|
485
466
|
const caseSensitive = request.caseSensitive === true;
|
|
486
|
-
const { testLine } = compileSearchPattern(request.query,
|
|
467
|
+
const { testLine } = compileSearchPattern(request.query, caseSensitive, defaults.maxPatternBytes);
|
|
487
468
|
const resolved = await resolveRepoPath(request.root, request.path);
|
|
488
469
|
const maxMatches = validateCodingLimit("maxMatches", request.maxMatches ?? defaults.maxMatches, HARD_MAX_SEARCH_MATCHES);
|
|
489
470
|
const context = validateCodingLimitAllowZero("context", request.context ?? defaults.maxContextLines, HARD_MAX_SEARCH_CONTEXT_LINES);
|
|
@@ -538,7 +519,7 @@ async function searchLocal(request, defaults) {
|
|
|
538
519
|
await runFile(resolved.absolute, resolved.relative);
|
|
539
520
|
}
|
|
540
521
|
else if (startStat.isDirectory()) {
|
|
541
|
-
for await (const event of
|
|
522
|
+
for await (const event of walk(resolved.rootReal, resolved.absolute, {
|
|
542
523
|
maxDepth: defaults.maxDepth,
|
|
543
524
|
maxEntries: defaults.maxEntries,
|
|
544
525
|
maxFiles: defaults.maxFiles,
|
|
@@ -622,12 +603,137 @@ async function searchLocal(request, defaults) {
|
|
|
622
603
|
filesSkippedOversize,
|
|
623
604
|
};
|
|
624
605
|
}
|
|
606
|
+
async function globLocal(request, defaults, walk) {
|
|
607
|
+
try {
|
|
608
|
+
validateGlobPattern(request.pattern, defaults.maxPatternBytes);
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
throw new RepositoryError(error instanceof Error ? error.message : String(error));
|
|
612
|
+
}
|
|
613
|
+
const resolved = await resolveRepoPath(request.root, request.path);
|
|
614
|
+
const maxResults = validateCodingLimit("maxResults", request.maxResults ?? defaults.maxResults, HARD_MAX_REPO_RESULTS);
|
|
615
|
+
const offset = validateCodingLimitAllowZero("offset", request.offset ?? 0, HARD_MAX_REPO_ENTRIES);
|
|
616
|
+
const maxDepth = validateCodingLimit("maxDepth", request.maxDepth ?? defaults.maxDepth, HARD_MAX_REPO_DEPTH);
|
|
617
|
+
const exclude = new Set(request.exclude ?? defaults.exclude);
|
|
618
|
+
const deadlineAt = request.deadlineMs !== undefined ? Date.now() + request.deadlineMs : Date.now() + defaults.maxTimeMs;
|
|
619
|
+
const collected = [];
|
|
620
|
+
let scannedEntries = 0;
|
|
621
|
+
let scannedFiles = 0;
|
|
622
|
+
let seen = 0;
|
|
623
|
+
let truncated = false;
|
|
624
|
+
let truncatedBy = null;
|
|
625
|
+
const maybeCollect = (relativePath) => {
|
|
626
|
+
if (!matchGlobPattern(request.pattern, relativePath))
|
|
627
|
+
return false;
|
|
628
|
+
if (seen < offset) {
|
|
629
|
+
seen++;
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
if (collected.length >= maxResults) {
|
|
633
|
+
truncated = true;
|
|
634
|
+
truncatedBy = "results";
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
collected.push(relativePath);
|
|
638
|
+
seen++;
|
|
639
|
+
return truncated;
|
|
640
|
+
};
|
|
641
|
+
try {
|
|
642
|
+
const startStat = await lstat(resolved.absolute);
|
|
643
|
+
if (!startStat.isDirectory()) {
|
|
644
|
+
scannedEntries = 1;
|
|
645
|
+
if (startStat.isFile()) {
|
|
646
|
+
scannedFiles = 1;
|
|
647
|
+
if (matchGlobPattern(request.pattern, resolved.relative)) {
|
|
648
|
+
if (offset === 0 && maxResults > 0)
|
|
649
|
+
collected.push(resolved.relative);
|
|
650
|
+
else if (offset === 0 && maxResults === 0) {
|
|
651
|
+
truncated = true;
|
|
652
|
+
truncatedBy = "results";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
paths: collected,
|
|
658
|
+
truncated,
|
|
659
|
+
truncatedBy,
|
|
660
|
+
scannedEntries,
|
|
661
|
+
scannedFiles,
|
|
662
|
+
offset,
|
|
663
|
+
nextOffset: undefined,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
669
|
+
throw new RepositoryError(`cannot open path: ${message}`);
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
for await (const event of walk(resolved.rootReal, resolved.absolute, {
|
|
673
|
+
maxDepth,
|
|
674
|
+
maxEntries: defaults.maxEntries,
|
|
675
|
+
maxFiles: defaults.maxFiles,
|
|
676
|
+
exclude,
|
|
677
|
+
includeHidden: request.includeHidden === true,
|
|
678
|
+
signal: request.signal,
|
|
679
|
+
deadlineAt,
|
|
680
|
+
})) {
|
|
681
|
+
if (event.type === "limit") {
|
|
682
|
+
truncated = true;
|
|
683
|
+
truncatedBy = event.truncatedBy;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
scannedEntries++;
|
|
687
|
+
if (event.entry.kind === "file")
|
|
688
|
+
scannedFiles++;
|
|
689
|
+
if (event.entry.kind !== "file")
|
|
690
|
+
continue;
|
|
691
|
+
if (maybeCollect(event.entry.path))
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
if (error instanceof RepositoryError && error.message === "Operation aborted") {
|
|
697
|
+
return {
|
|
698
|
+
paths: collected,
|
|
699
|
+
truncated: true,
|
|
700
|
+
truncatedBy: "abort",
|
|
701
|
+
scannedEntries,
|
|
702
|
+
scannedFiles,
|
|
703
|
+
offset,
|
|
704
|
+
nextOffset: collected.length > 0 || offset > 0 ? offset + collected.length : undefined,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (error instanceof RepositoryError && error.message === "Repository operation exceeded time limit") {
|
|
708
|
+
return {
|
|
709
|
+
paths: collected,
|
|
710
|
+
truncated: true,
|
|
711
|
+
truncatedBy: "time",
|
|
712
|
+
scannedEntries,
|
|
713
|
+
scannedFiles,
|
|
714
|
+
offset,
|
|
715
|
+
nextOffset: offset + collected.length,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
paths: collected,
|
|
722
|
+
truncated,
|
|
723
|
+
truncatedBy,
|
|
724
|
+
scannedEntries,
|
|
725
|
+
scannedFiles,
|
|
726
|
+
offset,
|
|
727
|
+
nextOffset: truncated ? offset + collected.length : undefined,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
625
730
|
/** Local filesystem repository operations (default backend). */
|
|
626
|
-
export function createLocalRepositoryOperations(limits) {
|
|
731
|
+
export function createLocalRepositoryOperations(limits, walk = walkRepository) {
|
|
627
732
|
const resolved = resolveRepositoryLimits(limits);
|
|
628
733
|
return {
|
|
629
|
-
list: (request) => listLocal(request, resolved),
|
|
630
|
-
search: (request) => searchLocal(request, resolved),
|
|
734
|
+
list: (request) => listLocal(request, resolved, walk),
|
|
735
|
+
search: (request) => searchLocal(request, resolved, walk),
|
|
736
|
+
glob: (request) => globLocal(request, resolved, walk),
|
|
631
737
|
};
|
|
632
738
|
}
|
|
633
739
|
//# sourceMappingURL=repository.js.map
|
package/dist/search.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `repo_search` tool: bounded native literal
|
|
2
|
+
* `repo_search` tool: bounded native literal repository text search.
|
|
3
3
|
*/
|
|
4
4
|
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
5
5
|
import { type RepositoryLimitOptions, type RepositoryOperations } from "./repository.js";
|