fix(cli): remove side-effect from agent:end state updater (#133) (#147)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #147.
This commit is contained in:
2026-03-15 19:09:13 +00:00
committed by jason.woltje
parent c4850fe6c1
commit 76abf11eba
4 changed files with 293 additions and 31 deletions

View File

@@ -2,12 +2,29 @@ import { Type } from '@sinclair/typebox';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { resolve, relative } from 'node:path';
const execAsync = promisify(exec);
const GIT_TIMEOUT_MS = 15_000;
const MAX_OUTPUT_BYTES = 100 * 1024; // 100 KB
/**
* Clamp a user-supplied cwd to within the sandbox directory.
* If the resolved path escapes the sandbox (via ../ or absolute path outside),
* falls back to the sandbox directory itself.
*/
function clampCwd(sandboxDir: string, requestedCwd?: string): string {
if (!requestedCwd) return sandboxDir;
const resolved = resolve(sandboxDir, requestedCwd);
const rel = relative(sandboxDir, resolved);
if (rel.startsWith('..') || rel.startsWith('/')) {
// Escape attempt — fall back to sandbox root
return sandboxDir;
}
return resolved;
}
async function runGit(
args: string[],
cwd?: string,
@@ -41,17 +58,24 @@ async function runGit(
}
}
export function createGitTools(defaultCwd?: string): ToolDefinition[] {
export function createGitTools(sandboxDir?: string): ToolDefinition[] {
const defaultCwd = sandboxDir ?? process.cwd();
const gitStatus: ToolDefinition = {
name: 'git_status',
label: 'Git Status',
description: 'Show the working tree status (staged, unstaged, untracked files).',
parameters: Type.Object({
cwd: Type.Optional(Type.String({ description: 'Repository working directory.' })),
cwd: Type.Optional(
Type.String({
description: 'Repository working directory (relative to sandbox or absolute within it).',
}),
),
}),
async execute(_toolCallId, params) {
const { cwd } = params as { cwd?: string };
const result = await runGit(['status', '--short', '--branch'], cwd ?? defaultCwd);
const safeCwd = clampCwd(defaultCwd, cwd);
const result = await runGit(['status', '--short', '--branch'], safeCwd);
const text = result.error
? `Error: ${result.error}\n${result.stderr}`
: result.stdout || '(no output)';
@@ -71,7 +95,11 @@ export function createGitTools(defaultCwd?: string): ToolDefinition[] {
oneline: Type.Optional(
Type.Boolean({ description: 'Compact one-line format (default true)' }),
),
cwd: Type.Optional(Type.String({ description: 'Repository working directory.' })),
cwd: Type.Optional(
Type.String({
description: 'Repository working directory (relative to sandbox or absolute within it).',
}),
),
}),
async execute(_toolCallId, params) {
const { limit, oneline, cwd } = params as {
@@ -79,9 +107,10 @@ export function createGitTools(defaultCwd?: string): ToolDefinition[] {
oneline?: boolean;
cwd?: string;
};
const safeCwd = clampCwd(defaultCwd, cwd);
const args = ['log', `--max-count=${limit ?? 20}`];
if (oneline !== false) args.push('--oneline');
const result = await runGit(args, cwd ?? defaultCwd);
const result = await runGit(args, safeCwd);
const text = result.error
? `Error: ${result.error}\n${result.stderr}`
: result.stdout || '(no commits)';
@@ -106,7 +135,11 @@ export function createGitTools(defaultCwd?: string): ToolDefinition[] {
path: Type.Optional(
Type.String({ description: 'Limit diff to a specific file or directory' }),
),
cwd: Type.Optional(Type.String({ description: 'Repository working directory.' })),
cwd: Type.Optional(
Type.String({
description: 'Repository working directory (relative to sandbox or absolute within it).',
}),
),
}),
async execute(_toolCallId, params) {
const { staged, ref, path, cwd } = params as {
@@ -115,12 +148,13 @@ export function createGitTools(defaultCwd?: string): ToolDefinition[] {
path?: string;
cwd?: string;
};
const safeCwd = clampCwd(defaultCwd, cwd);
const args = ['diff'];
if (staged) args.push('--cached');
if (ref) args.push(ref);
args.push('--');
if (path) args.push(path);
const result = await runGit(args, cwd ?? defaultCwd);
const result = await runGit(args, safeCwd);
const text = result.error
? `Error: ${result.error}\n${result.stderr}`
: result.stdout || '(no diff)';