@arnilo/prism-coding-agent 0.0.19 → 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 +24 -0
- package/README.md +38 -18
- package/dist/delete.d.ts +26 -0
- package/dist/delete.js +115 -0
- package/dist/edit.d.ts +2 -1
- package/dist/edit.js +10 -1
- 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 +145 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +16 -2
- package/dist/list.js +1 -1
- package/dist/move.d.ts +21 -0
- package/dist/move.js +147 -0
- package/dist/mutation-path.d.ts +7 -0
- package/dist/mutation-path.js +51 -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 +3 -1
- package/dist/repository.d.ts +24 -0
- package/dist/repository.js +126 -0
- package/dist/search.js +75 -13
- package/dist/shell.js +1 -1
- package/dist/write.d.ts +2 -1
- package/dist/write.js +10 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
21
|
+
## [0.0.20] - 2026-07-31
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Released with exact 0.0.20 graph.
|
|
25
|
+
|
|
26
|
+
|
|
3
27
|
## [0.0.19] - 2026-07-30
|
|
4
28
|
|
|
5
29
|
### 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 {
|
|
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 }` |
|
|
74
|
-
| `edit` | `{ path, edits: [{oldText,newText}] }` |
|
|
75
|
-
| `repo_list` | `{ path?, includeHidden?, maxDepth?, maxResults?, offset? }` | Deterministic relative entries;
|
|
76
|
-
| `repo_search` | `{ query, path?, mode?, caseSensitive?, includeHidden?, context?, maxMatches? }` | Literal
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
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
|
|
package/dist/delete.d.ts
ADDED
|
@@ -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
|
|
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
|