style: fix bash tools lint
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||||
import {
|
import {
|
||||||
bashTool,
|
bashTool,
|
||||||
createBashTool,
|
createBashTool,
|
||||||
createProcessTool,
|
createProcessTool,
|
||||||
processTool,
|
processTool,
|
||||||
} from "./bash-tools.js";
|
} from "./bash-tools.js";
|
||||||
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ describe("bash tool backgrounding", () => {
|
|||||||
const customProcess = createProcessTool();
|
const customProcess = createProcessTool();
|
||||||
|
|
||||||
const result = await customBash.execute("call1", {
|
const result = await customBash.execute("call1", {
|
||||||
command: "node -e \"setInterval(() => {}, 1000)\"",
|
command: 'node -e "setInterval(() => {}, 1000)"',
|
||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai";
|
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-ai";
|
||||||
import { StringEnum } from "@mariozechner/pi-ai";
|
import { StringEnum } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addSession,
|
addSession,
|
||||||
@@ -17,7 +17,11 @@ import {
|
|||||||
markExited,
|
markExited,
|
||||||
setJobTtlMs,
|
setJobTtlMs,
|
||||||
} from "./bash-process-registry.js";
|
} from "./bash-process-registry.js";
|
||||||
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "./shell-utils.js";
|
import {
|
||||||
|
getShellConfig,
|
||||||
|
killProcessTree,
|
||||||
|
sanitizeBinaryOutput,
|
||||||
|
} from "./shell-utils.js";
|
||||||
|
|
||||||
const CHUNK_LIMIT = 8 * 1024;
|
const CHUNK_LIMIT = 8 * 1024;
|
||||||
const DEFAULT_MAX_OUTPUT = clampNumber(
|
const DEFAULT_MAX_OUTPUT = clampNumber(
|
||||||
@@ -97,7 +101,7 @@ export function createBashTool(
|
|||||||
description:
|
description:
|
||||||
"Execute bash with background continuation. Use yieldMs/background to continue later via process tool.",
|
"Execute bash with background continuation. Use yieldMs/background to continue later via process tool.",
|
||||||
parameters: bashSchema,
|
parameters: bashSchema,
|
||||||
execute: async (toolCallId, args, signal, onUpdate) => {
|
execute: async (_toolCallId, args, signal, onUpdate) => {
|
||||||
const params = args as {
|
const params = args as {
|
||||||
command: string;
|
command: string;
|
||||||
workdir?: string;
|
workdir?: string;
|
||||||
@@ -179,7 +183,9 @@ export function createBashTool(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (signal?.aborted) onAbort();
|
if (signal?.aborted) onAbort();
|
||||||
else if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
else if (signal) {
|
||||||
|
signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
const effectiveTimeout =
|
const effectiveTimeout =
|
||||||
typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
||||||
@@ -287,7 +293,9 @@ export function createBashTool(
|
|||||||
: code === null
|
: code === null
|
||||||
? "Command aborted before exit code was captured"
|
? "Command aborted before exit code was captured"
|
||||||
: `Command exited with code ${code}`;
|
: `Command exited with code ${code}`;
|
||||||
const message = aggregated ? `${aggregated}\n\n${reason}` : reason;
|
const message = aggregated
|
||||||
|
? `${aggregated}\n\n${reason}`
|
||||||
|
: reason;
|
||||||
settle(() => reject(new Error(message)));
|
settle(() => reject(new Error(message)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -515,8 +523,85 @@ export function createProcessTool(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case "log": {
|
case "log": {
|
||||||
if (session) {
|
if (session) {
|
||||||
|
if (!session.backgrounded) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Session ${params.sessionId} is not backgrounded.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: { status: "failed" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { slice, totalLines, totalChars } = sliceLogLines(
|
||||||
|
session.aggregated,
|
||||||
|
params.offset,
|
||||||
|
params.limit,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: slice || "(no output yet)" }],
|
||||||
|
details: {
|
||||||
|
status: session.exited ? "completed" : "running",
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
total: totalLines,
|
||||||
|
totalLines,
|
||||||
|
totalChars,
|
||||||
|
truncated: session.truncated,
|
||||||
|
name: deriveSessionName(session.command),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (finished) {
|
||||||
|
const { slice, totalLines, totalChars } = sliceLogLines(
|
||||||
|
finished.aggregated,
|
||||||
|
params.offset,
|
||||||
|
params.limit,
|
||||||
|
);
|
||||||
|
const status =
|
||||||
|
finished.status === "completed" ? "completed" : "failed";
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: slice || "(no output recorded)" },
|
||||||
|
],
|
||||||
|
details: {
|
||||||
|
status,
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
total: totalLines,
|
||||||
|
totalLines,
|
||||||
|
totalChars,
|
||||||
|
truncated: finished.truncated,
|
||||||
|
exitCode: finished.exitCode ?? undefined,
|
||||||
|
exitSignal: finished.exitSignal ?? undefined,
|
||||||
|
name: deriveSessionName(finished.command),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `No session found for ${params.sessionId}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: { status: "failed" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "write": {
|
||||||
|
if (!session) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `No active session found for ${params.sessionId}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: { status: "failed" },
|
||||||
|
};
|
||||||
|
}
|
||||||
if (!session.backgrounded) {
|
if (!session.backgrounded) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -528,147 +613,80 @@ export function createProcessTool(
|
|||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { slice, totalLines, totalChars } = sliceLogLines(
|
if (!session.child.stdin || session.child.stdin.destroyed) {
|
||||||
session.aggregated,
|
return {
|
||||||
params.offset,
|
content: [
|
||||||
params.limit,
|
{
|
||||||
);
|
type: "text",
|
||||||
return {
|
text: `Session ${params.sessionId} stdin is not writable.`,
|
||||||
content: [{ type: "text", text: slice || "(no output yet)" }],
|
},
|
||||||
details: {
|
],
|
||||||
status: session.exited ? "completed" : "running",
|
details: { status: "failed" },
|
||||||
sessionId: params.sessionId,
|
};
|
||||||
total: totalLines,
|
}
|
||||||
totalLines,
|
await new Promise<void>((resolve, reject) => {
|
||||||
totalChars,
|
session.child.stdin.write(params.data ?? "", (err) => {
|
||||||
truncated: session.truncated,
|
if (err) reject(err);
|
||||||
name: deriveSessionName(session.command),
|
else resolve();
|
||||||
},
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
if (finished) {
|
|
||||||
const { slice, totalLines, totalChars } = sliceLogLines(
|
|
||||||
finished.aggregated,
|
|
||||||
params.offset,
|
|
||||||
params.limit,
|
|
||||||
);
|
|
||||||
const status = finished.status === "completed" ? "completed" : "failed";
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: slice || "(no output recorded)" },
|
|
||||||
],
|
|
||||||
details: {
|
|
||||||
status,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
total: totalLines,
|
|
||||||
totalLines,
|
|
||||||
totalChars,
|
|
||||||
truncated: finished.truncated,
|
|
||||||
exitCode: finished.exitCode ?? undefined,
|
|
||||||
exitSignal: finished.exitSignal ?? undefined,
|
|
||||||
name: deriveSessionName(finished.command),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: `No session found for ${params.sessionId}` },
|
|
||||||
],
|
|
||||||
details: { status: "failed" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "write": {
|
|
||||||
if (!session) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: `No active session found for ${params.sessionId}` },
|
|
||||||
],
|
|
||||||
details: { status: "failed" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!session.backgrounded) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Session ${params.sessionId} is not backgrounded.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: { status: "failed" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!session.child.stdin || session.child.stdin.destroyed) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Session ${params.sessionId} stdin is not writable.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: { status: "failed" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
session.child.stdin.write(params.data ?? "", (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve();
|
|
||||||
});
|
});
|
||||||
});
|
if (params.eof) {
|
||||||
if (params.eof) {
|
session.child.stdin.end();
|
||||||
session.child.stdin.end();
|
}
|
||||||
}
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Wrote ${(params.data ?? "").length} bytes to session ${
|
|
||||||
params.sessionId
|
|
||||||
}${params.eof ? " (stdin closed)" : ""}.`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: {
|
|
||||||
status: "running",
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
name: session ? deriveSessionName(session.command) : undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "kill": {
|
|
||||||
if (!session) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: `No active session found for ${params.sessionId}` },
|
|
||||||
],
|
|
||||||
details: { status: "failed" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!session.backgrounded) {
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Session ${params.sessionId} is not backgrounded.`,
|
text: `Wrote ${(params.data ?? "").length} bytes to session ${
|
||||||
|
params.sessionId
|
||||||
|
}${params.eof ? " (stdin closed)" : ""}.`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
details: { status: "failed" },
|
details: {
|
||||||
|
status: "running",
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
name: session ? deriveSessionName(session.command) : undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (session.child.pid) {
|
|
||||||
killProcessTree(session.child.pid);
|
case "kill": {
|
||||||
|
if (!session) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `No active session found for ${params.sessionId}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: { status: "failed" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!session.backgrounded) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Session ${params.sessionId} is not backgrounded.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: { status: "failed" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (session.child.pid) {
|
||||||
|
killProcessTree(session.child.pid);
|
||||||
|
}
|
||||||
|
markExited(session, null, "SIGKILL", "failed");
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: `Killed session ${params.sessionId}.` },
|
||||||
|
],
|
||||||
|
details: {
|
||||||
|
status: "failed",
|
||||||
|
name: session ? deriveSessionName(session.command) : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
markExited(session, null, "SIGKILL", "failed");
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: `Killed session ${params.sessionId}.` },
|
|
||||||
],
|
|
||||||
details: {
|
|
||||||
status: "failed",
|
|
||||||
name: session ? deriveSessionName(session.command) : undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "clear": {
|
case "clear": {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
@@ -718,7 +736,10 @@ export function createProcessTool(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{ type: "text", text: `No session found for ${params.sessionId}` },
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `No session found for ${params.sessionId}`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
@@ -816,7 +837,7 @@ function tokenizeCommand(command: string): string[] {
|
|||||||
function stripQuotes(value: string): string {
|
function stripQuotes(value: string): string {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (
|
if (
|
||||||
(trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||||
) {
|
) {
|
||||||
return trimmed.slice(1, -1);
|
return trimmed.slice(1, -1);
|
||||||
|
|||||||
Reference in New Issue
Block a user