@arnilo/prism-coding-agent 0.0.7 → 0.0.10

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/dist/git.js ADDED
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Structured Git operations over a typed runner.
3
+ *
4
+ * All invocations use file+argument arrays with `--` pathspec separation,
5
+ * `git check-ref-format` for refs, and safe config that disables hooks,
6
+ * external diff/textconv, pagers, and credential prompts by default.
7
+ */
8
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { DEFAULT_MAX_GIT_CHANGED_FILES, DEFAULT_MAX_GIT_DIFF_LINES, DEFAULT_MAX_GIT_MESSAGE_BYTES, DEFAULT_MAX_GIT_OUTPUT_BYTES, DEFAULT_MAX_GIT_PATCH_BYTES, DEFAULT_MAX_GIT_PATHS, DEFAULT_MAX_GIT_REF_BYTES, DEFAULT_MAX_GIT_WORKTREES, DEFAULT_MAX_PR_COMMITS, DEFAULT_MAX_PR_HANDOFF_BYTES, HARD_MAX_GIT_CHANGED_FILES, HARD_MAX_GIT_DIFF_LINES, HARD_MAX_GIT_MESSAGE_BYTES, HARD_MAX_GIT_OUTPUT_BYTES, HARD_MAX_GIT_PATCH_BYTES, HARD_MAX_GIT_PATHS, HARD_MAX_GIT_REF_BYTES, HARD_MAX_GIT_WORKTREES, HARD_MAX_PR_COMMITS, HARD_MAX_PR_HANDOFF_BYTES, validateCodingLimit, } from "./limits.js";
12
+ import { createBoundGitRunner, GitError, gitRequireOk, gitText, } from "./git-exec.js";
13
+ import { parsePorcelainV2 } from "./git-status.js";
14
+ import { resolveToCwd } from "./path-utils.js";
15
+ export function resolveGitLimits(options) {
16
+ return {
17
+ maxPaths: validateCodingLimit("maxPaths", options?.maxPaths ?? DEFAULT_MAX_GIT_PATHS, HARD_MAX_GIT_PATHS),
18
+ maxRefBytes: validateCodingLimit("maxRefBytes", options?.maxRefBytes ?? DEFAULT_MAX_GIT_REF_BYTES, HARD_MAX_GIT_REF_BYTES),
19
+ maxMessageBytes: validateCodingLimit("maxMessageBytes", options?.maxMessageBytes ?? DEFAULT_MAX_GIT_MESSAGE_BYTES, HARD_MAX_GIT_MESSAGE_BYTES),
20
+ maxOutputBytes: validateCodingLimit("maxOutputBytes", options?.maxOutputBytes ?? DEFAULT_MAX_GIT_OUTPUT_BYTES, HARD_MAX_GIT_OUTPUT_BYTES),
21
+ maxDiffLines: validateCodingLimit("maxDiffLines", options?.maxDiffLines ?? DEFAULT_MAX_GIT_DIFF_LINES, HARD_MAX_GIT_DIFF_LINES),
22
+ maxChangedFiles: validateCodingLimit("maxChangedFiles", options?.maxChangedFiles ?? DEFAULT_MAX_GIT_CHANGED_FILES, HARD_MAX_GIT_CHANGED_FILES),
23
+ maxPatchBytes: validateCodingLimit("maxPatchBytes", options?.maxPatchBytes ?? DEFAULT_MAX_GIT_PATCH_BYTES, HARD_MAX_GIT_PATCH_BYTES),
24
+ maxWorktrees: validateCodingLimit("maxWorktrees", options?.maxWorktrees ?? DEFAULT_MAX_GIT_WORKTREES, HARD_MAX_GIT_WORKTREES),
25
+ maxPrCommits: validateCodingLimit("maxPrCommits", options?.maxPrCommits ?? DEFAULT_MAX_PR_COMMITS, HARD_MAX_PR_COMMITS),
26
+ maxPrHandoffBytes: validateCodingLimit("maxPrHandoffBytes", options?.maxPrHandoffBytes ?? DEFAULT_MAX_PR_HANDOFF_BYTES, HARD_MAX_PR_HANDOFF_BYTES),
27
+ };
28
+ }
29
+ function byteLength(text) {
30
+ return Buffer.byteLength(text, "utf8");
31
+ }
32
+ async function validateBranchName(runner, cwd, name, limits, signal) {
33
+ if (typeof name !== "string" || name.length === 0)
34
+ throw new GitError("branch name is required");
35
+ if (byteLength(name) > limits.maxRefBytes)
36
+ throw new GitError(`branch name exceeds ${limits.maxRefBytes} byte limit`);
37
+ if (name.includes("\0") || name.includes("\n") || name.includes("\r") || name.startsWith("-")) {
38
+ throw new GitError("branch name must not start with '-' or contain NUL/newlines");
39
+ }
40
+ const result = await runner.exec({
41
+ args: ["check-ref-format", "--branch", name],
42
+ cwd,
43
+ signal,
44
+ maxOutputBytes: 64 * 1024,
45
+ });
46
+ if (result.exitCode !== 0) {
47
+ throw new GitError(`invalid branch name: ${name}`);
48
+ }
49
+ return name;
50
+ }
51
+ function validatePaths(paths, limits) {
52
+ if (!Array.isArray(paths) || paths.length === 0) {
53
+ throw new GitError("paths must be a non-empty array");
54
+ }
55
+ if (paths.length > limits.maxPaths) {
56
+ throw new GitError(`paths exceed ${limits.maxPaths} entry limit`);
57
+ }
58
+ const out = [];
59
+ for (const path of paths) {
60
+ if (typeof path !== "string" || path.length === 0) {
61
+ throw new GitError("each path must be a non-empty string");
62
+ }
63
+ if (path.includes("\0"))
64
+ throw new GitError("path must not contain NUL");
65
+ // Keep leading-dash paths as data; always pass after `--`.
66
+ out.push(path);
67
+ }
68
+ return out;
69
+ }
70
+ function truncateLines(text, maxLines) {
71
+ if (text.length === 0)
72
+ return { text: "", truncated: false, lineCount: 0 };
73
+ const endsWithNewline = text.endsWith("\n");
74
+ const lines = text.split("\n");
75
+ if (endsWithNewline)
76
+ lines.pop();
77
+ const lineCount = lines.length;
78
+ if (lineCount <= maxLines)
79
+ return { text, truncated: false, lineCount };
80
+ const kept = lines.slice(0, maxLines).join("\n") + "\n";
81
+ return { text: kept, truncated: true, lineCount };
82
+ }
83
+ async function withTempFile(prefix, contents, fn) {
84
+ const dir = await mkdtemp(join(tmpdir(), prefix));
85
+ const filePath = join(dir, "payload");
86
+ try {
87
+ await writeFile(filePath, contents, { mode: 0o600 });
88
+ return await fn(filePath);
89
+ }
90
+ finally {
91
+ await rm(dir, { recursive: true, force: true });
92
+ }
93
+ }
94
+ export async function createGitOperations(options) {
95
+ const cwd = resolveToCwd(options.cwd, process.cwd());
96
+ const limits = resolveGitLimits(options);
97
+ const runner = await createBoundGitRunner(options);
98
+ const artifacts = options.artifactWriter;
99
+ async function status(request) {
100
+ const args = ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all"];
101
+ if (request?.includeIgnored)
102
+ args.push("--ignored=traditional");
103
+ const result = await gitRequireOk(runner, {
104
+ args,
105
+ cwd,
106
+ signal: request?.signal,
107
+ maxOutputBytes: limits.maxOutputBytes,
108
+ }, "git status");
109
+ return parsePorcelainV2(result.stdout, { maxEntries: limits.maxChangedFiles });
110
+ }
111
+ async function ensureCleanOrCheckpoint(createCheckpoint, signal, label, allowPaths) {
112
+ const current = await status({ signal });
113
+ const blocking = current.entries.filter((entry) => {
114
+ if (entry.kind === "ignored")
115
+ return false;
116
+ if (!allowPaths)
117
+ return true;
118
+ if (allowPaths.has(entry.path))
119
+ return false;
120
+ if (entry.origPath && allowPaths.has(entry.origPath))
121
+ return false;
122
+ return true;
123
+ });
124
+ if (blocking.length === 0)
125
+ return undefined;
126
+ if (!createCheckpoint) {
127
+ throw new GitError(`${label} refused: worktree is dirty. Pass createCheckpoint=true to stash a bounded checkpoint first, or use a disposable worktree.`);
128
+ }
129
+ const stashPaths = [...new Set(blocking.flatMap((entry) => (entry.origPath ? [entry.path, entry.origPath] : [entry.path])))];
130
+ if (stashPaths.length > limits.maxPaths) {
131
+ throw new GitError(`checkpoint paths exceed ${limits.maxPaths} entry limit`);
132
+ }
133
+ await gitRequireOk(runner, {
134
+ args: ["stash", "push", "-u", "-m", "prism-git-checkpoint", "--", ...stashPaths],
135
+ cwd,
136
+ signal,
137
+ maxOutputBytes: limits.maxOutputBytes,
138
+ }, "git stash checkpoint");
139
+ const top = await gitRequireOk(runner, { args: ["rev-parse", "-q", "--verify", "refs/stash"], cwd, signal, maxOutputBytes: 64 * 1024 }, "git rev-parse stash");
140
+ return gitText(top).trim() || "refs/stash";
141
+ }
142
+ async function restoreCheckpoint(checkpoint, signal) {
143
+ if (!checkpoint)
144
+ return false;
145
+ await gitRequireOk(runner, {
146
+ args: ["stash", "pop", "--index"],
147
+ cwd,
148
+ signal,
149
+ maxOutputBytes: limits.maxOutputBytes,
150
+ }, "git stash pop");
151
+ return true;
152
+ }
153
+ async function diff(request) {
154
+ const built = ["diff", "--no-ext-diff", "--no-textconv", "--no-color"];
155
+ if (request?.staged)
156
+ built.push("--cached");
157
+ built.push("--");
158
+ if (request?.paths) {
159
+ built.push(...validatePaths(request.paths, limits));
160
+ }
161
+ const result = await gitRequireOk(runner, { args: built, cwd, signal: request?.signal, maxOutputBytes: limits.maxOutputBytes }, "git diff");
162
+ const raw = gitText(result);
163
+ const trimmed = truncateLines(raw, limits.maxDiffLines);
164
+ let artifact;
165
+ if (trimmed.truncated && artifacts) {
166
+ artifact = await artifacts({
167
+ kind: "diff",
168
+ filename: "diff.patch",
169
+ bytes: Buffer.from(raw, "utf8"),
170
+ });
171
+ }
172
+ return { ...trimmed, artifact };
173
+ }
174
+ async function branch(request) {
175
+ if (request.action === "list") {
176
+ const result = await gitRequireOk(runner, {
177
+ args: ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
178
+ cwd,
179
+ signal: request.signal,
180
+ maxOutputBytes: limits.maxOutputBytes,
181
+ }, "git for-each-ref");
182
+ const refs = gitText(result)
183
+ .split("\n")
184
+ .map((line) => line.trim())
185
+ .filter(Boolean);
186
+ return { refs };
187
+ }
188
+ const name = await validateBranchName(runner, cwd, request.name ?? "", limits, request.signal);
189
+ if (request.action === "validate")
190
+ return { name };
191
+ if (request.action === "create") {
192
+ await gitRequireOk(runner, { args: ["branch", "--", name], cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes }, "git branch create");
193
+ return { name };
194
+ }
195
+ // switch
196
+ const checkpoint = await ensureCleanOrCheckpoint(request.createCheckpoint, request.signal, "git switch");
197
+ try {
198
+ await gitRequireOk(runner, { args: ["switch", "--", name], cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes }, "git switch");
199
+ return { name, checkpoint };
200
+ }
201
+ catch (error) {
202
+ if (checkpoint)
203
+ await restoreCheckpoint(checkpoint, request.signal).catch(() => undefined);
204
+ throw error;
205
+ }
206
+ }
207
+ async function worktree(request) {
208
+ if (request.action === "list") {
209
+ const result = await gitRequireOk(runner, {
210
+ args: ["worktree", "list", "--porcelain", "-z"],
211
+ cwd,
212
+ signal: request.signal,
213
+ maxOutputBytes: limits.maxOutputBytes,
214
+ }, "git worktree list");
215
+ const records = gitText(result).split("\0").filter(Boolean);
216
+ const worktrees = [];
217
+ let current;
218
+ for (const record of records) {
219
+ if (record.startsWith("worktree ")) {
220
+ if (current)
221
+ worktrees.push(current);
222
+ current = { path: record.slice("worktree ".length) };
223
+ }
224
+ else if (current && record.startsWith("HEAD ")) {
225
+ current.head = record.slice("HEAD ".length);
226
+ }
227
+ else if (current && record.startsWith("branch ")) {
228
+ current.branch = record.slice("branch ".length);
229
+ }
230
+ }
231
+ if (current)
232
+ worktrees.push(current);
233
+ if (worktrees.length > limits.maxWorktrees) {
234
+ return { worktrees: worktrees.slice(0, limits.maxWorktrees) };
235
+ }
236
+ return { worktrees };
237
+ }
238
+ if (request.action === "add") {
239
+ const existing = await worktree({ action: "list", signal: request.signal });
240
+ if (existing.worktrees.length >= limits.maxWorktrees) {
241
+ throw new GitError(`worktree count would exceed ${limits.maxWorktrees} limit`);
242
+ }
243
+ if (!request.path)
244
+ throw new GitError("worktree path is required");
245
+ if (request.path.includes("\0") || request.path.startsWith("-")) {
246
+ throw new GitError("worktree path must not start with '-' or contain NUL");
247
+ }
248
+ const args = ["worktree", "add"];
249
+ if (request.branch) {
250
+ const branchName = await validateBranchName(runner, cwd, request.branch, limits, request.signal);
251
+ args.push("-b", branchName);
252
+ }
253
+ args.push("--", request.path);
254
+ await gitRequireOk(runner, { args, cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes }, "git worktree add");
255
+ return { worktrees: (await worktree({ action: "list", signal: request.signal })).worktrees, path: request.path };
256
+ }
257
+ // remove
258
+ if (!request.path)
259
+ throw new GitError("worktree path is required");
260
+ const args = ["worktree", "remove"];
261
+ if (request.force)
262
+ args.push("--force");
263
+ args.push("--", request.path);
264
+ await gitRequireOk(runner, { args, cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes }, "git worktree remove");
265
+ return { worktrees: (await worktree({ action: "list", signal: request.signal })).worktrees, path: request.path };
266
+ }
267
+ async function apply(request) {
268
+ if (typeof request.patch !== "string")
269
+ throw new GitError("patch must be a string");
270
+ const patchBytes = byteLength(request.patch);
271
+ if (patchBytes < 1)
272
+ throw new GitError("patch must be non-empty");
273
+ if (patchBytes > limits.maxPatchBytes) {
274
+ throw new GitError(`patch exceeds ${limits.maxPatchBytes} byte limit`);
275
+ }
276
+ const runApply = async (args, filePath) => runner.exec({
277
+ args: [...args, "--", filePath],
278
+ cwd,
279
+ signal: request.signal,
280
+ maxOutputBytes: limits.maxOutputBytes,
281
+ });
282
+ return await withTempFile("prism-git-patch-", request.patch, async (filePath) => {
283
+ if (request.action === "check") {
284
+ const result = await runApply(["apply", "--check"], filePath);
285
+ const output = (gitText(result, "stderr") || gitText(result)).trim();
286
+ if (result.exitCode !== 0) {
287
+ return { ok: false, output: output || `exit ${result.exitCode}` };
288
+ }
289
+ return { ok: true, output: output || "patch applies cleanly" };
290
+ }
291
+ const checkpoint = request.action === "apply"
292
+ ? await ensureCleanOrCheckpoint(request.createCheckpoint, request.signal, `git apply ${request.action}`)
293
+ : undefined;
294
+ // Always check first for apply/reverse.
295
+ const checkArgs = request.action === "reverse"
296
+ ? ["apply", "--reverse", "--check"]
297
+ : ["apply", "--check"];
298
+ const check = await runApply(checkArgs, filePath);
299
+ if (check.exitCode !== 0) {
300
+ const output = (gitText(check, "stderr") || gitText(check)).trim();
301
+ if (checkpoint)
302
+ await restoreCheckpoint(checkpoint, request.signal).catch(() => undefined);
303
+ return { ok: false, checkpoint, restored: Boolean(checkpoint), output: output || "patch check failed" };
304
+ }
305
+ const applyArgs = request.action === "reverse"
306
+ ? ["apply", "--reverse"]
307
+ : ["apply"];
308
+ const result = await runApply(applyArgs, filePath);
309
+ if (result.exitCode !== 0) {
310
+ const output = (gitText(result, "stderr") || gitText(result)).trim();
311
+ let restored = false;
312
+ if (checkpoint) {
313
+ restored = await restoreCheckpoint(checkpoint, request.signal).catch(() => false);
314
+ }
315
+ else {
316
+ // Best-effort restore of tracked files when no checkpoint was taken (clean tree).
317
+ await runner.exec({
318
+ args: ["checkout", "--", "."],
319
+ cwd,
320
+ signal: request.signal,
321
+ maxOutputBytes: limits.maxOutputBytes,
322
+ }).catch(() => undefined);
323
+ restored = true;
324
+ }
325
+ return { ok: false, checkpoint, restored, output: output || `apply failed with exit ${result.exitCode}` };
326
+ }
327
+ return {
328
+ ok: true,
329
+ checkpoint,
330
+ output: (gitText(result, "stderr") || gitText(result) || "patch applied").trim(),
331
+ };
332
+ });
333
+ }
334
+ async function commit(request) {
335
+ const paths = validatePaths(request.paths, limits);
336
+ if (typeof request.message !== "string" || request.message.trim().length === 0) {
337
+ throw new GitError("commit message is required");
338
+ }
339
+ if (byteLength(request.message) > limits.maxMessageBytes) {
340
+ throw new GitError(`commit message exceeds ${limits.maxMessageBytes} byte limit`);
341
+ }
342
+ const identity = options.commitIdentity;
343
+ if (!identity?.name?.trim() || !identity?.email?.trim()) {
344
+ throw new GitError("commitIdentity name and email are required for git commit");
345
+ }
346
+ if (identity.name.includes("\n") || identity.email.includes("\n")) {
347
+ throw new GitError("commitIdentity must not contain newlines");
348
+ }
349
+ const checkpoint = await ensureCleanOrCheckpoint(request.createCheckpoint, request.signal, "git commit", new Set(paths));
350
+ try {
351
+ await gitRequireOk(runner, {
352
+ args: ["add", "--", ...paths],
353
+ cwd,
354
+ signal: request.signal,
355
+ maxOutputBytes: limits.maxOutputBytes,
356
+ }, "git add");
357
+ await withTempFile("prism-git-msg-", request.message, async (messageFile) => {
358
+ await gitRequireOk(runner, {
359
+ args: [
360
+ "-c",
361
+ `user.name=${identity.name}`,
362
+ "-c",
363
+ `user.email=${identity.email}`,
364
+ "commit",
365
+ "--no-verify",
366
+ "-F",
367
+ messageFile,
368
+ "--",
369
+ ...paths,
370
+ ],
371
+ cwd,
372
+ signal: request.signal,
373
+ maxOutputBytes: limits.maxOutputBytes,
374
+ }, "git commit");
375
+ });
376
+ const shaResult = await gitRequireOk(runner, { args: ["rev-parse", "HEAD"], cwd, signal: request.signal, maxOutputBytes: 64 * 1024 }, "git rev-parse HEAD");
377
+ return { sha: gitText(shaResult).trim(), checkpoint };
378
+ }
379
+ catch (error) {
380
+ // Reset index for the attempted paths; never drop pre-existing dirty work unless checkpointed.
381
+ await runner.exec({
382
+ args: ["reset", "-q", "HEAD", "--", ...paths],
383
+ cwd,
384
+ signal: request.signal,
385
+ maxOutputBytes: limits.maxOutputBytes,
386
+ }).catch(() => undefined);
387
+ if (checkpoint)
388
+ await restoreCheckpoint(checkpoint, request.signal).catch(() => undefined);
389
+ throw error;
390
+ }
391
+ }
392
+ async function prHandoff(request) {
393
+ const base = request.base;
394
+ if (!base || byteLength(base) > limits.maxRefBytes) {
395
+ throw new GitError("base ref is required and must be within ref byte limits");
396
+ }
397
+ const headResult = await gitRequireOk(runner, {
398
+ args: ["rev-parse", "--verify", request.head ?? "HEAD"],
399
+ cwd,
400
+ signal: request.signal,
401
+ maxOutputBytes: 64 * 1024,
402
+ }, "git rev-parse head");
403
+ const head = gitText(headResult).trim();
404
+ const baseShaResult = await gitRequireOk(runner, { args: ["rev-parse", "--verify", base], cwd, signal: request.signal, maxOutputBytes: 64 * 1024 }, "git rev-parse base");
405
+ const baseSha = gitText(baseShaResult).trim();
406
+ const log = await gitRequireOk(runner, {
407
+ args: ["log", "--format=%H%x09%s", `${baseSha}..${head}`],
408
+ cwd,
409
+ signal: request.signal,
410
+ maxOutputBytes: limits.maxOutputBytes,
411
+ }, "git log");
412
+ const commits = gitText(log)
413
+ .split("\n")
414
+ .map((line) => line.trim())
415
+ .filter(Boolean)
416
+ .slice(0, limits.maxPrCommits)
417
+ .map((line) => {
418
+ const tab = line.indexOf("\t");
419
+ if (tab < 0)
420
+ return { sha: line, subject: "" };
421
+ return { sha: line.slice(0, tab), subject: line.slice(tab + 1) };
422
+ });
423
+ const nameStatus = await gitRequireOk(runner, {
424
+ args: ["diff", "--no-ext-diff", "--no-textconv", "--name-only", `${baseSha}...${head}`],
425
+ cwd,
426
+ signal: request.signal,
427
+ maxOutputBytes: limits.maxOutputBytes,
428
+ }, "git diff name-only");
429
+ const changedPaths = gitText(nameStatus)
430
+ .split("\n")
431
+ .map((line) => line.trim())
432
+ .filter(Boolean)
433
+ .slice(0, limits.maxChangedFiles)
434
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
435
+ const stat = await gitRequireOk(runner, {
436
+ args: ["diff", "--no-ext-diff", "--no-textconv", "--stat", `${baseSha}...${head}`],
437
+ cwd,
438
+ signal: request.signal,
439
+ maxOutputBytes: limits.maxOutputBytes,
440
+ }, "git diff --stat");
441
+ const diffstat = truncateLines(gitText(stat), 200).text.trim();
442
+ let artifact;
443
+ if (artifacts) {
444
+ if (request.includeBundle) {
445
+ const bundleDir = await mkdtemp(join(tmpdir(), "prism-git-bundle-"));
446
+ const bundlePath = join(bundleDir, "handoff.bundle");
447
+ try {
448
+ await gitRequireOk(runner, {
449
+ args: ["bundle", "create", bundlePath, `${baseSha}..${head}`],
450
+ cwd,
451
+ signal: request.signal,
452
+ maxOutputBytes: limits.maxOutputBytes,
453
+ }, "git bundle create");
454
+ const { readFile } = await import("node:fs/promises");
455
+ const bytes = await readFile(bundlePath);
456
+ artifact = await artifacts({ kind: "bundle", filename: "handoff.bundle", bytes });
457
+ }
458
+ finally {
459
+ await rm(bundleDir, { recursive: true, force: true });
460
+ }
461
+ }
462
+ else {
463
+ const patch = await gitRequireOk(runner, {
464
+ args: ["diff", "--no-ext-diff", "--no-textconv", "--binary", `${baseSha}...${head}`],
465
+ cwd,
466
+ signal: request.signal,
467
+ maxOutputBytes: limits.maxOutputBytes,
468
+ }, "git diff patch");
469
+ artifact = await artifacts({
470
+ kind: "patch",
471
+ filename: "handoff.patch",
472
+ bytes: patch.stdout,
473
+ });
474
+ }
475
+ }
476
+ const handoff = {
477
+ base: baseSha,
478
+ head,
479
+ commits,
480
+ changedPaths,
481
+ diffstat,
482
+ checks: [...(request.checks ?? [])],
483
+ artifact,
484
+ };
485
+ const encoded = Buffer.from(JSON.stringify(handoff), "utf8");
486
+ if (encoded.length > limits.maxPrHandoffBytes) {
487
+ throw new GitError(`PR handoff JSON exceeds ${limits.maxPrHandoffBytes} byte limit`);
488
+ }
489
+ return handoff;
490
+ }
491
+ return { status, diff, branch, worktree, apply, commit, prHandoff };
492
+ }
493
+ export { parsePorcelainV2 } from "./git-status.js";
494
+ export { GitError, SAFE_GIT_ENV, SAFE_GIT_CONFIG_ARGS, createBoundGitRunner, runGitCli, } from "./git-exec.js";
495
+ //# sourceMappingURL=git.js.map
package/dist/index.d.ts CHANGED
@@ -6,14 +6,32 @@ export { createWriteTool } from "./write.js";
6
6
  export type { WriteToolOptions, WriteOperations } from "./write.js";
7
7
  export { createEditTool } from "./edit.js";
8
8
  export type { EditToolOptions, EditOperations, EditToolDetails, Edit } from "./edit.js";
9
+ export { createRepoListTool } from "./list.js";
10
+ export type { ListToolOptions } from "./list.js";
11
+ export { createRepoSearchTool } from "./search.js";
12
+ export type { SearchToolOptions } from "./search.js";
13
+ export { createLocalRepositoryOperations, resolveRepositoryLimits, compileSearchPattern, isBinaryBuffer, resolveRepoPath, toRepoRelative, RepositoryError, DEFAULT_REPO_EXCLUDE, } from "./repository.js";
14
+ export type { RepoEntryKind, RepoListEntry, RepositoryListRequest, RepositoryListResult, RepositorySearchMatch, RepositorySearchRequest, RepositorySearchResult, RepositoryOperations, RepositoryLimitOptions, ResolvedRepositoryLimits, } from "./repository.js";
15
+ export { createGitOperations, resolveGitLimits, parsePorcelainV2, GitError, SAFE_GIT_ENV, SAFE_GIT_CONFIG_ARGS, createBoundGitRunner, runGitCli, } from "./git.js";
16
+ export type { GitOperations, GitLimitOptions, ResolvedGitLimits, ArtifactReference, ArtifactWriter, PrHandoff, CreateGitOperationsOptions, GitStatusResult, GitStatusEntry, GitStatusBranch, GitStatusEntryKind, GitRunner, GitExecRequest, GitExecResult, BoundGitRunner, CreateGitRunnerOptions, } from "./git.js";
17
+ export { createGitTools, createGitStatusTool, createGitDiffTool, createGitBranchTool, createGitWorktreeTool, createGitApplyTool, createGitCommitTool, createGitPrHandoffTool, } from "./git-tools.js";
18
+ export type { GitToolsOptions } from "./git-tools.js";
19
+ export { createCodingCheckTool } from "./checks.js";
20
+ export type { CodingCheckToolOptions, NamedCheckDefinition } from "./checks.js";
21
+ export { createDirectoryArtifactWriter, createTempArtifactWriter, sha256Hex } from "./artifacts.js";
22
+ export { CODING_CHECKPOINT_SCHEMA_VERSION, CODING_STATE_KEY, CodingCheckpointError, assertCodingResumeAllowed, buildCodingCheckpointMetadata, codingCheckpointStatePatch, codingPlanPathForTask, createCodingArtifactRef, createCodingPlanMarkdown, fingerprintJson, parseCodingPlanTodos, readCodingCheckpointFromState, readCodingPlanFile, resolveCodingCheckpointLimits, validateCodingCheckpointMetadata, verifyCodingArtifactBytes, writeCodingPlanFile, } from "./coding-checkpoint.js";
23
+ export type { CodingArtifactKind, CodingArtifactRef, CodingCheckSummary, CodingCheckpointLimitOptions, CodingCheckpointMetadata, CodingFingerprints, CodingHandoffSummary, CodingTaskStatus, CodingTodoItem, ResolvedCodingCheckpointLimits, } from "./coding-checkpoint.js";
9
24
  export { withFileMutationQueue } from "./file-mutation-queue.js";
10
25
  export { enforceExecutionPolicy } from "./execution-policy.js";
11
- export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, DEFAULT_MAX_WRITE_BYTES, DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, DEFAULT_SHELL_TIMEOUT_SECONDS, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_WRITE_BYTES, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, HARD_SHELL_TIMEOUT_SECONDS, HARD_MAX_TOTAL_OUTPUT_BYTES, } from "./limits.js";
26
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, DEFAULT_MAX_WRITE_BYTES, DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, DEFAULT_SHELL_TIMEOUT_SECONDS, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, DEFAULT_MAX_REPO_DEPTH, DEFAULT_MAX_REPO_ENTRIES, DEFAULT_MAX_REPO_FILES, DEFAULT_MAX_REPO_RESULTS, DEFAULT_MAX_REPO_CONCURRENCY, DEFAULT_MAX_SEARCH_SCAN_BYTES, DEFAULT_MAX_SEARCH_FILE_BYTES, DEFAULT_MAX_SEARCH_MATCHES, DEFAULT_MAX_SEARCH_PATTERN_BYTES, DEFAULT_MAX_SEARCH_LINE_BYTES, DEFAULT_MAX_SEARCH_CONTEXT_LINES, DEFAULT_MAX_SEARCH_TIME_MS, DEFAULT_MAX_GIT_PATHS, DEFAULT_MAX_GIT_REF_BYTES, DEFAULT_MAX_GIT_MESSAGE_BYTES, DEFAULT_MAX_GIT_OUTPUT_BYTES, DEFAULT_MAX_GIT_DIFF_LINES, DEFAULT_MAX_GIT_CHANGED_FILES, DEFAULT_MAX_GIT_PATCH_BYTES, DEFAULT_MAX_GIT_WORKTREES, DEFAULT_GIT_TIMEOUT_MS, DEFAULT_MAX_CHECK_NAMES, DEFAULT_MAX_CHECK_CONCURRENCY, DEFAULT_CHECK_TIMEOUT_MS, DEFAULT_MAX_CHECK_DIAGNOSTIC_LINES, DEFAULT_MAX_CHECK_OUTPUT_BYTES, DEFAULT_MAX_PR_HANDOFF_BYTES, DEFAULT_MAX_PR_COMMITS, DEFAULT_MAX_PLAN_BYTES, DEFAULT_MAX_TODOS, DEFAULT_MAX_TODO_TEXT_BYTES, DEFAULT_MAX_CODING_ARTIFACTS, DEFAULT_MAX_CODING_ARTIFACT_BYTES, DEFAULT_MAX_CHECK_SUMMARY_BYTES, DEFAULT_MAX_CODING_CHECKPOINT_BYTES, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_WRITE_BYTES, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, HARD_SHELL_TIMEOUT_SECONDS, HARD_MAX_TOTAL_OUTPUT_BYTES, HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_ENTRIES, HARD_MAX_REPO_FILES, HARD_MAX_REPO_RESULTS, HARD_MAX_REPO_CONCURRENCY, HARD_MAX_SEARCH_SCAN_BYTES, HARD_MAX_SEARCH_FILE_BYTES, HARD_MAX_SEARCH_MATCHES, HARD_MAX_SEARCH_PATTERN_BYTES, HARD_MAX_SEARCH_LINE_BYTES, HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_TIME_MS, HARD_MAX_GIT_PATHS, HARD_MAX_GIT_REF_BYTES, HARD_MAX_GIT_MESSAGE_BYTES, HARD_MAX_GIT_OUTPUT_BYTES, HARD_MAX_GIT_DIFF_LINES, HARD_MAX_GIT_CHANGED_FILES, HARD_MAX_GIT_PATCH_BYTES, HARD_MAX_GIT_WORKTREES, HARD_GIT_TIMEOUT_MS, HARD_MAX_CHECK_NAMES, HARD_MAX_CHECK_CONCURRENCY, HARD_CHECK_TIMEOUT_MS, HARD_MAX_CHECK_DIAGNOSTIC_LINES, HARD_MAX_CHECK_OUTPUT_BYTES, HARD_MAX_PR_HANDOFF_BYTES, HARD_MAX_PR_COMMITS, HARD_MAX_PLAN_BYTES, HARD_MAX_TODOS, HARD_MAX_TODO_TEXT_BYTES, HARD_MAX_CODING_ARTIFACTS, HARD_MAX_CODING_ARTIFACT_BYTES, HARD_MAX_CHECK_SUMMARY_BYTES, HARD_MAX_CODING_CHECKPOINT_BYTES, } from "./limits.js";
12
27
  import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
13
28
  import type { ShellToolOptions } from "./shell.js";
14
29
  import type { ReadToolOptions } from "./read.js";
15
30
  import type { WriteToolOptions } from "./write.js";
16
31
  import type { EditToolOptions } from "./edit.js";
32
+ import type { ListToolOptions } from "./list.js";
33
+ import type { SearchToolOptions } from "./search.js";
34
+ import type { RepositoryLimitOptions, RepositoryOperations } from "./repository.js";
17
35
  /** Per-tool options combined for the aggregator factories. */
18
36
  export interface ToolsOptions {
19
37
  /** Shared execution policy applied to every coding tool unless overridden per tool. */
@@ -22,12 +40,24 @@ export interface ToolsOptions {
22
40
  read?: ReadToolOptions;
23
41
  write?: WriteToolOptions;
24
42
  edit?: EditToolOptions;
43
+ list?: ListToolOptions;
44
+ search?: SearchToolOptions;
45
+ /**
46
+ * Shared repository limits/backends for `repo_list` / `repo_search`.
47
+ * Per-tool `list` / `search` options override these when both are set.
48
+ */
49
+ repository?: RepositoryLimitOptions & {
50
+ operations?: RepositoryOperations;
51
+ };
25
52
  }
26
53
  /**
27
- * The four coding tools: `shell`, `read`, `write`, `edit`. Register all of them for a coding agent.
54
+ * Full coding tool set: `shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`.
28
55
  */
29
56
  export declare function createCodingTools(cwd: string, options?: ToolsOptions): readonly ToolDefinition[];
30
- /** Read-only subset: `read` only (this package ships no grep/find/ls). */
57
+ /**
58
+ * Read-only subset: `read`, `repo_list`, `repo_search`.
59
+ * Deliberate 0.0.9 expansion from the previous `read`-only set.
60
+ */
31
61
  export declare function createReadOnlyTools(cwd: string, options?: ToolsOptions): readonly ToolDefinition[];
32
- /** Every tool this package provides — identical to {@link createCodingTools} for now. */
62
+ /** Every tool this package provides — identical to {@link createCodingTools}. */
33
63
  export declare function createAllTools(cwd: string, options?: ToolsOptions): readonly ToolDefinition[];
package/dist/index.js CHANGED
@@ -8,36 +8,70 @@ export { createShellTool, createLocalBashOperations, getShellConfig, killProcess
8
8
  export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, DEFAULT_MAX_IMAGE_BYTES, } from "./read.js";
9
9
  export { createWriteTool } from "./write.js";
10
10
  export { createEditTool } from "./edit.js";
11
+ export { createRepoListTool } from "./list.js";
12
+ export { createRepoSearchTool } from "./search.js";
13
+ export { createLocalRepositoryOperations, resolveRepositoryLimits, compileSearchPattern, isBinaryBuffer, resolveRepoPath, toRepoRelative, RepositoryError, DEFAULT_REPO_EXCLUDE, } from "./repository.js";
14
+ export { createGitOperations, resolveGitLimits, parsePorcelainV2, GitError, SAFE_GIT_ENV, SAFE_GIT_CONFIG_ARGS, createBoundGitRunner, runGitCli, } from "./git.js";
15
+ export { createGitTools, createGitStatusTool, createGitDiffTool, createGitBranchTool, createGitWorktreeTool, createGitApplyTool, createGitCommitTool, createGitPrHandoffTool, } from "./git-tools.js";
16
+ export { createCodingCheckTool } from "./checks.js";
17
+ export { createDirectoryArtifactWriter, createTempArtifactWriter, sha256Hex } from "./artifacts.js";
18
+ export { CODING_CHECKPOINT_SCHEMA_VERSION, CODING_STATE_KEY, CodingCheckpointError, assertCodingResumeAllowed, buildCodingCheckpointMetadata, codingCheckpointStatePatch, codingPlanPathForTask, createCodingArtifactRef, createCodingPlanMarkdown, fingerprintJson, parseCodingPlanTodos, readCodingCheckpointFromState, readCodingPlanFile, resolveCodingCheckpointLimits, validateCodingCheckpointMetadata, verifyCodingArtifactBytes, writeCodingPlanFile, } from "./coding-checkpoint.js";
11
19
  // --- generic primitives (re-exported for hosts that want them) ---
12
20
  export { withFileMutationQueue } from "./file-mutation-queue.js";
13
21
  export { enforceExecutionPolicy } from "./execution-policy.js";
14
- export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, DEFAULT_MAX_WRITE_BYTES, DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, DEFAULT_SHELL_TIMEOUT_SECONDS, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_WRITE_BYTES, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, HARD_SHELL_TIMEOUT_SECONDS, HARD_MAX_TOTAL_OUTPUT_BYTES, } from "./limits.js";
22
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, DEFAULT_MAX_WRITE_BYTES, DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, DEFAULT_SHELL_TIMEOUT_SECONDS, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, DEFAULT_MAX_REPO_DEPTH, DEFAULT_MAX_REPO_ENTRIES, DEFAULT_MAX_REPO_FILES, DEFAULT_MAX_REPO_RESULTS, DEFAULT_MAX_REPO_CONCURRENCY, DEFAULT_MAX_SEARCH_SCAN_BYTES, DEFAULT_MAX_SEARCH_FILE_BYTES, DEFAULT_MAX_SEARCH_MATCHES, DEFAULT_MAX_SEARCH_PATTERN_BYTES, DEFAULT_MAX_SEARCH_LINE_BYTES, DEFAULT_MAX_SEARCH_CONTEXT_LINES, DEFAULT_MAX_SEARCH_TIME_MS, DEFAULT_MAX_GIT_PATHS, DEFAULT_MAX_GIT_REF_BYTES, DEFAULT_MAX_GIT_MESSAGE_BYTES, DEFAULT_MAX_GIT_OUTPUT_BYTES, DEFAULT_MAX_GIT_DIFF_LINES, DEFAULT_MAX_GIT_CHANGED_FILES, DEFAULT_MAX_GIT_PATCH_BYTES, DEFAULT_MAX_GIT_WORKTREES, DEFAULT_GIT_TIMEOUT_MS, DEFAULT_MAX_CHECK_NAMES, DEFAULT_MAX_CHECK_CONCURRENCY, DEFAULT_CHECK_TIMEOUT_MS, DEFAULT_MAX_CHECK_DIAGNOSTIC_LINES, DEFAULT_MAX_CHECK_OUTPUT_BYTES, DEFAULT_MAX_PR_HANDOFF_BYTES, DEFAULT_MAX_PR_COMMITS, DEFAULT_MAX_PLAN_BYTES, DEFAULT_MAX_TODOS, DEFAULT_MAX_TODO_TEXT_BYTES, DEFAULT_MAX_CODING_ARTIFACTS, DEFAULT_MAX_CODING_ARTIFACT_BYTES, DEFAULT_MAX_CHECK_SUMMARY_BYTES, DEFAULT_MAX_CODING_CHECKPOINT_BYTES, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_WRITE_BYTES, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, HARD_SHELL_TIMEOUT_SECONDS, HARD_MAX_TOTAL_OUTPUT_BYTES, HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_ENTRIES, HARD_MAX_REPO_FILES, HARD_MAX_REPO_RESULTS, HARD_MAX_REPO_CONCURRENCY, HARD_MAX_SEARCH_SCAN_BYTES, HARD_MAX_SEARCH_FILE_BYTES, HARD_MAX_SEARCH_MATCHES, HARD_MAX_SEARCH_PATTERN_BYTES, HARD_MAX_SEARCH_LINE_BYTES, HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_TIME_MS, HARD_MAX_GIT_PATHS, HARD_MAX_GIT_REF_BYTES, HARD_MAX_GIT_MESSAGE_BYTES, HARD_MAX_GIT_OUTPUT_BYTES, HARD_MAX_GIT_DIFF_LINES, HARD_MAX_GIT_CHANGED_FILES, HARD_MAX_GIT_PATCH_BYTES, HARD_MAX_GIT_WORKTREES, HARD_GIT_TIMEOUT_MS, HARD_MAX_CHECK_NAMES, HARD_MAX_CHECK_CONCURRENCY, HARD_CHECK_TIMEOUT_MS, HARD_MAX_CHECK_DIAGNOSTIC_LINES, HARD_MAX_CHECK_OUTPUT_BYTES, HARD_MAX_PR_HANDOFF_BYTES, HARD_MAX_PR_COMMITS, HARD_MAX_PLAN_BYTES, HARD_MAX_TODOS, HARD_MAX_TODO_TEXT_BYTES, HARD_MAX_CODING_ARTIFACTS, HARD_MAX_CODING_ARTIFACT_BYTES, HARD_MAX_CHECK_SUMMARY_BYTES, HARD_MAX_CODING_CHECKPOINT_BYTES, } from "./limits.js";
15
23
  import { createShellTool } from "./shell.js";
16
24
  import { createReadTool } from "./read.js";
17
25
  import { createWriteTool } from "./write.js";
18
26
  import { createEditTool } from "./edit.js";
27
+ import { createRepoListTool } from "./list.js";
28
+ import { createRepoSearchTool } from "./search.js";
19
29
  function withSharedExecutionPolicy(toolOptions, shared) {
20
30
  if (!shared)
21
31
  return (toolOptions ?? {});
22
32
  return { ...(toolOptions ?? {}), executionPolicy: toolOptions?.executionPolicy ?? shared };
23
33
  }
34
+ function withRepositoryDefaults(toolOptions, shared) {
35
+ if (!shared && !toolOptions)
36
+ return {};
37
+ return {
38
+ ...(toolOptions ?? {}),
39
+ repository: toolOptions?.repository ?? shared,
40
+ operations: toolOptions?.operations ?? shared?.operations,
41
+ exclude: toolOptions?.exclude ?? shared?.exclude,
42
+ };
43
+ }
24
44
  /**
25
- * The four coding tools: `shell`, `read`, `write`, `edit`. Register all of them for a coding agent.
45
+ * Full coding tool set: `shell`, `read`, `write`, `edit`, `repo_list`, `repo_search`.
26
46
  */
27
47
  export function createCodingTools(cwd, options) {
28
48
  const policy = options?.executionPolicy;
49
+ const listOpts = withRepositoryDefaults(options?.list, options?.repository);
50
+ const searchOpts = withRepositoryDefaults(options?.search, options?.repository);
29
51
  return [
30
52
  createShellTool(cwd, withSharedExecutionPolicy(options?.shell, policy)),
31
53
  createReadTool(cwd, withSharedExecutionPolicy(options?.read, policy)),
32
54
  createWriteTool(cwd, withSharedExecutionPolicy(options?.write, policy)),
33
55
  createEditTool(cwd, withSharedExecutionPolicy(options?.edit, policy)),
56
+ createRepoListTool(cwd, withSharedExecutionPolicy(listOpts, policy)),
57
+ createRepoSearchTool(cwd, withSharedExecutionPolicy(searchOpts, policy)),
34
58
  ];
35
59
  }
36
- /** Read-only subset: `read` only (this package ships no grep/find/ls). */
60
+ /**
61
+ * Read-only subset: `read`, `repo_list`, `repo_search`.
62
+ * Deliberate 0.0.9 expansion from the previous `read`-only set.
63
+ */
37
64
  export function createReadOnlyTools(cwd, options) {
38
- return [createReadTool(cwd, withSharedExecutionPolicy(options?.read, options?.executionPolicy))];
65
+ const policy = options?.executionPolicy;
66
+ const listOpts = withRepositoryDefaults(options?.list, options?.repository);
67
+ const searchOpts = withRepositoryDefaults(options?.search, options?.repository);
68
+ return [
69
+ createReadTool(cwd, withSharedExecutionPolicy(options?.read, policy)),
70
+ createRepoListTool(cwd, withSharedExecutionPolicy(listOpts, policy)),
71
+ createRepoSearchTool(cwd, withSharedExecutionPolicy(searchOpts, policy)),
72
+ ];
39
73
  }
40
- /** Every tool this package provides — identical to {@link createCodingTools} for now. */
74
+ /** Every tool this package provides — identical to {@link createCodingTools}. */
41
75
  export function createAllTools(cwd, options) {
42
76
  return createCodingTools(cwd, options);
43
77
  }