@arnilo/prism-coding-agent 0.0.96 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
package/dist/search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CODING_OBSERVATION_EFFECT } from "./effects.js";
|
|
1
2
|
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
2
|
-
import { HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_MATCHES, validateCodingLimit, validateCodingLimitAllowZero
|
|
3
|
-
import { createLocalRepositoryOperations,
|
|
3
|
+
import { HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_MATCHES, validateCodingLimit, validateCodingLimitAllowZero } from "./limits.js";
|
|
4
|
+
import { createLocalRepositoryOperations, RepositoryError, resolveRepositoryLimits, } from "./repository.js";
|
|
4
5
|
import { truncateLine } from "./truncate.js";
|
|
5
6
|
function errorResult(toolCallId, message) {
|
|
6
7
|
return {
|
|
@@ -26,15 +27,71 @@ function formatMatch(match) {
|
|
|
26
27
|
}
|
|
27
28
|
function formatSearchText(result) {
|
|
28
29
|
if (result.matches.length === 0) {
|
|
29
|
-
return result.truncated
|
|
30
|
-
? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]`
|
|
31
|
-
: "(no matches)";
|
|
30
|
+
return result.truncated ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]` : "(no matches)";
|
|
32
31
|
}
|
|
33
32
|
const body = result.matches.map(formatMatch).join("\n");
|
|
34
33
|
if (!result.truncated)
|
|
35
34
|
return body;
|
|
36
35
|
return `${body}\n[truncated by ${result.truncatedBy ?? "limit"}]`;
|
|
37
36
|
}
|
|
37
|
+
function uniqueMatchPaths(matches) {
|
|
38
|
+
return [...new Set(matches.map((m) => m.path))].sort();
|
|
39
|
+
}
|
|
40
|
+
function formatFilesWithMatches(result) {
|
|
41
|
+
if (result.matches.length === 0) {
|
|
42
|
+
return result.truncated ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]` : "(no matches)";
|
|
43
|
+
}
|
|
44
|
+
const body = uniqueMatchPaths(result.matches).join("\n");
|
|
45
|
+
if (!result.truncated)
|
|
46
|
+
return body;
|
|
47
|
+
return `${body}\n[truncated by ${result.truncatedBy ?? "limit"}]`;
|
|
48
|
+
}
|
|
49
|
+
function formatCount(result) {
|
|
50
|
+
const matchCount = result.matches.length;
|
|
51
|
+
const fileCount = uniqueMatchPaths(result.matches).length;
|
|
52
|
+
if (matchCount === 0) {
|
|
53
|
+
return result.truncated ? `[truncated by ${result.truncatedBy ?? "limit"} before any matches]` : "0 matches in 0 files";
|
|
54
|
+
}
|
|
55
|
+
const noun = fileCount === 1 ? "file" : "files";
|
|
56
|
+
const body = `${matchCount} matches in ${fileCount} ${noun}`;
|
|
57
|
+
if (!result.truncated)
|
|
58
|
+
return body;
|
|
59
|
+
return `${body}\n[truncated by ${result.truncatedBy ?? "limit"}]`;
|
|
60
|
+
}
|
|
61
|
+
function formatSearchResult(result, outputMode) {
|
|
62
|
+
switch (outputMode) {
|
|
63
|
+
case "files_with_matches":
|
|
64
|
+
return formatFilesWithMatches(result);
|
|
65
|
+
case "count":
|
|
66
|
+
return formatCount(result);
|
|
67
|
+
default:
|
|
68
|
+
return formatSearchText(result);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseOutputMode(value) {
|
|
72
|
+
if (value === undefined || value === "content")
|
|
73
|
+
return "content";
|
|
74
|
+
if (value === "files_with_matches" || value === "count")
|
|
75
|
+
return value;
|
|
76
|
+
throw new Error(`unsupported outputMode: ${String(value)}`);
|
|
77
|
+
}
|
|
78
|
+
function buildSearchMetadata(result, outputMode) {
|
|
79
|
+
const base = {
|
|
80
|
+
outputMode,
|
|
81
|
+
truncated: result.truncated,
|
|
82
|
+
truncatedBy: result.truncatedBy,
|
|
83
|
+
matchCount: result.matches.length,
|
|
84
|
+
scannedBytes: result.scannedBytes,
|
|
85
|
+
scannedFiles: result.scannedFiles,
|
|
86
|
+
scannedEntries: result.scannedEntries,
|
|
87
|
+
filesSkippedBinary: result.filesSkippedBinary,
|
|
88
|
+
filesSkippedOversize: result.filesSkippedOversize,
|
|
89
|
+
};
|
|
90
|
+
if (outputMode === "content") {
|
|
91
|
+
return { ...base, matches: result.matches };
|
|
92
|
+
}
|
|
93
|
+
return { ...base, fileCount: uniqueMatchPaths(result.matches).length };
|
|
94
|
+
}
|
|
38
95
|
export function createRepoSearchTool(cwd, options) {
|
|
39
96
|
const limits = resolveRepositoryLimits({
|
|
40
97
|
...options?.repository,
|
|
@@ -45,19 +102,20 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
45
102
|
const ops = options?.operations ?? createLocalRepositoryOperations(limits);
|
|
46
103
|
return {
|
|
47
104
|
name: "repo_search",
|
|
48
|
-
|
|
105
|
+
effect: CODING_OBSERVATION_EFFECT,
|
|
106
|
+
description: `Search text files under the workspace using literal substring match. Use outputMode "files_with_matches" for paths only or "count" for totals without line bodies. Skips binary files, excluded basenames (default: ${limits.exclude.join(", ")}), and hidden names unless includeHidden is true. Does not follow symlinks. Caps matches/scanned bytes/time.`,
|
|
49
107
|
parameters: {
|
|
50
108
|
type: "object",
|
|
51
109
|
properties: {
|
|
52
|
-
query: { type: "string", description: "Literal text
|
|
110
|
+
query: { type: "string", description: "Literal text to search for (required)" },
|
|
53
111
|
path: {
|
|
54
112
|
type: "string",
|
|
55
113
|
description: "Workspace-relative directory or file to search (default: workspace root)",
|
|
56
114
|
},
|
|
57
115
|
mode: {
|
|
58
116
|
type: "string",
|
|
59
|
-
description:
|
|
60
|
-
enum: ["literal"
|
|
117
|
+
description: "Search mode: literal substring match only",
|
|
118
|
+
enum: ["literal"],
|
|
61
119
|
},
|
|
62
120
|
caseSensitive: {
|
|
63
121
|
type: "boolean",
|
|
@@ -75,6 +133,11 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
75
133
|
type: "number",
|
|
76
134
|
description: `Maximum matches to retain (default ${limits.maxMatches}, hard ${HARD_MAX_SEARCH_MATCHES})`,
|
|
77
135
|
},
|
|
136
|
+
outputMode: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: 'Result shape: "content" (default, line bodies), "files_with_matches" (unique paths), or "count" (match/file totals)',
|
|
139
|
+
enum: ["content", "files_with_matches", "count"],
|
|
140
|
+
},
|
|
78
141
|
},
|
|
79
142
|
required: ["query"],
|
|
80
143
|
additionalProperties: false,
|
|
@@ -87,7 +150,20 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
87
150
|
if (query.length === 0)
|
|
88
151
|
return errorResult(toolCallId, "query is required and must be a non-empty string.");
|
|
89
152
|
const path = typeof args.path === "string" ? args.path : undefined;
|
|
90
|
-
|
|
153
|
+
if (args.mode === "regex") {
|
|
154
|
+
return errorResult(toolCallId, 'repo_search no longer supports mode "regex"; use literal substring search.');
|
|
155
|
+
}
|
|
156
|
+
if (args.mode !== undefined && args.mode !== "literal") {
|
|
157
|
+
return errorResult(toolCallId, `unsupported search mode: ${String(args.mode)}`);
|
|
158
|
+
}
|
|
159
|
+
const mode = "literal";
|
|
160
|
+
let outputMode;
|
|
161
|
+
try {
|
|
162
|
+
outputMode = parseOutputMode(args.outputMode);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return errorResult(toolCallId, error instanceof Error ? error.message : String(error));
|
|
166
|
+
}
|
|
91
167
|
const caseSensitive = args.caseSensitive === true;
|
|
92
168
|
const includeHidden = args.includeHidden === true;
|
|
93
169
|
let contextLines;
|
|
@@ -110,6 +186,7 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
110
186
|
risk: "low",
|
|
111
187
|
metadata: {
|
|
112
188
|
mode,
|
|
189
|
+
outputMode,
|
|
113
190
|
caseSensitive,
|
|
114
191
|
includeHidden,
|
|
115
192
|
context: contextLines,
|
|
@@ -127,6 +204,7 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
127
204
|
query,
|
|
128
205
|
path,
|
|
129
206
|
mode,
|
|
207
|
+
outputMode,
|
|
130
208
|
caseSensitive,
|
|
131
209
|
includeHidden,
|
|
132
210
|
exclude: limits.exclude,
|
|
@@ -138,26 +216,12 @@ export function createRepoSearchTool(cwd, options) {
|
|
|
138
216
|
return {
|
|
139
217
|
toolCallId,
|
|
140
218
|
name: "repo_search",
|
|
141
|
-
content: [{ type: "text", text:
|
|
142
|
-
metadata:
|
|
143
|
-
truncated: result.truncated,
|
|
144
|
-
truncatedBy: result.truncatedBy,
|
|
145
|
-
matchCount: result.matches.length,
|
|
146
|
-
scannedBytes: result.scannedBytes,
|
|
147
|
-
scannedFiles: result.scannedFiles,
|
|
148
|
-
scannedEntries: result.scannedEntries,
|
|
149
|
-
filesSkippedBinary: result.filesSkippedBinary,
|
|
150
|
-
filesSkippedOversize: result.filesSkippedOversize,
|
|
151
|
-
matches: result.matches,
|
|
152
|
-
},
|
|
219
|
+
content: [{ type: "text", text: formatSearchResult(result, outputMode) }],
|
|
220
|
+
metadata: buildSearchMetadata(result, outputMode),
|
|
153
221
|
};
|
|
154
222
|
}
|
|
155
223
|
catch (error) {
|
|
156
|
-
const message = error instanceof RepositoryError
|
|
157
|
-
? error.message
|
|
158
|
-
: error instanceof Error
|
|
159
|
-
? error.message
|
|
160
|
-
: String(error);
|
|
224
|
+
const message = error instanceof RepositoryError ? error.message : error instanceof Error ? error.message : String(error);
|
|
161
225
|
return errorResult(toolCallId, message);
|
|
162
226
|
}
|
|
163
227
|
},
|
package/dist/shell.d.ts
CHANGED
|
@@ -55,6 +55,9 @@ export interface ShellToolOptions {
|
|
|
55
55
|
shellPath?: string;
|
|
56
56
|
/** Hook to adjust command, cwd, or env before execution. */
|
|
57
57
|
spawnHook?: BashSpawnHook;
|
|
58
|
+
/** Restrict the process environment cloned for the spawn hook / child process to these
|
|
59
|
+
* names (e.g. scrub secrets). Unset keeps the full `process.env` clone. */
|
|
60
|
+
envAllowlist?: readonly string[];
|
|
58
61
|
/** Max lines kept in the tail snapshot (default 2000). */
|
|
59
62
|
maxLines?: number;
|
|
60
63
|
/** Max bytes kept in the tail snapshot (default 50KB). */
|
package/dist/shell.js
CHANGED
|
@@ -23,12 +23,25 @@
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
24
|
import { constants, existsSync } from "node:fs";
|
|
25
25
|
import { access as fsAccess } from "node:fs/promises";
|
|
26
|
+
import { CODING_UNSUPPORTED_EFFECT } from "./effects.js";
|
|
26
27
|
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
27
|
-
import { OutputAccumulator } from "./output-accumulator.js";
|
|
28
28
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, DEFAULT_SHELL_TIMEOUT_SECONDS, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TOTAL_OUTPUT_BYTES, HARD_SHELL_TIMEOUT_SECONDS, validateCodingLimit, } from "./limits.js";
|
|
29
|
+
import { OutputAccumulator } from "./output-accumulator.js";
|
|
29
30
|
import { formatSize } from "./truncate.js";
|
|
30
31
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
31
32
|
// --- spawn internals (re-ported from pi utils/shell.js + utils/child-process.js) ---
|
|
33
|
+
/** Clone the process environment, optionally restricted to an allowlist of names. */
|
|
34
|
+
function pickSpawnEnv(allowlist) {
|
|
35
|
+
if (!allowlist)
|
|
36
|
+
return { ...process.env };
|
|
37
|
+
const env = {};
|
|
38
|
+
for (const name of allowlist) {
|
|
39
|
+
const value = process.env[name];
|
|
40
|
+
if (value !== undefined)
|
|
41
|
+
env[name] = value;
|
|
42
|
+
}
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
32
45
|
/** Resolve the shell binary + args. shellPath → SHELL env → /bin/bash → sh. */
|
|
33
46
|
export function getShellConfig(customShellPath) {
|
|
34
47
|
if (customShellPath) {
|
|
@@ -252,8 +265,9 @@ export function createShellTool(cwd, options) {
|
|
|
252
265
|
const tempFilePrefix = options?.tempFilePrefix ?? "prism-shell";
|
|
253
266
|
return {
|
|
254
267
|
name: "shell",
|
|
268
|
+
effect: CODING_UNSUPPORTED_EFFECT,
|
|
255
269
|
exclusive: true,
|
|
256
|
-
description: `Execute a shell command in the current working directory. Returns combined stdout and stderr. Output is truncated to the last ${maxLines} lines or ${maxBytes / 1024}KB and capped at ${formatSize(maxTotalOutputBytes)} total. Truncated successful output is saved to a temp file. Timeout defaults to ${defaultTimeout} seconds.`,
|
|
270
|
+
description: `Execute a shell command in the current working directory. Returns combined stdout and stderr. Prefer repo_list, repo_search, glob, read, write, edit, delete, or move for those jobs — use shell only when no dedicated tool fits. Output is truncated to the last ${maxLines} lines or ${maxBytes / 1024}KB and capped at ${formatSize(maxTotalOutputBytes)} total. Truncated successful output is saved to a temp file. Timeout defaults to ${defaultTimeout} seconds.`,
|
|
257
271
|
parameters: {
|
|
258
272
|
type: "object",
|
|
259
273
|
properties: {
|
|
@@ -284,9 +298,10 @@ export function createShellTool(cwd, options) {
|
|
|
284
298
|
return { toolCallId, name: "shell", content: [{ type: "text", text: message }], error: { message } };
|
|
285
299
|
}
|
|
286
300
|
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
|
301
|
+
const baseEnv = pickSpawnEnv(options?.envAllowlist);
|
|
287
302
|
let spawnContext = spawnHook
|
|
288
|
-
? spawnHook({ command: resolvedCommand, cwd, env:
|
|
289
|
-
: { command: resolvedCommand, cwd, env:
|
|
303
|
+
? spawnHook({ command: resolvedCommand, cwd, env: baseEnv })
|
|
304
|
+
: { command: resolvedCommand, cwd, env: baseEnv };
|
|
290
305
|
const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
291
306
|
kind: "shell",
|
|
292
307
|
operation: "execute",
|
|
@@ -309,9 +324,7 @@ export function createShellTool(cwd, options) {
|
|
|
309
324
|
onLimit: () => outputAbort.abort("output-limit"),
|
|
310
325
|
onStorageError: () => outputAbort.abort("output-storage-error"),
|
|
311
326
|
});
|
|
312
|
-
const signal = context.signal
|
|
313
|
-
? AbortSignal.any([context.signal, outputAbort.signal])
|
|
314
|
-
: outputAbort.signal;
|
|
327
|
+
const signal = context.signal ? AbortSignal.any([context.signal, outputAbort.signal]) : outputAbort.signal;
|
|
315
328
|
let acceptingOutput = true;
|
|
316
329
|
const handleData = (data) => {
|
|
317
330
|
if (acceptingOutput)
|
|
@@ -399,7 +412,9 @@ export function createShellTool(cwd, options) {
|
|
|
399
412
|
try {
|
|
400
413
|
await output.cleanupTempFile();
|
|
401
414
|
}
|
|
402
|
-
catch {
|
|
415
|
+
catch {
|
|
416
|
+
/* retain primary error */
|
|
417
|
+
}
|
|
403
418
|
const message = err instanceof Error ? err.message : String(err);
|
|
404
419
|
return { toolCallId, name: "shell", content: [{ type: "text", text: message }], error: { message } };
|
|
405
420
|
}
|
package/dist/truncate.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Never returns partial lines (except the documented tail single-line edge case).
|
|
11
11
|
*/
|
|
12
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, HARD_MAX_BYTES, HARD_MAX_LINES, validateCodingLimit
|
|
12
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, HARD_MAX_BYTES, HARD_MAX_LINES, validateCodingLimit } from "./limits.js";
|
|
13
13
|
export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "./limits.js";
|
|
14
14
|
/**
|
|
15
15
|
* Default char cap for {@link truncateLine}. pi names this GREP_MAX_LINE_LENGTH
|
package/dist/write.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
|
|
2
|
+
import type { CodingLifecycleEvent } from "./lifecycle.js";
|
|
3
|
+
import { type ReadBeforeWriteOptions } from "./read-path-set.js";
|
|
2
4
|
/**
|
|
3
5
|
* Pluggable operations for the write tool. Override to delegate file writing to remote systems
|
|
4
6
|
* (e.g. SSH) while keeping the tool's directory-creation + per-path serialization behavior.
|
|
@@ -14,12 +16,14 @@ export interface WriteOperations {
|
|
|
14
16
|
signal?: AbortSignal;
|
|
15
17
|
}) => Promise<void>;
|
|
16
18
|
}
|
|
17
|
-
export interface WriteToolOptions {
|
|
19
|
+
export interface WriteToolOptions extends ReadBeforeWriteOptions {
|
|
18
20
|
/** Structured pre-execution policy checked before filesystem writes. */
|
|
19
21
|
executionPolicy?: ExecutionPolicy;
|
|
20
22
|
/** Custom operations backend (default: local filesystem). */
|
|
21
23
|
operations?: WriteOperations;
|
|
22
24
|
/** Maximum UTF-8 input bytes accepted before any filesystem mutation (default 8 MiB). */
|
|
23
25
|
maxInputBytes?: number;
|
|
26
|
+
/** Optional consumer-gated lifecycle listener (file_changed / permission_denied). */
|
|
27
|
+
onEvent?: (event: CodingLifecycleEvent) => void;
|
|
24
28
|
}
|
|
25
29
|
export declare function createWriteTool(cwd: string, options?: WriteToolOptions): ToolDefinition;
|
package/dist/write.js
CHANGED
|
@@ -15,14 +15,17 @@
|
|
|
15
15
|
* (pi would throw "Operation aborted" even after a successful write — misleading, so dropped).
|
|
16
16
|
*/
|
|
17
17
|
import { Buffer } from "node:buffer";
|
|
18
|
-
import { mkdir as fsMkdir
|
|
18
|
+
import { mkdir as fsMkdir } from "node:fs/promises";
|
|
19
19
|
import { dirname } from "node:path";
|
|
20
|
+
import { atomicWriteUtf8File } from "./atomic-write.js";
|
|
21
|
+
import { CODING_LOCAL_EFFECT } from "./effects.js";
|
|
20
22
|
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
21
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
22
23
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
23
|
-
import { DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES, validateCodingLimit
|
|
24
|
+
import { DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES, validateCodingLimit } from "./limits.js";
|
|
25
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
26
|
+
import { refuseReadBeforeWrite } from "./read-path-set.js";
|
|
24
27
|
const defaultWriteOperations = {
|
|
25
|
-
writeFile: (path, content, options) =>
|
|
28
|
+
writeFile: (path, content, options) => atomicWriteUtf8File(path, content, { signal: options?.signal }),
|
|
26
29
|
mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => { }),
|
|
27
30
|
};
|
|
28
31
|
function errorResult(toolCallId, message) {
|
|
@@ -43,12 +46,17 @@ export function createWriteTool(cwd, options) {
|
|
|
43
46
|
const maxInputBytes = validateCodingLimit("maxInputBytes", options?.maxInputBytes ?? DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES);
|
|
44
47
|
return {
|
|
45
48
|
name: "write",
|
|
46
|
-
|
|
49
|
+
effect: CODING_LOCAL_EFFECT,
|
|
50
|
+
description: "Create or overwrite a file (full replace). Creates parent directories. Prefer edit for targeted changes. When the host enabled requireReadBeforeWrite, read the path first or pass force=true to override.",
|
|
47
51
|
parameters: {
|
|
48
52
|
type: "object",
|
|
49
53
|
properties: {
|
|
50
54
|
path: { type: "string", description: "Path to the file to write (relative or absolute)" },
|
|
51
55
|
content: { type: "string", description: "Content to write to the file" },
|
|
56
|
+
force: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
description: "Bypass read-before-write guard when the host enabled requireReadBeforeWrite.",
|
|
59
|
+
},
|
|
52
60
|
},
|
|
53
61
|
required: ["path", "content"],
|
|
54
62
|
additionalProperties: false,
|
|
@@ -57,6 +65,7 @@ export function createWriteTool(cwd, options) {
|
|
|
57
65
|
const toolCallId = context.toolCallId;
|
|
58
66
|
const path = typeof args.path === "string" ? args.path : "";
|
|
59
67
|
const content = typeof args.content === "string" ? args.content : undefined;
|
|
68
|
+
const force = args.force === true;
|
|
60
69
|
if (path.length === 0) {
|
|
61
70
|
return errorResult(toolCallId, "path is required and must be a non-empty string.");
|
|
62
71
|
}
|
|
@@ -75,10 +84,13 @@ export function createWriteTool(cwd, options) {
|
|
|
75
84
|
paths: [absolutePath],
|
|
76
85
|
risk: "medium",
|
|
77
86
|
metadata: { bytes, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
|
|
78
|
-
}, toolCallId, "write");
|
|
87
|
+
}, toolCallId, "write", (denied) => options?.onEvent?.({ type: "permission_denied", ...denied }));
|
|
79
88
|
if (!policyCheck.allowed)
|
|
80
89
|
return policyCheck.result;
|
|
81
90
|
const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
|
|
91
|
+
const rbwRefusal = refuseReadBeforeWrite("write", path, allowedPath, options, force);
|
|
92
|
+
if (rbwRefusal)
|
|
93
|
+
return errorResult(toolCallId, rbwRefusal);
|
|
82
94
|
const dir = dirname(allowedPath);
|
|
83
95
|
return await withFileMutationQueue(allowedPath, async () => {
|
|
84
96
|
// Check abort before each fs op — do not start a new operation once aborted. We intentionally
|
|
@@ -89,6 +101,7 @@ export function createWriteTool(cwd, options) {
|
|
|
89
101
|
if (context.signal?.aborted)
|
|
90
102
|
return errorResult(toolCallId, "Operation aborted");
|
|
91
103
|
await ops.writeFile(allowedPath, content, { maxBytes: maxInputBytes, signal: context.signal });
|
|
104
|
+
options?.onEvent?.({ type: "file_changed", path: allowedPath, op: "write", toolCallId });
|
|
92
105
|
const lines = countLines(content);
|
|
93
106
|
return {
|
|
94
107
|
toolCallId,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism-coding-agent",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Optional coding-agent tools (shell, read, write, edit, repo_list, repo_search, opt-in Git/check, and durable plan/checkpoint helpers) package for Prism.",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Optional coding-agent tools (shell, read, write, edit, repo_list, repo_search, glob, delete, move, opt-in Git/check/ask-user-decision, and durable plan/checkpoint helpers) package for Prism.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -28,11 +28,13 @@
|
|
|
28
28
|
"diff": "^9.0.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@arnilo/prism": "0.0
|
|
31
|
+
"@arnilo/prism": "0.1.0",
|
|
32
|
+
"@arnilo/prism-workflows": "0.1.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@arnilo/prism": "file:../..",
|
|
35
|
-
"@arnilo/prism-evals": "file:../evals"
|
|
36
|
+
"@arnilo/prism-evals": "file:../evals",
|
|
37
|
+
"@arnilo/prism-workflows": "file:../workflows"
|
|
36
38
|
},
|
|
37
39
|
"engines": {
|
|
38
40
|
"node": ">=20"
|