mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-08 21:09:23 +08:00
feat: add bash pty diagnostics
This commit is contained in:
2
.npmrc
2
.npmrc
@@ -1 +1 @@
|
||||
allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext
|
||||
allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext,node-pty
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
- macOS: log health refresh failures and recovery to make gateway issues easier to diagnose.
|
||||
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
|
||||
- macOS codesign: include camera entitlement so permission prompts work in the menu bar app.
|
||||
- Agent tools: bash tool supports real TTY via `stdinMode: "pty"` with node-pty, warning + fallback on load/start failure.
|
||||
- Agent tools: map `camera.snap` JPEG payloads to `image/jpeg` to avoid MIME mismatch errors.
|
||||
- Tests: cover `camera.snap` MIME mapping to prevent image/png vs image/jpeg mismatches.
|
||||
- macOS camera: wait for exposure/white balance to settle before capturing a snap to avoid dark images.
|
||||
|
||||
44
docs/bash.md
Normal file
44
docs/bash.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
summary: "Bash tool usage, stdin modes, and TTY support"
|
||||
read_when:
|
||||
- Using or modifying the bash tool
|
||||
- Debugging stdin or TTY behavior
|
||||
---
|
||||
|
||||
# Bash tool
|
||||
|
||||
Run shell commands in the workspace. Supports foreground + background execution via `process`.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `command` (required)
|
||||
- `yieldMs` (default 20000): auto-background after delay
|
||||
- `background` (bool): background immediately
|
||||
- `timeout` (seconds, default 1800): kill on expiry
|
||||
- `stdinMode` (`pipe` | `pty`):
|
||||
- `pipe` (default): classic stdin/stdout/stderr pipes
|
||||
- `pty`: real TTY via node-pty (merged stdout/stderr)
|
||||
|
||||
## TTY mode (`stdinMode: "pty"`)
|
||||
|
||||
- Uses node-pty if available. If node-pty fails to load/start, the tool warns and falls back to `pipe`.
|
||||
- Output streams are merged (no separate stderr).
|
||||
- `process write` sends raw input; `eof: true` sends Ctrl-D (`\x04`).
|
||||
|
||||
## Examples
|
||||
|
||||
Foreground:
|
||||
```json
|
||||
{"tool":"bash","command":"ls -la"}
|
||||
```
|
||||
|
||||
Background + poll:
|
||||
```json
|
||||
{"tool":"bash","command":"npm run build","yieldMs":1000}
|
||||
{"tool":"process","action":"poll","sessionId":"<id>"}
|
||||
```
|
||||
|
||||
TTY command:
|
||||
```json
|
||||
{"tool":"bash","command":"htop","stdinMode":"pty","background":true}
|
||||
```
|
||||
65
src/agents/bash-tools.pty.test.ts
Normal file
65
src/agents/bash-tools.pty.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("bash tool pty mode", () => {
|
||||
it("falls back to pipe with warning when node-pty fails to load", async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("node-pty", () => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
|
||||
const { createBashTool } = await import("./bash-tools.js");
|
||||
const tool = createBashTool({ backgroundMs: 10, timeoutSec: 1 });
|
||||
const result = await tool.execute("call", {
|
||||
command: "echo test",
|
||||
stdinMode: "pty",
|
||||
});
|
||||
|
||||
const text = result.content.find((c) => c.type === "text")?.text ?? "";
|
||||
expect(text).toContain("Warning: node-pty failed to load");
|
||||
expect(text).toContain("falling back to pipe mode.");
|
||||
|
||||
vi.doUnmock("node-pty");
|
||||
});
|
||||
|
||||
it("uses node-pty when available", async () => {
|
||||
vi.resetModules();
|
||||
const spawn = vi.fn(() => {
|
||||
let onData: ((data: string) => void) | undefined;
|
||||
let onExit:
|
||||
| ((event: { exitCode: number | null; signal?: number | null }) => void)
|
||||
| undefined;
|
||||
const pty = {
|
||||
pid: 4321,
|
||||
onData: (cb: (data: string) => void) => {
|
||||
onData = cb;
|
||||
},
|
||||
onExit: (
|
||||
cb: (event: { exitCode: number | null; signal?: number | null }) => void,
|
||||
) => {
|
||||
onExit = cb;
|
||||
},
|
||||
write: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
};
|
||||
setTimeout(() => {
|
||||
onData?.("hello\n");
|
||||
onExit?.({ exitCode: 0, signal: null });
|
||||
}, 10);
|
||||
return pty;
|
||||
});
|
||||
vi.doMock("node-pty", () => ({ spawn }));
|
||||
|
||||
const { createBashTool } = await import("./bash-tools.js");
|
||||
const tool = createBashTool({ backgroundMs: 10, timeoutSec: 1 });
|
||||
const result = await tool.execute("call", {
|
||||
command: "ignored",
|
||||
stdinMode: "pty",
|
||||
});
|
||||
|
||||
const text = result.content.find((c) => c.type === "text")?.text ?? "";
|
||||
expect(text).toContain("hello");
|
||||
expect(text).not.toContain("Warning:");
|
||||
|
||||
vi.doUnmock("node-pty");
|
||||
});
|
||||
});
|
||||
@@ -34,13 +34,14 @@ const DEFAULT_MAX_OUTPUT = clampNumber(
|
||||
const DEFAULT_PTY_NAME = "xterm-256color";
|
||||
|
||||
type PtyModule = typeof import("node-pty");
|
||||
let ptyModulePromise: Promise<PtyModule | null> | null = null;
|
||||
type PtyLoadResult = { module: PtyModule | null; error?: unknown };
|
||||
let ptyModulePromise: Promise<PtyLoadResult> | null = null;
|
||||
|
||||
async function loadPtyModule(): Promise<PtyModule | null> {
|
||||
async function loadPtyModule(): Promise<PtyLoadResult> {
|
||||
if (!ptyModulePromise) {
|
||||
ptyModulePromise = import("node-pty")
|
||||
.then((mod) => mod)
|
||||
.catch(() => null);
|
||||
.then((mod) => ({ module: mod }))
|
||||
.catch((error) => ({ module: null, error }));
|
||||
}
|
||||
return ptyModulePromise;
|
||||
}
|
||||
@@ -166,10 +167,11 @@ export function createBashTool(
|
||||
let pty: IPty | undefined;
|
||||
|
||||
if (stdinMode === "pty") {
|
||||
const ptyModule = await loadPtyModule();
|
||||
const { module: ptyModule, error: ptyError } = await loadPtyModule();
|
||||
if (!ptyModule) {
|
||||
warning =
|
||||
"Warning: node-pty failed to load; falling back to pipe mode.";
|
||||
`Warning: node-pty failed to load${formatPtyError(ptyError)}; ` +
|
||||
"falling back to pipe mode.";
|
||||
stdinMode = "pipe";
|
||||
} else {
|
||||
const ptyEnv = {
|
||||
@@ -184,9 +186,10 @@ export function createBashTool(
|
||||
cols: 120,
|
||||
rows: 30,
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
warning =
|
||||
"Warning: node-pty failed to start; falling back to pipe mode.";
|
||||
`Warning: node-pty failed to start${formatPtyError(error)}; ` +
|
||||
"falling back to pipe mode.";
|
||||
stdinMode = "pipe";
|
||||
}
|
||||
}
|
||||
@@ -886,6 +889,20 @@ function killSession(session: {
|
||||
}
|
||||
}
|
||||
|
||||
function formatPtyError(error: unknown) {
|
||||
if (!error) return "";
|
||||
if (typeof error === "string") return ` (${error})`;
|
||||
if (error instanceof Error) {
|
||||
const firstLine = error.message.split(/\r?\n/)[0]?.trim();
|
||||
return firstLine ? ` (${firstLine})` : "";
|
||||
}
|
||||
try {
|
||||
return ` (${JSON.stringify(error)})`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function clampNumber(
|
||||
value: number | undefined,
|
||||
defaultValue: number,
|
||||
|
||||
Reference in New Issue
Block a user