feat: add minimal tool set (read/write/patch/shell) to workflow-agent-react (#222)
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
export { createReactAdapter } from "./create-react-adapter.js";
|
||||
export type { ReactAdapterConfig, ReactToolHandler } from "./types.js";
|
||||
export { defaultTools, defaultToolHandler } from "./tools/index.js";
|
||||
export type { ToolEntry, ToolHandler } from "./tools/index.js";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ToolDefinition } from "@uncaged/workflow-reactor";
|
||||
import type { ToolEntry } from "./types.js";
|
||||
import { readFileTool } from "./read-file.js";
|
||||
import { writeFileTool } from "./write-file.js";
|
||||
import { patchFileTool } from "./patch-file.js";
|
||||
import { shellExecTool } from "./shell-exec.js";
|
||||
|
||||
const ALL_TOOLS: ToolEntry[] = [readFileTool, writeFileTool, patchFileTool, shellExecTool];
|
||||
|
||||
export const defaultTools: readonly ToolDefinition[] = ALL_TOOLS.map((t) => t.definition);
|
||||
|
||||
export async function defaultToolHandler(name: string, args: string): Promise<string> {
|
||||
const entry = ALL_TOOLS.find((t) => t.definition.function.name === name);
|
||||
if (!entry) return `Unknown tool: ${name}`;
|
||||
return entry.handler(args);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { readFileTool } from "./read-file.js";
|
||||
export { writeFileTool } from "./write-file.js";
|
||||
export { patchFileTool } from "./patch-file.js";
|
||||
export { shellExecTool } from "./shell-exec.js";
|
||||
export { defaultTools, defaultToolHandler } from "./defaults.js";
|
||||
export type { ToolEntry, ToolHandler } from "./types.js";
|
||||
@@ -0,0 +1,40 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import type { ToolEntry } from "./types.js";
|
||||
|
||||
export const patchFileTool: ToolEntry = {
|
||||
definition: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "patch_file",
|
||||
description: "Find and replace a string in a file (first occurrence only).",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to the file" },
|
||||
old_string: { type: "string", description: "Text to find" },
|
||||
new_string: { type: "string", description: "Replacement text" },
|
||||
},
|
||||
required: ["path", "old_string", "new_string"],
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: string): Promise<string> => {
|
||||
try {
|
||||
const parsed = JSON.parse(args) as { path: string; old_string: string; new_string: string };
|
||||
const content = await readFile(parsed.path, "utf-8");
|
||||
const firstIdx = content.indexOf(parsed.old_string);
|
||||
if (firstIdx === -1) {
|
||||
return `Error: old_string not found in ${parsed.path}`;
|
||||
}
|
||||
const secondIdx = content.indexOf(parsed.old_string, firstIdx + 1);
|
||||
if (secondIdx !== -1) {
|
||||
return `Error: old_string is not unique in ${parsed.path} (found multiple occurrences)`;
|
||||
}
|
||||
const updated = content.slice(0, firstIdx) + parsed.new_string + content.slice(firstIdx + parsed.old_string.length);
|
||||
await writeFile(parsed.path, updated);
|
||||
return `Successfully patched ${parsed.path}`;
|
||||
} catch (err) {
|
||||
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type { ToolEntry } from "./types.js";
|
||||
|
||||
export const readFileTool: ToolEntry = {
|
||||
definition: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "read_file",
|
||||
description: "Read a text file and return lines with line numbers.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to the file to read" },
|
||||
offset: { type: ["number", "null"], description: "Start line number (1-indexed, default: 1)" },
|
||||
limit: { type: ["number", "null"], description: "Max lines to read (default: all)" },
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: string): Promise<string> => {
|
||||
try {
|
||||
const parsed = JSON.parse(args) as { path: string; offset: number | null; limit: number | null };
|
||||
const content = await readFile(parsed.path, "utf-8");
|
||||
const allLines = content.split("\n");
|
||||
const offset = parsed.offset ?? 1;
|
||||
const start = Math.max(0, offset - 1);
|
||||
const end = parsed.limit != null ? Math.min(allLines.length, start + parsed.limit) : allLines.length;
|
||||
const lines = allLines.slice(start, end);
|
||||
return lines.map((line, i) => `${start + i + 1}|${line}`).join("\n");
|
||||
} catch (err) {
|
||||
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import type { ToolEntry } from "./types.js";
|
||||
|
||||
const MAX_OUTPUT = 10000;
|
||||
|
||||
export const shellExecTool: ToolEntry = {
|
||||
definition: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "shell_exec",
|
||||
description: "Execute a shell command and return stdout + stderr.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
command: { type: "string", description: "Shell command to run" },
|
||||
timeout: { type: ["number", "null"], description: "Timeout in seconds (default: 30)" },
|
||||
},
|
||||
required: ["command"],
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: string): Promise<string> => {
|
||||
try {
|
||||
const parsed = JSON.parse(args) as { command: string; timeout: number | null };
|
||||
const timeoutMs = (parsed.timeout ?? 30) * 1000;
|
||||
const output = execSync(parsed.command, {
|
||||
encoding: "utf-8",
|
||||
timeout: timeoutMs,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
maxBuffer: MAX_OUTPUT * 2,
|
||||
});
|
||||
return output.length > MAX_OUTPUT ? `${output.slice(0, MAX_OUTPUT)}\n...(truncated)` : output;
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === "object" && "status" in err && (err as { status: unknown }).status === null) {
|
||||
return "Error: command timed out";
|
||||
}
|
||||
if (err && typeof err === "object" && "stderr" in err) {
|
||||
const e = err as { stderr: string; stdout: string; status: number };
|
||||
const combined = `${e.stdout ?? ""}${e.stderr ?? ""}`;
|
||||
return combined.length > MAX_OUTPUT ? `${combined.slice(0, MAX_OUTPUT)}\n...(truncated)` : combined || `Error: command exited with status ${e.status}`;
|
||||
}
|
||||
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ToolDefinition } from "@uncaged/workflow-reactor";
|
||||
|
||||
export type ToolHandler = (args: string) => Promise<string>;
|
||||
|
||||
export type ToolEntry = {
|
||||
definition: ToolDefinition;
|
||||
handler: ToolHandler;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
import type { ToolEntry } from "./types.js";
|
||||
|
||||
export const writeFileTool: ToolEntry = {
|
||||
definition: {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "write_file",
|
||||
description: "Write content to a file, creating parent directories as needed.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Path to write" },
|
||||
content: { type: "string", description: "File content" },
|
||||
},
|
||||
required: ["path", "content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: string): Promise<string> => {
|
||||
try {
|
||||
const parsed = JSON.parse(args) as { path: string; content: string };
|
||||
await mkdir(dirname(parsed.path), { recursive: true });
|
||||
const buf = Buffer.from(parsed.content, "utf-8");
|
||||
await writeFile(parsed.path, buf);
|
||||
return `Successfully wrote ${buf.length} bytes to ${parsed.path}`;
|
||||
} catch (err) {
|
||||
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user