@arnilo/prism-coding-agent 0.0.20 → 0.0.22

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.22] - 2026-07-31
4
+
5
+ ### Changed
6
+ - Released with exact 0.0.22 graph.
7
+
8
+ ## [0.0.21] - 2026-07-31
9
+
10
+ ### Added
11
+ - `repo_search` `outputMode` (`content` | `files_with_matches` | `count`).
12
+ - Bounded `glob` tool (`*`/`?`/`**`; no brace expansion).
13
+ - Optional session-scoped `requireReadBeforeWrite` + `ReadPathSet` + `force` on write/edit.
14
+ - Bounded `delete` (file or empty dir) and `move` tools with dual-path mutation queue.
15
+
16
+ ### Changed
17
+ - `createCodingTools` returns 9 tools; `createReadOnlyTools` returns 4 (includes `glob`).
18
+
19
+
20
+
3
21
  ## [0.0.20] - 2026-07-31
4
22
 
5
23
  ### Changed
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @arnilo/prism-coding-agent
2
2
 
3
- Optional first-party coding tools package for [Prism](https://www.npmjs.com/package/@arnilo/prism). Provides host shell/filesystem/repository tools — `shell`, `read`, `write`, `edit`, `repo_list`, `repo_search` — plus opt-in structured Git/check tools via `createGitTools()`, opt-in `createAskUserDecisionTool({ ask })`, and durable plan/checkpoint helpers for workflow composition — as Prism `ToolDefinition` objects. **Inert until a host imports it and registers the tools into a `ToolRegistry`.** No tool is auto-registered; hosts pick factories (or filter aggregator output) and may mix in their own `ToolDefinition`s.
3
+ Optional first-party coding tools package for [Prism](https://www.npmjs.com/package/@arnilo/prism). Provides host shell/filesystem/repository tools — `shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`, `glob`, `delete`, `move` — plus opt-in structured Git/check tools via `createGitTools()`, opt-in `createAskUserDecisionTool({ ask })`, and durable plan/checkpoint helpers for workflow composition — as Prism `ToolDefinition` objects. **Inert until a host imports it and registers the tools into a `ToolRegistry`.** No tool is auto-registered; hosts pick factories (or filter aggregator output) and may mix in their own `ToolDefinition`s.
4
4
 
5
- Behavior is a behavioral port of the pi coding agent's `bash`/`read`/`write`/`edit` tools, adapted to Prism's `ToolDefinition` / `ToolResult` contracts (no `@earendil-works/*` or `typebox` dependencies). List/search/Git are native Prism tools.
5
+ Behavior is a behavioral port of the pi coding agent's `bash`/`read`/`write`/`edit` tools, adapted to Prism's `ToolDefinition` / `ToolResult` contracts (no `@earendil-works/*` or `typebox` dependencies). List/search/glob/Git are native Prism tools (hand-rolled glob; no picomatch/ripgrep).
6
6
 
7
7
  > ⚠️ **These tools perform real shell and filesystem operations on the host. They provide no sandbox.** Gate them with Prism `PermissionPolicy` / `ToolValidator` / trust policies before registering them for any provider turn. For disposable sandbox composition with required `workspaceMode`, use `@arnilo/prism-coding-security` (`createSandboxCodingComposition`). See the [coding agent tools docs](https://github.com/ashiqrniloy/prism/blob/main/docs/coding-agent-tools.md) and the [host security guide](https://github.com/ashiqrniloy/prism/blob/main/docs/host-security.md).
8
8
 
@@ -16,7 +16,7 @@ npm install @arnilo/prism-coding-agent
16
16
 
17
17
  ## Usage
18
18
 
19
- Register the full coding set:
19
+ Register the full coding set (nine tools):
20
20
 
21
21
  ```ts
22
22
  import { createToolRegistry } from "@arnilo/prism";
@@ -25,7 +25,7 @@ import { createCodingTools } from "@arnilo/prism-coding-agent";
25
25
  const tools = createToolRegistry(createCodingTools(process.cwd()));
26
26
  ```
27
27
 
28
- Read-only subset (inspection-only agents):
28
+ Read-only subset (inspection-only agents — includes `glob`):
29
29
 
30
30
  ```ts
31
31
  import { createReadOnlyTools } from "@arnilo/prism-coding-agent";
@@ -38,7 +38,14 @@ Shared `ToolsOptions.executionPolicy` applies to every tool returned by full, al
38
38
  Individual tools with options:
39
39
 
40
40
  ```ts
41
- import { createShellTool, createWriteTool, createAskUserDecisionTool } from "@arnilo/prism-coding-agent";
41
+ import {
42
+ createShellTool,
43
+ createWriteTool,
44
+ createAskUserDecisionTool,
45
+ createReadPathSet,
46
+ createReadTool,
47
+ createEditTool,
48
+ } from "@arnilo/prism-coding-agent";
42
49
 
43
50
  const shell = createShellTool(process.cwd(), {
44
51
  shellPath: "/bin/bash", // force bash; default: SHELL env → /bin/bash → sh
@@ -55,6 +62,12 @@ const remoteWrite = createWriteTool(process.cwd(), {
55
62
  },
56
63
  });
57
64
 
65
+ // Optional soft guard: share one ReadPathSet across read/write/edit.
66
+ const readPaths = createReadPathSet();
67
+ const read = createReadTool(process.cwd(), { readPathSet: readPaths });
68
+ const write = createWriteTool(process.cwd(), { requireReadBeforeWrite: true, readPathSet: readPaths });
69
+ const edit = createEditTool(process.cwd(), { requireReadBeforeWrite: true, readPathSet: readPaths });
70
+
58
71
  // Opt-in: not in createCodingTools(). Host owns the UI.
59
72
  const askUser = createAskUserDecisionTool({
60
73
  ask: async ({ question, options }) => {
@@ -68,14 +81,17 @@ const askUser = createAskUserDecisionTool({
68
81
 
69
82
  | Tool | Input | Result |
70
83
  | --- | --- | --- |
71
- | `shell` | `{ command, timeout? }` | Combined output + `metadata.exitCode`; 600-second default timeout and 64 MiB total-output cap. Non-zero exit is **not** an error. |
72
- | `read` | `{ path, offset?, limit? }` | Streamed bounded text page or bounded `[note, ImageContent]`. |
73
- | `write` | `{ path, content }` | Bounded UTF-8 input; `Successfully wrote N bytes (M lines) to <abs>`. |
74
- | `edit` | `{ path, edits: [{oldText,newText}] }` | Bounded target/input/count; `Successfully replaced N block(s)` + diff metadata. |
75
- | `repo_list` | `{ path?, includeHidden?, maxDepth?, maxResults?, offset? }` | Deterministic relative entries; skips hidden/excluded basenames; does not follow symlinks; paginates with `nextOffset`. |
76
- | `repo_search` | `{ query, path?, mode?, caseSensitive?, includeHidden?, context?, maxMatches? }` | Literal substring matches with context; skips binary/excluded paths; finite scan/match/time caps. |
77
- | `git_*` / `coding_check` | via `createGitTools(cwd, { commitIdentity, checks? })` | Opt-in structured Git status/diff/branch/worktree/apply/commit/PR-handoff and named checks. Not in `createCodingTools()`. |
78
- | `ask_user_decision` | via `createAskUserDecisionTool({ ask })` | Opt-in user choice: question + options (3 pros/3 cons); `selectionMode` single\|multiple; `allowCustom` for XOR free-text; host `ask` returns `selectedId` / `selectedIds` / `customText`. Durable: `suspendAskUserDecision` + resume validators. Not in default aggregators. |
84
+ | `shell` | `{ command, timeout? }` | Combined output + `metadata.exitCode`; 600-second default timeout and 64 MiB total-output cap. Non-zero exit is **not** an error. Prefer dedicated tools when they fit. |
85
+ | `read` | `{ path, offset?, limit? }` | Streamed bounded text page or bounded `[note, ImageContent]`. Continue with suggested offset when truncated. |
86
+ | `write` | `{ path, content, force? }` | Full overwrite; bounded UTF-8 input; optional read-before-write. |
87
+ | `edit` | `{ path, edits: [{oldText,newText}], force? }` | Exact-then-fuzzy replace; **fuzzy may succeed silently** — prefer exact `oldText`; duplicates fail closed. |
88
+ | `repo_list` | `{ path?, includeHidden?, maxDepth?, maxResults?, offset? }` | Deterministic relative entries; paginates with `nextOffset`. Prefer `glob` for patterns. |
89
+ | `repo_search` | `{ query, path?, mode?, caseSensitive?, includeHidden?, context?, maxMatches?, outputMode? }` | Literal search; `outputMode`: `content` \| `files_with_matches` \| `count`. |
90
+ | `glob` | `{ pattern, path?, includeHidden?, maxDepth?, maxResults?, offset? }` | Filename match (`*`/`?`/`**`; no braces). Files only. |
91
+ | `delete` | `{ path }` | High-risk: file or empty dir only; **no trash**. |
92
+ | `move` | `{ from, to, overwrite? }` | High-risk rename/move; `overwrite` default false; **no trash**. |
93
+ | `git_*` / `coding_check` | via `createGitTools(cwd, { commitIdentity, checks? })` | Opt-in structured Git + named checks. Not in `createCodingTools()`. |
94
+ | `ask_user_decision` | via `createAskUserDecisionTool({ ask })` | Opt-in user choice. Not in default aggregators. |
79
95
 
80
96
  ### pi name mapping
81
97
 
@@ -83,19 +99,23 @@ const askUser = createAskUserDecisionTool({
83
99
  | --- | --- |
84
100
  | `shell` | `bash` |
85
101
  | `read` / `write` / `edit` | `read` / `write` / `edit` |
86
- | `repo_list` / `repo_search` | _(native; no pi equivalent shipped)_ |
102
+ | `repo_list` / `repo_search` / `glob` / `delete` / `move` | _(native; no pi equivalent shipped)_ |
103
+
104
+ ### Phase 4 non-goals
105
+
106
+ No PDF reader, trash daemon, PTY, or LSP tools in 0.0.21. See [coding agent tools docs](https://github.com/ashiqrniloy/prism/blob/main/docs/coding-agent-tools.md).
87
107
 
88
108
  ## Exports
89
109
 
90
- Factories: `createShellTool`, `createReadTool`, `createWriteTool`, `createEditTool`, `createRepoListTool`, `createRepoSearchTool`, `createCodingTools`, `createReadOnlyTools`, `createAllTools`, `createGitTools`, `createCodingCheckTool`, `createAskUserDecisionTool`, `createLocalBashOperations`, `createLocalRepositoryOperations`, `createGitOperations`.
110
+ Factories: `createShellTool`, `createReadTool`, `createWriteTool`, `createEditTool`, `createRepoListTool`, `createRepoSearchTool`, `createGlobTool`, `createDeleteTool`, `createMoveTool`, `createCodingTools`, `createReadOnlyTools`, `createAllTools`, `createGitTools`, `createCodingCheckTool`, `createAskUserDecisionTool`, `createLocalBashOperations`, `createLocalRepositoryOperations`, `createGitOperations`, `createReadPathSet`.
91
111
 
92
- Helpers: `detectSupportedImageMimeType`, `detectSupportedImageMimeTypeFromFile`, `getShellConfig`, `killProcessTree`, `waitForChildProcess`, `withFileMutationQueue`, `resolveRepositoryLimits`, `writeCodingPlanFile`, `readCodingPlanFile`, `buildCodingCheckpointMetadata`, `validateCodingCheckpointMetadata`, `assertCodingResumeAllowed`, `fingerprintJson`, `runCodingGoalVerify`, `createCodingGoalVerifyWorkflow`, `suspendAskUserDecision`, `createAskUserDecisionResumeValidator`, `validateAskUserDecisionResume`, `validateAskUserDecisionAgentResume`. Default/hard coding, repository, Git, and plan/checkpoint limit constants are exported for host configuration.
112
+ Helpers: `detectSupportedImageMimeType`, `detectSupportedImageMimeTypeFromFile`, `getShellConfig`, `killProcessTree`, `waitForChildProcess`, `withFileMutationQueue`, `resolveRepositoryLimits`, `matchGlobPattern`, `validateGlobPattern`, `writeCodingPlanFile`, `readCodingPlanFile`, `buildCodingCheckpointMetadata`, `validateCodingCheckpointMetadata`, `assertCodingResumeAllowed`, `fingerprintJson`, `runCodingGoalVerify`, `createCodingGoalVerifyWorkflow`, `suspendAskUserDecision`, `createAskUserDecisionResumeValidator`, `validateAskUserDecisionResume`, `validateAskUserDecisionAgentResume`. Default/hard coding, repository, Git, and plan/checkpoint limit constants are exported for host configuration.
93
113
 
94
- Option/operation types: `ToolsOptions`, `ShellToolOptions`/`BashOperations`, `ReadToolOptions`/`ReadOperations`/`ReadTextOptions`/`ReadTextResult`, `WriteToolOptions`/`WriteOperations`, `EditToolOptions`/`EditOperations`/`EditToolDetails`.
114
+ Option/operation types: `ToolsOptions`, `ShellToolOptions`/`BashOperations`, `ReadToolOptions`/`ReadOperations`/`ReadTextOptions`/`ReadTextResult`, `WriteToolOptions`/`WriteOperations`, `EditToolOptions`/`EditOperations`/`EditToolDetails`, `DeleteToolOptions`/`DeleteOperations`, `MoveToolOptions`/`MoveOperations`, `GlobToolOptions`, `ReadPathSet`.
95
115
 
96
116
  Text reads stop after one page or `maxScanBytes` instead of loading the file. Custom `ReadOperations` must implement bounded `readText` and `statFile`; custom `EditOperations` must implement `statFile` and honor the supplied read cap/signal. Successful truncated shell output is retained in an exclusive Unix `0600` temp file owned by the host; timeout, abort, output-limit, and spill failures remove unpublished spill files. Hosts should delete published `metadata.fullOutputPath` files after use.
97
117
 
98
- Network-free adversarial evaluation fixtures live in `src/__tests__/eval-fixtures.test.ts` and reuse `@arnilo/prism-evals` for CI thresholds. See `examples/coding-browser-evaluation.ts` and `docs/evaluations.md`.
118
+ Network-free adversarial evaluation fixtures live in `src/__tests__/eval-fixtures.test.ts` and reuse `@arnilo/prism-evals` for CI thresholds. See `examples/coding-browser-evaluation.ts`, `examples/coding-tools-capability-gaps.ts`, and `docs/evaluations.md`.
99
119
 
100
120
  ## License
101
121
 
@@ -0,0 +1,26 @@
1
+ import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
2
+ export interface MutationStat {
3
+ isFile(): boolean;
4
+ isDirectory(): boolean;
5
+ isSymbolicLink(): boolean;
6
+ size: number;
7
+ }
8
+ export interface DeleteOperations {
9
+ lstat: (absolutePath: string, options?: {
10
+ signal?: AbortSignal;
11
+ }) => Promise<MutationStat>;
12
+ unlink: (absolutePath: string, options?: {
13
+ signal?: AbortSignal;
14
+ }) => Promise<void>;
15
+ rmdir: (absolutePath: string, options?: {
16
+ signal?: AbortSignal;
17
+ }) => Promise<void>;
18
+ readdir: (absolutePath: string, options?: {
19
+ signal?: AbortSignal;
20
+ }) => Promise<readonly string[]>;
21
+ }
22
+ export interface DeleteToolOptions {
23
+ executionPolicy?: ExecutionPolicy;
24
+ operations?: DeleteOperations;
25
+ }
26
+ export declare function createDeleteTool(cwd: string, options?: DeleteToolOptions): ToolDefinition;
package/dist/delete.js ADDED
@@ -0,0 +1,115 @@
1
+ import { lstat, readdir, rmdir, unlink } from "node:fs/promises";
2
+ import { enforceExecutionPolicy } from "./execution-policy.js";
3
+ import { withFileMutationQueue } from "./file-mutation-queue.js";
4
+ import { resolveContainedMutationPath } from "./mutation-path.js";
5
+ const defaultDeleteOperations = {
6
+ lstat: async (path) => {
7
+ const st = await lstat(path);
8
+ return {
9
+ isFile: () => st.isFile(),
10
+ isDirectory: () => st.isDirectory(),
11
+ isSymbolicLink: () => st.isSymbolicLink(),
12
+ size: st.size,
13
+ };
14
+ },
15
+ unlink: (path) => unlink(path).then(() => { }),
16
+ rmdir: (path) => rmdir(path).then(() => { }),
17
+ readdir: (path) => readdir(path),
18
+ };
19
+ function errorResult(toolCallId, message) {
20
+ return {
21
+ toolCallId,
22
+ name: "delete",
23
+ content: [{ type: "text", text: message }],
24
+ error: { message },
25
+ };
26
+ }
27
+ export function createDeleteTool(cwd, options) {
28
+ const ops = options?.operations ?? defaultDeleteOperations;
29
+ return {
30
+ name: "delete",
31
+ description: "High-risk: permanently delete a single file or empty directory in the workspace. Non-empty directories are rejected (no recursive delete). No trash/recycle — host undo is not automatic. Prefer edit/write when content can be fixed in place.",
32
+ parameters: {
33
+ type: "object",
34
+ properties: {
35
+ path: { type: "string", description: "Path to the file or empty directory to delete (relative or absolute)" },
36
+ },
37
+ required: ["path"],
38
+ additionalProperties: false,
39
+ },
40
+ async execute(args, context) {
41
+ const toolCallId = context.toolCallId;
42
+ const path = typeof args.path === "string" ? args.path : "";
43
+ if (path.length === 0) {
44
+ return errorResult(toolCallId, "path is required and must be a non-empty string.");
45
+ }
46
+ try {
47
+ let absolutePath;
48
+ try {
49
+ absolutePath = await resolveContainedMutationPath(cwd, path);
50
+ }
51
+ catch (error) {
52
+ const err = error;
53
+ if (err.code === "ENOENT")
54
+ return errorResult(toolCallId, `No such file or directory: ${path}`);
55
+ throw error;
56
+ }
57
+ const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
58
+ kind: "delete",
59
+ operation: "delete",
60
+ paths: [absolutePath],
61
+ risk: "high",
62
+ metadata: { sessionId: context.sessionId, runId: context.runId, signal: context.signal },
63
+ }, toolCallId, "delete");
64
+ if (!policyCheck.allowed)
65
+ return policyCheck.result;
66
+ const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
67
+ return await withFileMutationQueue(allowedPath, async () => {
68
+ if (context.signal?.aborted)
69
+ return errorResult(toolCallId, "Operation aborted");
70
+ let st;
71
+ try {
72
+ st = await ops.lstat(allowedPath, { signal: context.signal });
73
+ }
74
+ catch (error) {
75
+ const err = error;
76
+ if (err.code === "ENOENT")
77
+ return errorResult(toolCallId, `No such file or directory: ${path}`);
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ return errorResult(toolCallId, message);
80
+ }
81
+ if (context.signal?.aborted)
82
+ return errorResult(toolCallId, "Operation aborted");
83
+ if (st.isFile() || st.isSymbolicLink()) {
84
+ await ops.unlink(allowedPath, { signal: context.signal });
85
+ return {
86
+ toolCallId,
87
+ name: "delete",
88
+ content: [{ type: "text", text: `Successfully deleted ${allowedPath}` }],
89
+ metadata: { path: allowedPath, kind: st.isSymbolicLink() ? "symlink" : "file", bytes: st.size },
90
+ };
91
+ }
92
+ if (st.isDirectory()) {
93
+ const entries = await ops.readdir(allowedPath, { signal: context.signal });
94
+ if (entries.length > 0) {
95
+ return errorResult(toolCallId, `Directory is not empty: ${path}. Recursive delete is not supported.`);
96
+ }
97
+ await ops.rmdir(allowedPath, { signal: context.signal });
98
+ return {
99
+ toolCallId,
100
+ name: "delete",
101
+ content: [{ type: "text", text: `Successfully deleted empty directory ${allowedPath}` }],
102
+ metadata: { path: allowedPath, kind: "directory" },
103
+ };
104
+ }
105
+ return errorResult(toolCallId, `Unsupported file type: ${path}`);
106
+ });
107
+ }
108
+ catch (error) {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ return errorResult(toolCallId, message);
111
+ }
112
+ },
113
+ };
114
+ }
115
+ //# sourceMappingURL=delete.js.map
package/dist/edit.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 ReadBeforeWriteOptions } from "./read-path-set.js";
24
25
  export interface Edit {
25
26
  oldText: string;
26
27
  newText: string;
@@ -59,7 +60,7 @@ export interface EditOperations {
59
60
  size: number;
60
61
  }>;
61
62
  }
62
- export interface EditToolOptions {
63
+ export interface EditToolOptions extends ReadBeforeWriteOptions {
63
64
  /** Structured pre-execution policy checked before filesystem writes. */
64
65
  executionPolicy?: ExecutionPolicy;
65
66
  /** Custom operations backend (default: local filesystem). */
package/dist/edit.js CHANGED
@@ -29,6 +29,7 @@ import { enforceExecutionPolicy } from "./execution-policy.js";
29
29
  import { withFileMutationQueue } from "./file-mutation-queue.js";
30
30
  import { DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, validateCodingLimit, } from "./limits.js";
31
31
  import { resolveToCwd } from "./path-utils.js";
32
+ import { refuseReadBeforeWrite } from "./read-path-set.js";
32
33
  const defaultEditOperations = {
33
34
  readFile: (path, options) => readFileBounded(path, options.maxBytes, options.signal),
34
35
  writeFile: (path, content, options) => atomicWriteUtf8File(path, content, { signal: options?.signal }),
@@ -94,11 +95,15 @@ export function createEditTool(cwd, options) {
94
95
  const maxEdits = validateCodingLimit("maxEdits", options?.maxEdits ?? DEFAULT_MAX_EDITS, HARD_MAX_EDITS);
95
96
  return {
96
97
  name: "edit",
97
- description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
98
+ description: "Edit a single file using exact-then-fuzzy text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. Exact match is tried first; if it fails, fuzzy match (unicode normalize + whitespace collapse) may still succeed silently — prefer exact oldText to avoid wrong-region edits. Duplicate/ambiguous matches fail closed and leave the file unchanged. If two changes affect the same block or nearby lines, merge them into one edit. Do not include large unchanged regions just to connect distant changes. When the host enabled requireReadBeforeWrite, read the path first or pass force=true.",
98
99
  parameters: {
99
100
  type: "object",
100
101
  properties: {
101
102
  path: { type: "string", description: "Path to the file to edit (relative or absolute)" },
103
+ force: {
104
+ type: "boolean",
105
+ description: "Bypass read-before-write guard when the host enabled requireReadBeforeWrite.",
106
+ },
102
107
  edits: {
103
108
  type: "array",
104
109
  description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.",
@@ -125,6 +130,7 @@ export function createEditTool(cwd, options) {
125
130
  return errorResult(toolCallId, `edit input exceeds ${maxInputBytes} byte limit.`);
126
131
  }
127
132
  const prepared = prepareEditArguments(args);
133
+ const force = args.force === true;
128
134
  if (prepared.path.length === 0) {
129
135
  return errorResult(toolCallId, "path is required and must be a non-empty string.");
130
136
  }
@@ -145,6 +151,9 @@ export function createEditTool(cwd, options) {
145
151
  if (!policyCheck.allowed)
146
152
  return policyCheck.result;
147
153
  const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
154
+ const rbwRefusal = refuseReadBeforeWrite("edit", prepared.path, allowedPath, options, force);
155
+ if (rbwRefusal)
156
+ return errorResult(toolCallId, rbwRefusal);
148
157
  return await withFileMutationQueue(allowedPath, async () => {
149
158
  if (context.signal?.aborted)
150
159
  return errorResult(toolCallId, "Operation aborted");
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Minimal glob matcher: `*`, `?`, and `**` only. No brace expansion, no regex.
3
+ */
4
+ export declare function validateGlobPattern(pattern: string, maxPatternBytes: number): void;
5
+ /** Match a workspace-relative path against a glob pattern using `/` separators. */
6
+ export declare function matchGlobPattern(pattern: string, path: string): boolean;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Minimal glob matcher: `*`, `?`, and `**` only. No brace expansion, no regex.
3
+ */
4
+ export function validateGlobPattern(pattern, maxPatternBytes) {
5
+ const patternBytes = Buffer.byteLength(pattern, "utf8");
6
+ if (patternBytes < 1)
7
+ throw new Error("pattern must be non-empty");
8
+ if (patternBytes > maxPatternBytes) {
9
+ throw new Error(`pattern exceeds ${maxPatternBytes} byte pattern limit`);
10
+ }
11
+ if (pattern.includes("{") || pattern.includes("}")) {
12
+ throw new Error("brace expansion is not supported in glob patterns");
13
+ }
14
+ }
15
+ function matchSegment(pattern, segment) {
16
+ function go(pi, si) {
17
+ if (pi === pattern.length)
18
+ return si === segment.length;
19
+ const pc = pattern[pi];
20
+ if (pc === "*") {
21
+ if (pi === pattern.length - 1)
22
+ return true;
23
+ for (let k = si; k <= segment.length; k++) {
24
+ if (go(pi + 1, k))
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+ if (pc === "?") {
30
+ if (si >= segment.length)
31
+ return false;
32
+ return go(pi + 1, si + 1);
33
+ }
34
+ if (si >= segment.length || pc !== segment[si])
35
+ return false;
36
+ return go(pi + 1, si + 1);
37
+ }
38
+ return go(0, 0);
39
+ }
40
+ function splitPattern(pattern) {
41
+ const normalized = pattern.replace(/\\/g, "/");
42
+ if (normalized === "")
43
+ return [];
44
+ const parts = normalized.split("/");
45
+ if (parts.length > 0 && parts[0] === "")
46
+ parts.shift();
47
+ if (parts.length > 0 && parts[parts.length - 1] === "")
48
+ parts.pop();
49
+ return parts;
50
+ }
51
+ function matchSegments(patternParts, pathParts, pi, pj) {
52
+ while (pi < patternParts.length) {
53
+ const part = patternParts[pi];
54
+ if (part === "**") {
55
+ pi++;
56
+ if (pi === patternParts.length)
57
+ return true;
58
+ for (let k = pj; k <= pathParts.length; k++) {
59
+ if (matchSegments(patternParts, pathParts, pi, k))
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ if (pj >= pathParts.length)
65
+ return false;
66
+ if (!matchSegment(part, pathParts[pj]))
67
+ return false;
68
+ pi++;
69
+ pj++;
70
+ }
71
+ return pj === pathParts.length;
72
+ }
73
+ /** Match a workspace-relative path against a glob pattern using `/` separators. */
74
+ export function matchGlobPattern(pattern, path) {
75
+ const patternParts = splitPattern(pattern);
76
+ const pathParts = splitPattern(path);
77
+ if (patternParts.length === 0)
78
+ return pathParts.length === 0;
79
+ return matchSegments(patternParts, pathParts, 0, 0);
80
+ }
81
+ //# sourceMappingURL=glob-match.js.map
package/dist/glob.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `glob` tool: bounded native pattern file finder.
3
+ */
4
+ import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
5
+ import { type RepositoryLimitOptions, type RepositoryOperations } from "./repository.js";
6
+ export interface GlobToolOptions {
7
+ executionPolicy?: ExecutionPolicy;
8
+ operations?: RepositoryOperations;
9
+ repository?: RepositoryLimitOptions;
10
+ maxDepth?: number;
11
+ maxResults?: number;
12
+ exclude?: readonly string[];
13
+ }
14
+ export declare function createGlobTool(cwd: string, options?: GlobToolOptions): ToolDefinition;
package/dist/glob.js ADDED
@@ -0,0 +1,145 @@
1
+ import { enforceExecutionPolicy } from "./execution-policy.js";
2
+ import { HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_RESULTS, validateCodingLimit, validateCodingLimitAllowZero } from "./limits.js";
3
+ import { createLocalRepositoryOperations, RepositoryError, resolveRepositoryLimits, } from "./repository.js";
4
+ function errorResult(toolCallId, message) {
5
+ return {
6
+ toolCallId,
7
+ name: "glob",
8
+ content: [{ type: "text", text: message }],
9
+ error: { message },
10
+ };
11
+ }
12
+ function formatGlobText(result) {
13
+ if (result.paths.length === 0) {
14
+ return result.truncated ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]` : "(no matches)";
15
+ }
16
+ const lines = [...result.paths];
17
+ if (result.truncated) {
18
+ const next = result.nextOffset !== undefined ? ` Use offset=${result.nextOffset} to continue.` : "";
19
+ lines.push(`[truncated by ${result.truncatedBy ?? "limit"}.${next}]`);
20
+ }
21
+ return lines.join("\n");
22
+ }
23
+ export function createGlobTool(cwd, options) {
24
+ const limits = resolveRepositoryLimits({
25
+ ...options?.repository,
26
+ maxDepth: options?.maxDepth ?? options?.repository?.maxDepth,
27
+ maxResults: options?.maxResults ?? options?.repository?.maxResults,
28
+ exclude: options?.exclude ?? options?.repository?.exclude,
29
+ });
30
+ const ops = options?.operations ?? createLocalRepositoryOperations(limits);
31
+ return {
32
+ name: "glob",
33
+ description: `Find workspace files by glob pattern without shell find. Supports * (segment), ? (one char), and ** (directories). Brace expansion is rejected. Skips hidden names and excluded basenames (default: ${limits.exclude.join(", ")}) unless overridden. Does not follow symlinks. Results paginate with offset/maxResults (default ${limits.maxResults}). Depth default ${limits.maxDepth}. Prefer repo_list to enumerate directories and repo_search to find text inside files.`,
34
+ parameters: {
35
+ type: "object",
36
+ properties: {
37
+ pattern: {
38
+ type: "string",
39
+ description: "Glob pattern matched against workspace-relative file paths (required)",
40
+ },
41
+ path: {
42
+ type: "string",
43
+ description: "Workspace-relative directory or file to search under (default: workspace root)",
44
+ },
45
+ includeHidden: {
46
+ type: "boolean",
47
+ description: "Include dotfile/dotdir names (default false). Excluded basenames still apply.",
48
+ },
49
+ maxDepth: {
50
+ type: "number",
51
+ description: `Maximum directory depth to descend (default ${limits.maxDepth}, hard ${HARD_MAX_REPO_DEPTH})`,
52
+ },
53
+ maxResults: {
54
+ type: "number",
55
+ description: `Maximum paths returned in this page (default ${limits.maxResults}, hard ${HARD_MAX_REPO_RESULTS})`,
56
+ },
57
+ offset: {
58
+ type: "number",
59
+ description: "Number of matching paths to skip before retaining results (default 0)",
60
+ },
61
+ },
62
+ required: ["pattern"],
63
+ additionalProperties: false,
64
+ },
65
+ async execute(args, context) {
66
+ const toolCallId = context.toolCallId;
67
+ if (context.signal?.aborted)
68
+ return errorResult(toolCallId, "Operation aborted");
69
+ const pattern = typeof args.pattern === "string" ? args.pattern : "";
70
+ if (!pattern)
71
+ return errorResult(toolCallId, "pattern must be a non-empty string");
72
+ const path = typeof args.path === "string" ? args.path : undefined;
73
+ const includeHidden = args.includeHidden === true;
74
+ let maxDepth;
75
+ let maxResults;
76
+ let offset = 0;
77
+ try {
78
+ if (args.maxDepth !== undefined) {
79
+ maxDepth = validateCodingLimit("maxDepth", args.maxDepth, HARD_MAX_REPO_DEPTH);
80
+ }
81
+ if (args.maxResults !== undefined) {
82
+ maxResults = validateCodingLimit("maxResults", args.maxResults, HARD_MAX_REPO_RESULTS);
83
+ }
84
+ if (args.offset !== undefined) {
85
+ offset = validateCodingLimitAllowZero("offset", args.offset, limits.maxEntries);
86
+ }
87
+ }
88
+ catch (error) {
89
+ return errorResult(toolCallId, error instanceof Error ? error.message : String(error));
90
+ }
91
+ const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
92
+ kind: "glob",
93
+ operation: "glob",
94
+ paths: [path ? path : cwd],
95
+ risk: "low",
96
+ metadata: {
97
+ pattern,
98
+ includeHidden,
99
+ maxDepth,
100
+ maxResults,
101
+ offset,
102
+ sessionId: context.sessionId,
103
+ runId: context.runId,
104
+ signal: context.signal,
105
+ },
106
+ }, toolCallId, "glob");
107
+ if (!policyCheck.allowed)
108
+ return policyCheck.result;
109
+ try {
110
+ const result = await ops.glob({
111
+ root: cwd,
112
+ pattern,
113
+ path,
114
+ includeHidden,
115
+ exclude: limits.exclude,
116
+ maxDepth: maxDepth ?? limits.maxDepth,
117
+ maxResults: maxResults ?? limits.maxResults,
118
+ offset,
119
+ signal: context.signal,
120
+ deadlineMs: limits.maxTimeMs,
121
+ });
122
+ return {
123
+ toolCallId,
124
+ name: "glob",
125
+ content: [{ type: "text", text: formatGlobText(result) }],
126
+ metadata: {
127
+ truncated: result.truncated,
128
+ truncatedBy: result.truncatedBy,
129
+ offset: result.offset,
130
+ nextOffset: result.nextOffset,
131
+ returned: result.paths.length,
132
+ scannedEntries: result.scannedEntries,
133
+ scannedFiles: result.scannedFiles,
134
+ paths: result.paths,
135
+ },
136
+ };
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof RepositoryError ? error.message : error instanceof Error ? error.message : String(error);
140
+ return errorResult(toolCallId, message);
141
+ }
142
+ },
143
+ };
144
+ }
145
+ //# sourceMappingURL=glob.js.map