gitclaw 0.3.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.
Files changed (58) hide show
  1. package/README.md +440 -0
  2. package/dist/agents.d.ts +8 -0
  3. package/dist/agents.js +82 -0
  4. package/dist/audit.d.ts +27 -0
  5. package/dist/audit.js +55 -0
  6. package/dist/compliance.d.ts +30 -0
  7. package/dist/compliance.js +108 -0
  8. package/dist/config.d.ts +11 -0
  9. package/dist/config.js +43 -0
  10. package/dist/examples.d.ts +6 -0
  11. package/dist/examples.js +40 -0
  12. package/dist/exports.d.ts +13 -0
  13. package/dist/exports.js +6 -0
  14. package/dist/hooks.d.ts +24 -0
  15. package/dist/hooks.js +108 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +542 -0
  18. package/dist/knowledge.d.ts +17 -0
  19. package/dist/knowledge.js +55 -0
  20. package/dist/loader.d.ts +64 -0
  21. package/dist/loader.js +222 -0
  22. package/dist/sandbox.d.ts +28 -0
  23. package/dist/sandbox.js +54 -0
  24. package/dist/sdk-hooks.d.ts +8 -0
  25. package/dist/sdk-hooks.js +31 -0
  26. package/dist/sdk-types.d.ts +127 -0
  27. package/dist/sdk-types.js +1 -0
  28. package/dist/sdk.d.ts +6 -0
  29. package/dist/sdk.js +444 -0
  30. package/dist/session.d.ts +15 -0
  31. package/dist/session.js +127 -0
  32. package/dist/skills.d.ts +18 -0
  33. package/dist/skills.js +104 -0
  34. package/dist/tool-loader.d.ts +3 -0
  35. package/dist/tool-loader.js +138 -0
  36. package/dist/tools/cli.d.ts +3 -0
  37. package/dist/tools/cli.js +86 -0
  38. package/dist/tools/index.d.ts +13 -0
  39. package/dist/tools/index.js +29 -0
  40. package/dist/tools/memory.d.ts +3 -0
  41. package/dist/tools/memory.js +128 -0
  42. package/dist/tools/read.d.ts +3 -0
  43. package/dist/tools/read.js +46 -0
  44. package/dist/tools/sandbox-cli.d.ts +4 -0
  45. package/dist/tools/sandbox-cli.js +48 -0
  46. package/dist/tools/sandbox-memory.d.ts +4 -0
  47. package/dist/tools/sandbox-memory.js +117 -0
  48. package/dist/tools/sandbox-read.d.ts +4 -0
  49. package/dist/tools/sandbox-read.js +25 -0
  50. package/dist/tools/sandbox-write.d.ts +4 -0
  51. package/dist/tools/sandbox-write.js +26 -0
  52. package/dist/tools/shared.d.ts +38 -0
  53. package/dist/tools/shared.js +69 -0
  54. package/dist/tools/write.d.ts +3 -0
  55. package/dist/tools/write.js +28 -0
  56. package/dist/workflows.d.ts +8 -0
  57. package/dist/workflows.js +81 -0
  58. package/package.json +57 -0
package/dist/skills.js ADDED
@@ -0,0 +1,104 @@
1
+ import { readFile, readdir, stat } from "fs/promises";
2
+ import { join } from "path";
3
+ import yaml from "js-yaml";
4
+ const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
5
+ function parseFrontmatter(content) {
6
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
7
+ if (!match) {
8
+ return { frontmatter: {}, body: content };
9
+ }
10
+ const frontmatter = yaml.load(match[1]);
11
+ return { frontmatter, body: match[2] };
12
+ }
13
+ async function dirExists(path) {
14
+ try {
15
+ const s = await stat(path);
16
+ return s.isDirectory();
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export async function discoverSkills(agentDir) {
23
+ const skillsDir = join(agentDir, "skills");
24
+ if (!(await dirExists(skillsDir))) {
25
+ return [];
26
+ }
27
+ const entries = await readdir(skillsDir, { withFileTypes: true });
28
+ const skills = [];
29
+ for (const entry of entries) {
30
+ if (!entry.isDirectory())
31
+ continue;
32
+ const skillDir = join(skillsDir, entry.name);
33
+ const skillFile = join(skillDir, "SKILL.md");
34
+ let content;
35
+ try {
36
+ content = await readFile(skillFile, "utf-8");
37
+ }
38
+ catch {
39
+ continue; // no SKILL.md, skip
40
+ }
41
+ const { frontmatter } = parseFrontmatter(content);
42
+ const name = frontmatter.name;
43
+ const description = frontmatter.description;
44
+ if (!name || !description) {
45
+ console.warn(`Skipping skill "${entry.name}": missing name or description in frontmatter`);
46
+ continue;
47
+ }
48
+ if (name !== entry.name) {
49
+ console.warn(`Skipping skill "${entry.name}": name "${name}" does not match directory`);
50
+ continue;
51
+ }
52
+ if (!KEBAB_RE.test(name)) {
53
+ console.warn(`Skipping skill "${entry.name}": name must be kebab-case`);
54
+ continue;
55
+ }
56
+ skills.push({
57
+ name,
58
+ description,
59
+ directory: skillDir,
60
+ filePath: skillFile,
61
+ });
62
+ }
63
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
64
+ }
65
+ export async function loadSkill(meta) {
66
+ const content = await readFile(meta.filePath, "utf-8");
67
+ const { body } = parseFrontmatter(content);
68
+ return {
69
+ ...meta,
70
+ instructions: body.trim(),
71
+ hasScripts: await dirExists(join(meta.directory, "scripts")),
72
+ hasReferences: await dirExists(join(meta.directory, "references")),
73
+ };
74
+ }
75
+ export function formatSkillsForPrompt(skills) {
76
+ if (skills.length === 0)
77
+ return "";
78
+ const skillEntries = skills
79
+ .map((s) => `<skill>\n<name>${s.name}</name>\n<description>${s.description}</description>\n</skill>`)
80
+ .join("\n");
81
+ return `# Skills
82
+
83
+ <available_skills>
84
+ ${skillEntries}
85
+ </available_skills>
86
+
87
+ When a task matches a skill, use the \`read\` tool to load \`skills/<name>/SKILL.md\` for full instructions. Scripts within a skill are relative to the skill's directory (e.g., \`skills/<name>/scripts/\`). Use the \`cli\` tool to execute them.`;
88
+ }
89
+ export async function expandSkillCommand(input, skills) {
90
+ const match = input.match(/^\/skill:([a-z0-9-]+)\s*([\s\S]*)$/);
91
+ if (!match)
92
+ return null;
93
+ const skillName = match[1];
94
+ const args = match[2].trim();
95
+ const skill = skills.find((s) => s.name === skillName);
96
+ if (!skill)
97
+ return null;
98
+ const parsed = await loadSkill(skill);
99
+ let expanded = `<skill name="${skillName}" baseDir="${skill.directory}">\n${parsed.instructions}\n</skill>`;
100
+ if (args) {
101
+ expanded += `\n\n${args}`;
102
+ }
103
+ return { expanded, skillName };
104
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ export declare function buildTypeboxSchema(schema: Record<string, any>): any;
3
+ export declare function loadDeclarativeTools(agentDir: string): Promise<AgentTool<any>[]>;
@@ -0,0 +1,138 @@
1
+ import { readFile, readdir, stat } from "fs/promises";
2
+ import { join } from "path";
3
+ import { spawn } from "child_process";
4
+ import yaml from "js-yaml";
5
+ import { Type } from "@sinclair/typebox";
6
+ export function buildTypeboxSchema(schema) {
7
+ // Convert a simplified JSON-schema-like object to Typebox properties
8
+ const properties = {};
9
+ if (schema.properties) {
10
+ for (const [key, def] of Object.entries(schema.properties)) {
11
+ const desc = def.description || "";
12
+ const required = schema.required?.includes(key) ?? false;
13
+ let prop;
14
+ switch (def.type) {
15
+ case "number":
16
+ prop = Type.Number({ description: desc });
17
+ break;
18
+ case "boolean":
19
+ prop = Type.Boolean({ description: desc });
20
+ break;
21
+ case "array":
22
+ prop = Type.Array(Type.Any(), { description: desc });
23
+ break;
24
+ case "object":
25
+ prop = Type.Any({ description: desc });
26
+ break;
27
+ default:
28
+ prop = Type.String({ description: desc });
29
+ break;
30
+ }
31
+ properties[key] = required ? prop : Type.Optional(prop);
32
+ }
33
+ }
34
+ return Type.Object(properties);
35
+ }
36
+ function createDeclarativeTool(def, agentDir) {
37
+ const schema = buildTypeboxSchema(def.input_schema);
38
+ const scriptPath = join(agentDir, "tools", def.implementation.script);
39
+ const runtime = def.implementation.runtime || "sh";
40
+ return {
41
+ name: def.name,
42
+ label: def.name,
43
+ description: def.description,
44
+ parameters: schema,
45
+ execute: async (_toolCallId, args, signal) => {
46
+ if (signal?.aborted)
47
+ throw new Error("Operation aborted");
48
+ return new Promise((resolve, reject) => {
49
+ const child = spawn(runtime, [scriptPath], {
50
+ cwd: agentDir,
51
+ stdio: ["pipe", "pipe", "pipe"],
52
+ env: { ...process.env },
53
+ });
54
+ let stdout = "";
55
+ let stderr = "";
56
+ child.stdout.on("data", (data) => {
57
+ stdout += data.toString("utf-8");
58
+ });
59
+ child.stderr.on("data", (data) => {
60
+ stderr += data.toString("utf-8");
61
+ });
62
+ // Send args as JSON on stdin
63
+ child.stdin.write(JSON.stringify(args));
64
+ child.stdin.end();
65
+ const timeout = setTimeout(() => {
66
+ child.kill("SIGTERM");
67
+ reject(new Error(`Tool "${def.name}" timed out after 120s`));
68
+ }, 120_000);
69
+ const onAbort = () => child.kill("SIGTERM");
70
+ if (signal)
71
+ signal.addEventListener("abort", onAbort, { once: true });
72
+ child.on("error", (err) => {
73
+ clearTimeout(timeout);
74
+ if (signal)
75
+ signal.removeEventListener("abort", onAbort);
76
+ reject(new Error(`Tool "${def.name}" failed to start: ${err.message}`));
77
+ });
78
+ child.on("close", (code) => {
79
+ clearTimeout(timeout);
80
+ if (signal)
81
+ signal.removeEventListener("abort", onAbort);
82
+ if (signal?.aborted) {
83
+ reject(new Error("Operation aborted"));
84
+ return;
85
+ }
86
+ if (code !== 0 && code !== null) {
87
+ reject(new Error(`Tool "${def.name}" exited with code ${code}: ${stderr.trim()}`));
88
+ return;
89
+ }
90
+ // Try parsing JSON output
91
+ let text = stdout.trim();
92
+ try {
93
+ const parsed = JSON.parse(text);
94
+ if (parsed.text)
95
+ text = parsed.text;
96
+ else if (parsed.result)
97
+ text = typeof parsed.result === "string" ? parsed.result : JSON.stringify(parsed.result);
98
+ }
99
+ catch {
100
+ // Raw text output is fine
101
+ }
102
+ resolve({
103
+ content: [{ type: "text", text: text || "(no output)" }],
104
+ details: undefined,
105
+ });
106
+ });
107
+ });
108
+ },
109
+ };
110
+ }
111
+ export async function loadDeclarativeTools(agentDir) {
112
+ const toolsDir = join(agentDir, "tools");
113
+ try {
114
+ const s = await stat(toolsDir);
115
+ if (!s.isDirectory())
116
+ return [];
117
+ }
118
+ catch {
119
+ return [];
120
+ }
121
+ const entries = await readdir(toolsDir);
122
+ const tools = [];
123
+ for (const entry of entries) {
124
+ if (!entry.endsWith(".yaml") && !entry.endsWith(".yml"))
125
+ continue;
126
+ try {
127
+ const raw = await readFile(join(toolsDir, entry), "utf-8");
128
+ const def = yaml.load(raw);
129
+ if (def?.name && def?.description && def?.input_schema && def?.implementation?.script) {
130
+ tools.push(createDeclarativeTool(def, agentDir));
131
+ }
132
+ }
133
+ catch {
134
+ // Skip invalid tool definitions
135
+ }
136
+ }
137
+ return tools;
138
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { cliSchema } from "./shared.js";
3
+ export declare function createCliTool(cwd: string, defaultTimeout?: number): AgentTool<typeof cliSchema>;
@@ -0,0 +1,86 @@
1
+ import { spawn } from "child_process";
2
+ import { cliSchema, MAX_OUTPUT, DEFAULT_TIMEOUT } from "./shared.js";
3
+ export function createCliTool(cwd, defaultTimeout) {
4
+ const baseTimeout = defaultTimeout ?? DEFAULT_TIMEOUT;
5
+ return {
6
+ name: "cli",
7
+ label: "cli",
8
+ description: "Execute a shell command. Returns stdout and stderr combined. Output is truncated if it exceeds ~100KB. Default timeout is 120 seconds.",
9
+ parameters: cliSchema,
10
+ execute: async (_toolCallId, { command, timeout }, signal, onUpdate) => {
11
+ const timeoutSecs = timeout ?? baseTimeout;
12
+ return new Promise((resolve, reject) => {
13
+ if (signal?.aborted) {
14
+ reject(new Error("Operation aborted"));
15
+ return;
16
+ }
17
+ const child = spawn("sh", ["-c", command], {
18
+ cwd,
19
+ stdio: ["ignore", "pipe", "pipe"],
20
+ env: { ...process.env },
21
+ });
22
+ let output = "";
23
+ let timedOut = false;
24
+ const timeoutHandle = setTimeout(() => {
25
+ timedOut = true;
26
+ child.kill("SIGTERM");
27
+ }, timeoutSecs * 1000);
28
+ const onData = (data) => {
29
+ output += data.toString("utf-8");
30
+ if (onUpdate && output.length <= MAX_OUTPUT) {
31
+ onUpdate({
32
+ content: [{ type: "text", text: output }],
33
+ details: undefined,
34
+ });
35
+ }
36
+ };
37
+ child.stdout?.on("data", onData);
38
+ child.stderr?.on("data", onData);
39
+ const onAbort = () => {
40
+ child.kill("SIGTERM");
41
+ };
42
+ if (signal) {
43
+ signal.addEventListener("abort", onAbort, { once: true });
44
+ }
45
+ child.on("error", (err) => {
46
+ clearTimeout(timeoutHandle);
47
+ if (signal)
48
+ signal.removeEventListener("abort", onAbort);
49
+ reject(err);
50
+ });
51
+ child.on("close", (code) => {
52
+ clearTimeout(timeoutHandle);
53
+ if (signal)
54
+ signal.removeEventListener("abort", onAbort);
55
+ if (signal?.aborted) {
56
+ reject(new Error("Operation aborted"));
57
+ return;
58
+ }
59
+ if (timedOut) {
60
+ reject(new Error(`Command timed out after ${timeoutSecs} seconds\n${output}`));
61
+ return;
62
+ }
63
+ // Truncate if needed
64
+ let text = output;
65
+ if (text.length > MAX_OUTPUT) {
66
+ text = text.slice(-MAX_OUTPUT);
67
+ text = `[output truncated, showing last ~100KB]\n${text}`;
68
+ }
69
+ if (!text) {
70
+ text = "(no output)";
71
+ }
72
+ if (code !== 0 && code !== null) {
73
+ text += `\n\nExit code: ${code}`;
74
+ reject(new Error(text));
75
+ }
76
+ else {
77
+ resolve({
78
+ content: [{ type: "text", text }],
79
+ details: { exitCode: code },
80
+ });
81
+ }
82
+ });
83
+ });
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import type { SandboxContext } from "../sandbox.js";
3
+ export interface BuiltinToolsConfig {
4
+ dir: string;
5
+ timeout?: number;
6
+ sandbox?: SandboxContext;
7
+ }
8
+ /**
9
+ * Create the four built-in tools (cli, read, write, memory).
10
+ * If a SandboxContext is provided, returns sandbox-backed tools;
11
+ * otherwise returns the standard local tools.
12
+ */
13
+ export declare function createBuiltinTools(config: BuiltinToolsConfig): AgentTool<any>[];
@@ -0,0 +1,29 @@
1
+ import { createCliTool } from "./cli.js";
2
+ import { createReadTool } from "./read.js";
3
+ import { createWriteTool } from "./write.js";
4
+ import { createMemoryTool } from "./memory.js";
5
+ import { createSandboxCliTool } from "./sandbox-cli.js";
6
+ import { createSandboxReadTool } from "./sandbox-read.js";
7
+ import { createSandboxWriteTool } from "./sandbox-write.js";
8
+ import { createSandboxMemoryTool } from "./sandbox-memory.js";
9
+ /**
10
+ * Create the four built-in tools (cli, read, write, memory).
11
+ * If a SandboxContext is provided, returns sandbox-backed tools;
12
+ * otherwise returns the standard local tools.
13
+ */
14
+ export function createBuiltinTools(config) {
15
+ if (config.sandbox) {
16
+ return [
17
+ createSandboxCliTool(config.sandbox, config.timeout),
18
+ createSandboxReadTool(config.sandbox),
19
+ createSandboxWriteTool(config.sandbox),
20
+ createSandboxMemoryTool(config.sandbox),
21
+ ];
22
+ }
23
+ return [
24
+ createCliTool(config.dir, config.timeout),
25
+ createReadTool(config.dir),
26
+ createWriteTool(config.dir),
27
+ createMemoryTool(config.dir),
28
+ ];
29
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { memorySchema } from "./shared.js";
3
+ export declare function createMemoryTool(cwd: string): AgentTool<typeof memorySchema>;
@@ -0,0 +1,128 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { join, dirname } from "path";
3
+ import { execSync } from "child_process";
4
+ import { memorySchema, DEFAULT_MEMORY_PATH } from "./shared.js";
5
+ import yaml from "js-yaml";
6
+ async function loadMemoryConfig(cwd) {
7
+ try {
8
+ const raw = await readFile(join(cwd, "memory", "memory.yaml"), "utf-8");
9
+ const config = yaml.load(raw);
10
+ if (!config?.layers || !Array.isArray(config.layers))
11
+ return null;
12
+ return config;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function getWorkingLayer(config) {
19
+ if (!config) {
20
+ return { path: DEFAULT_MEMORY_PATH };
21
+ }
22
+ const working = config.layers.find((l) => l.name === "working") || config.layers[0];
23
+ if (!working) {
24
+ return { path: DEFAULT_MEMORY_PATH };
25
+ }
26
+ return { path: working.path, maxLines: working.max_lines };
27
+ }
28
+ async function archiveOverflow(cwd, content, maxLines) {
29
+ const lines = content.split("\n");
30
+ if (lines.length <= maxLines)
31
+ return content;
32
+ // Keep the last maxLines, archive the rest
33
+ const overflow = lines.slice(0, lines.length - maxLines).join("\n");
34
+ const kept = lines.slice(lines.length - maxLines).join("\n");
35
+ const now = new Date();
36
+ const archiveFile = `memory/archive/${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}.md`;
37
+ const archivePath = join(cwd, archiveFile);
38
+ await mkdir(dirname(archivePath), { recursive: true });
39
+ // Append to archive
40
+ let existing = "";
41
+ try {
42
+ existing = await readFile(archivePath, "utf-8");
43
+ }
44
+ catch {
45
+ // New archive file
46
+ }
47
+ const archiveEntry = `\n---\n_Archived: ${now.toISOString()}_\n\n${overflow}\n`;
48
+ await writeFile(archivePath, existing + archiveEntry, "utf-8");
49
+ // Try to git add the archive
50
+ try {
51
+ execSync(`git add "${archiveFile}"`, { cwd, stdio: "pipe" });
52
+ }
53
+ catch {
54
+ // Not in git, that's fine
55
+ }
56
+ return kept;
57
+ }
58
+ export function createMemoryTool(cwd) {
59
+ return {
60
+ name: "memory",
61
+ label: "memory",
62
+ description: "Git-backed memory. Use 'load' to read current memory, 'save' to update memory and commit to git. Each save creates a git commit, giving you full history of what you've remembered.",
63
+ parameters: memorySchema,
64
+ execute: async (_toolCallId, { action, content, message }, signal) => {
65
+ if (signal?.aborted)
66
+ throw new Error("Operation aborted");
67
+ const config = await loadMemoryConfig(cwd);
68
+ const { path: memoryPath, maxLines } = getWorkingLayer(config);
69
+ const memoryFile = join(cwd, memoryPath);
70
+ if (action === "load") {
71
+ try {
72
+ const text = await readFile(memoryFile, "utf-8");
73
+ const trimmed = text.trim();
74
+ if (!trimmed || trimmed === "# Memory") {
75
+ return {
76
+ content: [{ type: "text", text: "No memories yet." }],
77
+ details: undefined,
78
+ };
79
+ }
80
+ return {
81
+ content: [{ type: "text", text: trimmed }],
82
+ details: undefined,
83
+ };
84
+ }
85
+ catch {
86
+ return {
87
+ content: [{ type: "text", text: "No memories yet." }],
88
+ details: undefined,
89
+ };
90
+ }
91
+ }
92
+ // action === "save"
93
+ if (!content) {
94
+ throw new Error("content is required for save action");
95
+ }
96
+ const commitMsg = message || "Update memory";
97
+ // Apply max_lines archiving if configured
98
+ let finalContent = content;
99
+ if (maxLines) {
100
+ finalContent = await archiveOverflow(cwd, content, maxLines);
101
+ }
102
+ await mkdir(dirname(memoryFile), { recursive: true });
103
+ await writeFile(memoryFile, finalContent, "utf-8");
104
+ try {
105
+ execSync(`git add "${memoryPath}" && git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, {
106
+ cwd,
107
+ stdio: "pipe",
108
+ });
109
+ }
110
+ catch (err) {
111
+ const stderr = err.stderr?.toString() || "";
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: `Memory saved to ${memoryPath} but git commit failed: ${stderr.trim() || "unknown error"}. The file was still written.`,
117
+ },
118
+ ],
119
+ details: undefined,
120
+ };
121
+ }
122
+ return {
123
+ content: [{ type: "text", text: `Memory saved and committed: "${commitMsg}"` }],
124
+ details: undefined,
125
+ };
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import { readSchema } from "./shared.js";
3
+ export declare function createReadTool(cwd: string): AgentTool<typeof readSchema>;
@@ -0,0 +1,46 @@
1
+ import { readFile } from "fs/promises";
2
+ import { resolve } from "path";
3
+ import { readSchema, MAX_LINES, paginateLines } from "./shared.js";
4
+ function resolvePath(path, cwd) {
5
+ return path.startsWith("/") ? path : resolve(cwd, path);
6
+ }
7
+ function isBinary(buffer) {
8
+ // Check first 8KB for null bytes
9
+ const check = buffer.subarray(0, 8192);
10
+ for (let i = 0; i < check.length; i++) {
11
+ if (check[i] === 0)
12
+ return true;
13
+ }
14
+ return false;
15
+ }
16
+ export function createReadTool(cwd) {
17
+ return {
18
+ name: "read",
19
+ label: "read",
20
+ description: `Read the contents of a file. Output is limited to ${MAX_LINES} lines or ~100KB. Use offset/limit for large files.`,
21
+ parameters: readSchema,
22
+ execute: async (_toolCallId, { path, offset, limit }, signal) => {
23
+ if (signal?.aborted)
24
+ throw new Error("Operation aborted");
25
+ const absolutePath = resolvePath(path, cwd);
26
+ const buffer = await readFile(absolutePath);
27
+ if (isBinary(buffer)) {
28
+ return {
29
+ content: [{ type: "text", text: `[Binary file: ${path} (${buffer.length} bytes)]` }],
30
+ details: undefined,
31
+ };
32
+ }
33
+ const text = buffer.toString("utf-8");
34
+ const page = paginateLines(text, offset, limit);
35
+ let result = page.text;
36
+ if (page.hasMore) {
37
+ const nextOffset = page.shownRange[1] + 1;
38
+ result += `\n\n[Showing lines ${page.shownRange[0]}-${page.shownRange[1]} of ${page.totalLines}. Use offset=${nextOffset} to continue.]`;
39
+ }
40
+ return {
41
+ content: [{ type: "text", text: result }],
42
+ details: undefined,
43
+ };
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import type { SandboxContext } from "../sandbox.js";
3
+ import { cliSchema } from "./shared.js";
4
+ export declare function createSandboxCliTool(ctx: SandboxContext, defaultTimeout?: number): AgentTool<typeof cliSchema>;
@@ -0,0 +1,48 @@
1
+ import { cliSchema, MAX_OUTPUT, DEFAULT_TIMEOUT, truncateOutput } from "./shared.js";
2
+ export function createSandboxCliTool(ctx, defaultTimeout) {
3
+ const baseTimeout = defaultTimeout ?? DEFAULT_TIMEOUT;
4
+ return {
5
+ name: "cli",
6
+ label: "cli",
7
+ description: "Execute a shell command in the sandbox VM. Returns stdout and stderr combined. Output is truncated if it exceeds ~100KB. Default timeout is 120 seconds.",
8
+ parameters: cliSchema,
9
+ execute: async (_toolCallId, { command, timeout }, signal, onUpdate) => {
10
+ if (signal?.aborted)
11
+ throw new Error("Operation aborted");
12
+ const timeoutSecs = timeout ?? baseTimeout;
13
+ let output = "";
14
+ const result = await ctx.gitMachine.run(command, {
15
+ cwd: ctx.repoPath,
16
+ timeout: timeoutSecs,
17
+ onStdout: (data) => {
18
+ output += data;
19
+ if (onUpdate && output.length <= MAX_OUTPUT) {
20
+ onUpdate({
21
+ content: [{ type: "text", text: output }],
22
+ details: undefined,
23
+ });
24
+ }
25
+ },
26
+ onStderr: (data) => {
27
+ output += data;
28
+ if (onUpdate && output.length <= MAX_OUTPUT) {
29
+ onUpdate({
30
+ content: [{ type: "text", text: output }],
31
+ details: undefined,
32
+ });
33
+ }
34
+ },
35
+ });
36
+ const exitCode = result?.exitCode ?? 0;
37
+ let text = truncateOutput(output) || "(no output)";
38
+ if (exitCode !== 0) {
39
+ text += `\n\nExit code: ${exitCode}`;
40
+ throw new Error(text);
41
+ }
42
+ return {
43
+ content: [{ type: "text", text }],
44
+ details: { exitCode },
45
+ };
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ import type { SandboxContext } from "../sandbox.js";
3
+ import { memorySchema } from "./shared.js";
4
+ export declare function createSandboxMemoryTool(ctx: SandboxContext): AgentTool<typeof memorySchema>;