refactor: remove bash pty mode
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
### Breaking
|
### Breaking
|
||||||
- Identifiers: rename bundle IDs and internal domains to `com.clawdis.*` (macOS: `com.clawdis.mac`, iOS: `com.clawdis.ios`, Android: `com.clawdis.android`) and update the gateway LaunchAgent label to `com.clawdis.gateway`.
|
- Identifiers: rename bundle IDs and internal domains to `com.clawdis.*` (macOS: `com.clawdis.mac`, iOS: `com.clawdis.ios`, Android: `com.clawdis.android`) and update the gateway LaunchAgent label to `com.clawdis.gateway`.
|
||||||
- Agent tools: drop the `clawdis_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
|
- Agent tools: drop the `clawdis_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
|
||||||
|
- Bash tool: remove `stdinMode: "pty"`/node-pty support; use the tmux skill for real TTYs.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
|
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
|
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
|
||||||
|
- Bash tool: default auto-background delay to 10s.
|
||||||
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
|
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
|
||||||
- Block streaming: default to text_end and suppress duplicate block sends while in-flight.
|
- Block streaming: default to text_end and suppress duplicate block sends while in-flight.
|
||||||
- Block streaming: avoid duplicate block chunks when providers repeat full content on text_end.
|
- Block streaming: avoid duplicate block chunks when providers repeat full content on text_end.
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ Clawdis runs shell commands through the `bash` tool and keeps long‑running tas
|
|||||||
|
|
||||||
Key parameters:
|
Key parameters:
|
||||||
- `command` (required)
|
- `command` (required)
|
||||||
- `yieldMs` (default 20000): auto‑background after this delay
|
- `yieldMs` (default 10000): auto‑background after this delay
|
||||||
- `background` (bool): background immediately
|
- `background` (bool): background immediately
|
||||||
- `timeout` (seconds, default 1800): kill the process after this timeout
|
- `timeout` (seconds, default 1800): kill the process after this timeout
|
||||||
- `stdinMode` (`pipe` | `pty`): use a real TTY when `pty` is requested and node-pty loads (otherwise warns + falls back)
|
- Need a real TTY? Use the tmux skill.
|
||||||
- `workdir`, `env`
|
- `workdir`, `env`
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
@@ -30,7 +30,7 @@ Environment overrides:
|
|||||||
- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h)
|
- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h)
|
||||||
|
|
||||||
Config (preferred):
|
Config (preferred):
|
||||||
- `agent.bash.backgroundMs` (default 20000)
|
- `agent.bash.backgroundMs` (default 10000)
|
||||||
- `agent.bash.timeoutSec` (default 1800)
|
- `agent.bash.timeoutSec` (default 1800)
|
||||||
- `agent.bash.cleanupMs` (default 1800000)
|
- `agent.bash.cleanupMs` (default 1800000)
|
||||||
|
|
||||||
|
|||||||
17
docs/bash.md
17
docs/bash.md
@@ -12,18 +12,10 @@ Run shell commands in the workspace. Supports foreground + background execution
|
|||||||
## Parameters
|
## Parameters
|
||||||
|
|
||||||
- `command` (required)
|
- `command` (required)
|
||||||
- `yieldMs` (default 20000): auto-background after delay
|
- `yieldMs` (default 10000): auto-background after delay
|
||||||
- `background` (bool): background immediately
|
- `background` (bool): background immediately
|
||||||
- `timeout` (seconds, default 1800): kill on expiry
|
- `timeout` (seconds, default 1800): kill on expiry
|
||||||
- `stdinMode` (`pipe` | `pty`):
|
- Need a real TTY? Use the tmux skill.
|
||||||
- `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
|
## Examples
|
||||||
|
|
||||||
@@ -37,8 +29,3 @@ Background + poll:
|
|||||||
{"tool":"bash","command":"npm run build","yieldMs":1000}
|
{"tool":"bash","command":"npm run build","yieldMs":1000}
|
||||||
{"tool":"process","action":"poll","sessionId":"<id>"}
|
{"tool":"process","action":"poll","sessionId":"<id>"}
|
||||||
```
|
```
|
||||||
|
|
||||||
TTY command:
|
|
||||||
```json
|
|
||||||
{"tool":"bash","command":"htop","stdinMode":"pty","background":true}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ Controls the embedded agent runtime (model/thinking/verbose/timeouts).
|
|||||||
},
|
},
|
||||||
maxConcurrent: 3,
|
maxConcurrent: 3,
|
||||||
bash: {
|
bash: {
|
||||||
backgroundMs: 20000,
|
backgroundMs: 10000,
|
||||||
timeoutSec: 1800,
|
timeoutSec: 1800,
|
||||||
cleanupMs: 1800000
|
cleanupMs: 1800000
|
||||||
},
|
},
|
||||||
@@ -427,7 +427,7 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
|||||||
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
|
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
|
||||||
|
|
||||||
`agent.bash` configures background bash defaults:
|
`agent.bash` configures background bash defaults:
|
||||||
- `backgroundMs`: time before auto-background (ms, default 20000)
|
- `backgroundMs`: time before auto-background (ms, default 10000)
|
||||||
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
||||||
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
|
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ Run shell commands in the workspace.
|
|||||||
|
|
||||||
Core parameters:
|
Core parameters:
|
||||||
- `command` (required)
|
- `command` (required)
|
||||||
- `yieldMs` (auto-background after timeout, default 20000)
|
- `yieldMs` (auto-background after timeout, default 10000)
|
||||||
- `background` (immediate background)
|
- `background` (immediate background)
|
||||||
- `timeout` (seconds; kills the process if exceeded, default 1800)
|
- `timeout` (seconds; kills the process if exceeded, default 1800)
|
||||||
- `stdinMode` (`pipe` | `pty`; `pty` uses node-pty for a real TTY with fallback warning)
|
- Need a real TTY? Use the tmux skill.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
||||||
|
|||||||
@@ -95,7 +95,6 @@
|
|||||||
"grammy": "^1.39.2",
|
"grammy": "^1.39.2",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"long": "5.3.2",
|
"long": "5.3.2",
|
||||||
"node-pty": "^1.1.0",
|
|
||||||
"playwright-core": "1.57.0",
|
"playwright-core": "1.57.0",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -94,9 +94,6 @@ importers:
|
|||||||
long:
|
long:
|
||||||
specifier: 5.3.2
|
specifier: 5.3.2
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
node-pty:
|
|
||||||
specifier: ^1.1.0
|
|
||||||
version: 1.1.0
|
|
||||||
playwright-core:
|
playwright-core:
|
||||||
specifier: 1.57.0
|
specifier: 1.57.0
|
||||||
version: 1.57.0
|
version: 1.57.0
|
||||||
@@ -2188,9 +2185,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
node-pty@1.1.0:
|
|
||||||
resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==}
|
|
||||||
|
|
||||||
node-wav@0.0.2:
|
node-wav@0.0.2:
|
||||||
resolution: {integrity: sha512-M6Rm/bbG6De/gKGxOpeOobx/dnGuP0dz40adqx38boqHhlWssBJZgLCPBNtb9NkrmnKYiV04xELq+R6PFOnoLA==}
|
resolution: {integrity: sha512-M6Rm/bbG6De/gKGxOpeOobx/dnGuP0dz40adqx38boqHhlWssBJZgLCPBNtb9NkrmnKYiV04xELq+R6PFOnoLA==}
|
||||||
engines: {node: '>=4.4.0'}
|
engines: {node: '>=4.4.0'}
|
||||||
@@ -4834,10 +4828,6 @@ snapshots:
|
|||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
formdata-polyfill: 4.0.10
|
formdata-polyfill: 4.0.10
|
||||||
|
|
||||||
node-pty@1.1.0:
|
|
||||||
dependencies:
|
|
||||||
node-addon-api: 7.1.1
|
|
||||||
|
|
||||||
node-wav@0.0.2:
|
node-wav@0.0.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ describe("bash process registry", () => {
|
|||||||
id: "sess",
|
id: "sess",
|
||||||
command: "echo test",
|
command: "echo test",
|
||||||
child: { pid: 123 } as ChildProcessWithoutNullStreams,
|
child: { pid: 123 } as ChildProcessWithoutNullStreams,
|
||||||
stdinMode: "pipe",
|
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
maxOutputChars: 10,
|
maxOutputChars: 10,
|
||||||
@@ -49,7 +48,6 @@ describe("bash process registry", () => {
|
|||||||
id: "sess",
|
id: "sess",
|
||||||
command: "echo test",
|
command: "echo test",
|
||||||
child: { pid: 123 } as ChildProcessWithoutNullStreams,
|
child: { pid: 123 } as ChildProcessWithoutNullStreams,
|
||||||
stdinMode: "pipe",
|
|
||||||
startedAt: Date.now(),
|
startedAt: Date.now(),
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
maxOutputChars: 100,
|
maxOutputChars: 100,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
import type { IPty } from "node-pty";
|
|
||||||
|
|
||||||
const DEFAULT_JOB_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
const DEFAULT_JOB_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
const MIN_JOB_TTL_MS = 60 * 1000; // 1 minute
|
const MIN_JOB_TTL_MS = 60 * 1000; // 1 minute
|
||||||
@@ -16,15 +15,11 @@ let jobTtlMs = clampTtl(
|
|||||||
|
|
||||||
export type ProcessStatus = "running" | "completed" | "failed" | "killed";
|
export type ProcessStatus = "running" | "completed" | "failed" | "killed";
|
||||||
|
|
||||||
export type ProcessStdinMode = "pipe" | "pty";
|
|
||||||
|
|
||||||
export interface ProcessSession {
|
export interface ProcessSession {
|
||||||
id: string;
|
id: string;
|
||||||
command: string;
|
command: string;
|
||||||
child?: ChildProcessWithoutNullStreams;
|
child?: ChildProcessWithoutNullStreams;
|
||||||
pty?: IPty;
|
|
||||||
pid?: number;
|
pid?: number;
|
||||||
stdinMode: ProcessStdinMode;
|
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
maxOutputChars: number;
|
maxOutputChars: number;
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,10 +2,8 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { existsSync, statSync } from "node:fs";
|
import { existsSync, statSync } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import path from "node:path";
|
|
||||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import type { IPty } from "node-pty";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addSession,
|
addSession,
|
||||||
@@ -18,7 +16,6 @@ import {
|
|||||||
listRunningSessions,
|
listRunningSessions,
|
||||||
markBackgrounded,
|
markBackgrounded,
|
||||||
markExited,
|
markExited,
|
||||||
type ProcessStdinMode,
|
|
||||||
setJobTtlMs,
|
setJobTtlMs,
|
||||||
} from "./bash-process-registry.js";
|
} from "./bash-process-registry.js";
|
||||||
import {
|
import {
|
||||||
@@ -34,23 +31,6 @@ const DEFAULT_MAX_OUTPUT = clampNumber(
|
|||||||
1_000,
|
1_000,
|
||||||
150_000,
|
150_000,
|
||||||
);
|
);
|
||||||
const DEFAULT_SHELL_PATH = "/bin/sh";
|
|
||||||
const DEFAULT_PATH =
|
|
||||||
"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
||||||
const DEFAULT_PTY_NAME = "xterm-256color";
|
|
||||||
|
|
||||||
type PtyModule = typeof import("node-pty");
|
|
||||||
type PtyLoadResult = { module: PtyModule | null; error?: unknown };
|
|
||||||
let ptyModulePromise: Promise<PtyLoadResult> | null = null;
|
|
||||||
|
|
||||||
async function loadPtyModule(): Promise<PtyLoadResult> {
|
|
||||||
if (!ptyModulePromise) {
|
|
||||||
ptyModulePromise = import("node-pty")
|
|
||||||
.then((mod) => ({ module: mod }))
|
|
||||||
.catch((error) => ({ module: null, error }));
|
|
||||||
}
|
|
||||||
return ptyModulePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stringEnum = (
|
const stringEnum = (
|
||||||
values: readonly string[],
|
values: readonly string[],
|
||||||
@@ -81,7 +61,7 @@ const bashSchema = Type.Object({
|
|||||||
env: Type.Optional(Type.Record(Type.String(), Type.String())),
|
env: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
yieldMs: Type.Optional(
|
yieldMs: Type.Optional(
|
||||||
Type.Number({
|
Type.Number({
|
||||||
description: "Milliseconds to wait before backgrounding (default 20000)",
|
description: "Milliseconds to wait before backgrounding (default 10000)",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
background: Type.Optional(
|
background: Type.Optional(
|
||||||
@@ -92,11 +72,6 @@ const bashSchema = Type.Object({
|
|||||||
description: "Timeout in seconds (optional, kills process on expiry)",
|
description: "Timeout in seconds (optional, kills process on expiry)",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
stdinMode: Type.Optional(
|
|
||||||
stringEnum(["pipe", "pty"] as const, {
|
|
||||||
description: "stdin mode (pipe or pty when node-pty is available)",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BashToolDetails =
|
export type BashToolDetails =
|
||||||
@@ -120,7 +95,7 @@ export function createBashTool(
|
|||||||
): AgentTool<any, BashToolDetails> {
|
): AgentTool<any, BashToolDetails> {
|
||||||
const defaultBackgroundMs = clampNumber(
|
const defaultBackgroundMs = clampNumber(
|
||||||
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
|
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
|
||||||
20_000,
|
10_000,
|
||||||
10,
|
10,
|
||||||
120_000,
|
120_000,
|
||||||
);
|
);
|
||||||
@@ -133,7 +108,7 @@ export function createBashTool(
|
|||||||
name: "bash",
|
name: "bash",
|
||||||
label: "bash",
|
label: "bash",
|
||||||
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. For real TTY mode, use the tmux skill.",
|
||||||
parameters: bashSchema,
|
parameters: bashSchema,
|
||||||
execute: async (_toolCallId, args, signal, onUpdate) => {
|
execute: async (_toolCallId, args, signal, onUpdate) => {
|
||||||
const params = args as {
|
const params = args as {
|
||||||
@@ -143,7 +118,6 @@ export function createBashTool(
|
|||||||
yieldMs?: number;
|
yieldMs?: number;
|
||||||
background?: boolean;
|
background?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
stdinMode?: "pipe" | "pty";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!params.command) {
|
if (!params.command) {
|
||||||
@@ -169,84 +143,18 @@ export function createBashTool(
|
|||||||
|
|
||||||
const { shell, args: shellArgs } = getShellConfig();
|
const { shell, args: shellArgs } = getShellConfig();
|
||||||
const env = params.env ? { ...process.env, ...params.env } : process.env;
|
const env = params.env ? { ...process.env, ...params.env } : process.env;
|
||||||
const requestedStdinMode = params.stdinMode === "pty" ? "pty" : "pipe";
|
const child = spawn(shell, [...shellArgs, params.command], {
|
||||||
let stdinMode: ProcessStdinMode = requestedStdinMode;
|
cwd: workdir,
|
||||||
let warning: string | null = null;
|
env,
|
||||||
let child: ChildProcessWithoutNullStreams | undefined;
|
detached: true,
|
||||||
let pty: IPty | undefined;
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
if (stdinMode === "pty") {
|
|
||||||
const { module: ptyModule, error: ptyError } = await loadPtyModule();
|
|
||||||
if (!ptyModule) {
|
|
||||||
warning =
|
|
||||||
`Warning: node-pty failed to load${formatPtyError(ptyError)}; ` +
|
|
||||||
"falling back to pipe mode.";
|
|
||||||
stdinMode = "pipe";
|
|
||||||
} else {
|
|
||||||
const ptyEnv = ensurePath({
|
|
||||||
...env,
|
|
||||||
TERM: env.TERM ?? DEFAULT_PTY_NAME,
|
|
||||||
} as Record<string, string>);
|
|
||||||
const ptyShell = resolveShellPath(shell, ptyEnv);
|
|
||||||
try {
|
|
||||||
pty = ptyModule.spawn(ptyShell, [...shellArgs, params.command], {
|
|
||||||
cwd: workdir,
|
|
||||||
env: ptyEnv,
|
|
||||||
name: ptyEnv.TERM || DEFAULT_PTY_NAME,
|
|
||||||
cols: 120,
|
|
||||||
rows: 30,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (
|
|
||||||
ptyShell !== DEFAULT_SHELL_PATH &&
|
|
||||||
existsSync(DEFAULT_SHELL_PATH)
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
pty = ptyModule.spawn(
|
|
||||||
DEFAULT_SHELL_PATH,
|
|
||||||
[...shellArgs, params.command],
|
|
||||||
{
|
|
||||||
cwd: workdir,
|
|
||||||
env: ptyEnv,
|
|
||||||
name: ptyEnv.TERM || DEFAULT_PTY_NAME,
|
|
||||||
cols: 120,
|
|
||||||
rows: 30,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (fallbackError) {
|
|
||||||
warning =
|
|
||||||
`Warning: node-pty failed to start${formatPtyError(fallbackError)}; ` +
|
|
||||||
"falling back to pipe mode.";
|
|
||||||
stdinMode = "pipe";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warning =
|
|
||||||
`Warning: node-pty failed to start${formatPtyError(error)}; ` +
|
|
||||||
"falling back to pipe mode.";
|
|
||||||
stdinMode = "pipe";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdinMode === "pipe") {
|
|
||||||
child = spawn(shell, [...shellArgs, params.command], {
|
|
||||||
cwd: workdir,
|
|
||||||
env,
|
|
||||||
detached: true,
|
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (warning) warnings.push(warning);
|
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
child,
|
child,
|
||||||
pty,
|
pid: child?.pid,
|
||||||
pid: child?.pid ?? pty?.pid,
|
|
||||||
stdinMode,
|
|
||||||
startedAt,
|
startedAt,
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
maxOutputChars: maxOutput,
|
maxOutputChars: maxOutput,
|
||||||
@@ -309,33 +217,21 @@ export function createBashTool(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (child) {
|
child.stdout.on("data", (data) => {
|
||||||
child.stdout.on("data", (data) => {
|
const str = sanitizeBinaryOutput(data.toString());
|
||||||
const str = sanitizeBinaryOutput(data.toString());
|
for (const chunk of chunkString(str)) {
|
||||||
for (const chunk of chunkString(str)) {
|
appendOutput(session, "stdout", chunk);
|
||||||
appendOutput(session, "stdout", chunk);
|
emitUpdate();
|
||||||
emitUpdate();
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
const str = sanitizeBinaryOutput(data.toString());
|
const str = sanitizeBinaryOutput(data.toString());
|
||||||
for (const chunk of chunkString(str)) {
|
for (const chunk of chunkString(str)) {
|
||||||
appendOutput(session, "stderr", chunk);
|
appendOutput(session, "stderr", chunk);
|
||||||
emitUpdate();
|
emitUpdate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (pty) {
|
|
||||||
pty.onData((data) => {
|
|
||||||
const str = sanitizeBinaryOutput(data);
|
|
||||||
for (const chunk of chunkString(str)) {
|
|
||||||
appendOutput(session, "stdout", chunk);
|
|
||||||
emitUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<AgentToolResult<BashToolDetails>>(
|
return new Promise<AgentToolResult<BashToolDetails>>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
@@ -434,24 +330,16 @@ export function createBashTool(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (child) {
|
child.once("exit", (code, exitSignal) => {
|
||||||
child.once("exit", (code, exitSignal) => {
|
handleExit(code, exitSignal);
|
||||||
handleExit(code, exitSignal);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
child.once("error", (err) => {
|
child.once("error", (err) => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) clearTimeout(yieldTimer);
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
markExited(session, null, null, "failed");
|
markExited(session, null, null, "failed");
|
||||||
settle(() => reject(err));
|
settle(() => reject(err));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (pty) {
|
|
||||||
pty.onExit(({ exitCode, signal }) => {
|
|
||||||
handleExit(exitCode ?? null, signal ?? null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -747,43 +635,25 @@ export function createProcessTool(
|
|||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (session.stdinMode === "pty") {
|
if (!session.child?.stdin || session.child.stdin.destroyed) {
|
||||||
if (!session.pty || session.exited) {
|
return {
|
||||||
return {
|
content: [
|
||||||
content: [
|
{
|
||||||
{
|
type: "text",
|
||||||
type: "text",
|
text: `Session ${params.sessionId} stdin is not writable.`,
|
||||||
text: `Session ${params.sessionId} stdin is not writable.`,
|
},
|
||||||
},
|
],
|
||||||
],
|
details: { status: "failed" },
|
||||||
details: { status: "failed" },
|
};
|
||||||
};
|
}
|
||||||
}
|
await new Promise<void>((resolve, reject) => {
|
||||||
session.pty.write(params.data ?? "");
|
session.child?.stdin.write(params.data ?? "", (err) => {
|
||||||
if (params.eof) {
|
if (err) reject(err);
|
||||||
session.pty.write("\x04");
|
else resolve();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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) {
|
});
|
||||||
session.child.stdin.end();
|
if (params.eof) {
|
||||||
}
|
session.child.stdin.end();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -908,21 +778,12 @@ export const processTool = createProcessTool();
|
|||||||
|
|
||||||
function killSession(session: {
|
function killSession(session: {
|
||||||
pid?: number;
|
pid?: number;
|
||||||
stdinMode: ProcessStdinMode;
|
|
||||||
pty?: IPty;
|
|
||||||
child?: ChildProcessWithoutNullStreams;
|
child?: ChildProcessWithoutNullStreams;
|
||||||
}) {
|
}) {
|
||||||
const pid = session.pid ?? session.child?.pid ?? session.pty?.pid;
|
const pid = session.pid ?? session.child?.pid;
|
||||||
if (pid) {
|
if (pid) {
|
||||||
killProcessTree(pid);
|
killProcessTree(pid);
|
||||||
}
|
}
|
||||||
if (session.stdinMode === "pty") {
|
|
||||||
try {
|
|
||||||
session.pty?.kill();
|
|
||||||
} catch {
|
|
||||||
// ignore kill failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveWorkdir(workdir: string, warnings: string[]) {
|
function resolveWorkdir(workdir: string, warnings: string[]) {
|
||||||
@@ -949,44 +810,6 @@ function safeCwd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensurePath(env: Record<string, string>) {
|
|
||||||
if (!env.PATH?.trim()) {
|
|
||||||
env.PATH = DEFAULT_PATH;
|
|
||||||
}
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveShellPath(shell: string, env: Record<string, string>) {
|
|
||||||
if (process.platform === "win32") return shell;
|
|
||||||
if (shell.includes("/") && existsSync(shell)) {
|
|
||||||
return shell;
|
|
||||||
}
|
|
||||||
const searchPath = env.PATH ?? "";
|
|
||||||
for (const segment of searchPath.split(path.delimiter)) {
|
|
||||||
if (!segment) continue;
|
|
||||||
const candidate = path.join(segment, shell);
|
|
||||||
if (existsSync(candidate)) return candidate;
|
|
||||||
}
|
|
||||||
if (existsSync(DEFAULT_SHELL_PATH)) {
|
|
||||||
return DEFAULT_SHELL_PATH;
|
|
||||||
}
|
|
||||||
return shell;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
function clampNumber(
|
||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
defaultValue: number,
|
defaultValue: number,
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
};
|
};
|
||||||
const queueHandle: EmbeddedPiQueueHandle = {
|
const queueHandle: EmbeddedPiQueueHandle = {
|
||||||
queueMessage: async (text: string) => {
|
queueMessage: async (text: string) => {
|
||||||
await session.queueMessage(text);
|
await session.steer(text);
|
||||||
},
|
},
|
||||||
isStreaming: () => session.isStreaming,
|
isStreaming: () => session.isStreaming,
|
||||||
abort: abortRun,
|
abort: abortRun,
|
||||||
|
|||||||
@@ -367,8 +367,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
|||||||
message.content?.trim() ??
|
message.content?.trim() ??
|
||||||
media?.placeholder ??
|
media?.placeholder ??
|
||||||
message.embeds[0]?.description ??
|
message.embeds[0]?.description ??
|
||||||
(forwardedSnapshot ? "<forwarded message>" : "") ??
|
(forwardedSnapshot ? "<forwarded message>" : "");
|
||||||
"";
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
logVerbose(`discord: drop message ${message.id} (empty content)`);
|
logVerbose(`discord: drop message ${message.id} (empty content)`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
|
||||||
import {
|
import {
|
||||||
connectOk,
|
connectOk,
|
||||||
installGatewayTestHooks,
|
installGatewayTestHooks,
|
||||||
@@ -64,6 +63,9 @@ describe("gateway server providers", () => {
|
|||||||
test("telegram.logout clears bot token from config", async () => {
|
test("telegram.logout clears bot token from config", async () => {
|
||||||
const prevToken = process.env.TELEGRAM_BOT_TOKEN;
|
const prevToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
delete process.env.TELEGRAM_BOT_TOKEN;
|
delete process.env.TELEGRAM_BOT_TOKEN;
|
||||||
|
const { readConfigFileSnapshot, writeConfigFile } = await import(
|
||||||
|
"../config/config.js"
|
||||||
|
);
|
||||||
await writeConfigFile({
|
await writeConfigFile({
|
||||||
telegram: {
|
telegram: {
|
||||||
botToken: "123:abc",
|
botToken: "123:abc",
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ import {
|
|||||||
authorizeGatewayConnect,
|
authorizeGatewayConnect,
|
||||||
type ResolvedGatewayAuth,
|
type ResolvedGatewayAuth,
|
||||||
} from "./auth.js";
|
} from "./auth.js";
|
||||||
|
import {
|
||||||
|
type GatewayReloadPlan,
|
||||||
|
type ProviderKind,
|
||||||
|
startGatewayConfigReloader,
|
||||||
|
} from "./config-reload.js";
|
||||||
import { normalizeControlUiBasePath } from "./control-ui.js";
|
import { normalizeControlUiBasePath } from "./control-ui.js";
|
||||||
import { resolveHooksConfig } from "./hooks.js";
|
import { resolveHooksConfig } from "./hooks.js";
|
||||||
import {
|
import {
|
||||||
@@ -94,13 +99,12 @@ import {
|
|||||||
resolveGatewayBindHost,
|
resolveGatewayBindHost,
|
||||||
} from "./net.js";
|
} from "./net.js";
|
||||||
import { createBridgeHandlers } from "./server-bridge.js";
|
import { createBridgeHandlers } from "./server-bridge.js";
|
||||||
import { createBridgeSubscriptionManager } from "./server-bridge-subscriptions.js";
|
|
||||||
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
|
|
||||||
import {
|
import {
|
||||||
startGatewayConfigReloader,
|
type BridgeListConnectedFn,
|
||||||
type GatewayReloadPlan,
|
type BridgeSendEventFn,
|
||||||
type ProviderKind,
|
createBridgeSubscriptionManager,
|
||||||
} from "./config-reload.js";
|
} from "./server-bridge-subscriptions.js";
|
||||||
|
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
|
||||||
import { createAgentEventHandler, createChatRunState } from "./server-chat.js";
|
import { createAgentEventHandler, createChatRunState } from "./server-chat.js";
|
||||||
import {
|
import {
|
||||||
DEDUPE_MAX,
|
DEDUPE_MAX,
|
||||||
@@ -862,6 +866,11 @@ export async function startGatewayServer(
|
|||||||
const bridgeSubscribe = bridgeSubscriptions.subscribe;
|
const bridgeSubscribe = bridgeSubscriptions.subscribe;
|
||||||
const bridgeUnsubscribe = bridgeSubscriptions.unsubscribe;
|
const bridgeUnsubscribe = bridgeSubscriptions.unsubscribe;
|
||||||
const bridgeUnsubscribeAll = bridgeSubscriptions.unsubscribeAll;
|
const bridgeUnsubscribeAll = bridgeSubscriptions.unsubscribeAll;
|
||||||
|
const bridgeSendEvent: BridgeSendEventFn = (opts) => {
|
||||||
|
bridge?.sendEvent(opts);
|
||||||
|
};
|
||||||
|
const bridgeListConnected: BridgeListConnectedFn = () =>
|
||||||
|
bridge?.listConnected() ?? [];
|
||||||
const bridgeSendToSession = (
|
const bridgeSendToSession = (
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
event: string,
|
event: string,
|
||||||
@@ -871,20 +880,16 @@ export async function startGatewayServer(
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
event,
|
event,
|
||||||
payload,
|
payload,
|
||||||
bridge ? (opts) => bridge.sendEvent(opts) : undefined,
|
bridgeSendEvent,
|
||||||
);
|
);
|
||||||
const bridgeSendToAllSubscribed = (event: string, payload: unknown) =>
|
const bridgeSendToAllSubscribed = (event: string, payload: unknown) =>
|
||||||
bridgeSubscriptions.sendToAllSubscribed(
|
bridgeSubscriptions.sendToAllSubscribed(event, payload, bridgeSendEvent);
|
||||||
event,
|
|
||||||
payload,
|
|
||||||
bridge ? (opts) => bridge.sendEvent(opts) : undefined,
|
|
||||||
);
|
|
||||||
const bridgeSendToAllConnected = (event: string, payload: unknown) =>
|
const bridgeSendToAllConnected = (event: string, payload: unknown) =>
|
||||||
bridgeSubscriptions.sendToAllConnected(
|
bridgeSubscriptions.sendToAllConnected(
|
||||||
event,
|
event,
|
||||||
payload,
|
payload,
|
||||||
bridge ? () => bridge.listConnected() : undefined,
|
bridgeListConnected,
|
||||||
bridge ? (opts) => bridge.sendEvent(opts) : undefined,
|
bridgeSendEvent,
|
||||||
);
|
);
|
||||||
|
|
||||||
const broadcastVoiceWakeChanged = (triggers: string[]) => {
|
const broadcastVoiceWakeChanged = (triggers: string[]) => {
|
||||||
@@ -1663,7 +1668,9 @@ export async function startGatewayServer(
|
|||||||
|
|
||||||
if (plan.restartProviders.size > 0) {
|
if (plan.restartProviders.size > 0) {
|
||||||
if (process.env.CLAWDIS_SKIP_PROVIDERS === "1") {
|
if (process.env.CLAWDIS_SKIP_PROVIDERS === "1") {
|
||||||
logProviders.info("skipping provider reload (CLAWDIS_SKIP_PROVIDERS=1)");
|
logProviders.info(
|
||||||
|
"skipping provider reload (CLAWDIS_SKIP_PROVIDERS=1)",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const restartProvider = async (
|
const restartProvider = async (
|
||||||
name: ProviderKind,
|
name: ProviderKind,
|
||||||
@@ -1712,10 +1719,7 @@ export async function startGatewayServer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCommandLaneConcurrency(
|
setCommandLaneConcurrency("cron", nextConfig.cron?.maxConcurrentRuns ?? 1);
|
||||||
"cron",
|
|
||||||
nextConfig.cron?.maxConcurrentRuns ?? 1,
|
|
||||||
);
|
|
||||||
setCommandLaneConcurrency("main", nextConfig.agent?.maxConcurrent ?? 1);
|
setCommandLaneConcurrency("main", nextConfig.agent?.maxConcurrent ?? 1);
|
||||||
|
|
||||||
if (plan.hotReasons.length > 0) {
|
if (plan.hotReasons.length > 0) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, expect, vi } from "vitest";
|
import { afterEach, beforeEach, expect, vi } from "vitest";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { agentCommand } from "../commands/agent.js";
|
|
||||||
import { resetAgentRunContextForTest } from "../infra/agent-events.js";
|
import { resetAgentRunContextForTest } from "../infra/agent-events.js";
|
||||||
import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js";
|
import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js";
|
||||||
import { rawDataToString } from "../infra/ws.js";
|
import { rawDataToString } from "../infra/ws.js";
|
||||||
@@ -64,6 +63,7 @@ const hoisted = vi.hoisted(() => ({
|
|||||||
}>,
|
}>,
|
||||||
},
|
},
|
||||||
cronIsolatedRun: vi.fn(async () => ({ status: "ok", summary: "ok" })),
|
cronIsolatedRun: vi.fn(async () => ({ status: "ok", summary: "ok" })),
|
||||||
|
agentCommand: vi.fn().mockResolvedValue(undefined),
|
||||||
testIsNixMode: { value: false },
|
testIsNixMode: { value: false },
|
||||||
sessionStoreSaveDelayMs: { value: 0 },
|
sessionStoreSaveDelayMs: { value: 0 },
|
||||||
}));
|
}));
|
||||||
@@ -75,6 +75,7 @@ export const bridgeSendEvent = hoisted.bridgeSendEvent;
|
|||||||
export const testTailnetIPv4 = hoisted.testTailnetIPv4;
|
export const testTailnetIPv4 = hoisted.testTailnetIPv4;
|
||||||
export const piSdkMock = hoisted.piSdkMock;
|
export const piSdkMock = hoisted.piSdkMock;
|
||||||
export const cronIsolatedRun = hoisted.cronIsolatedRun;
|
export const cronIsolatedRun = hoisted.cronIsolatedRun;
|
||||||
|
export const agentCommand = hoisted.agentCommand;
|
||||||
|
|
||||||
export const testState = {
|
export const testState = {
|
||||||
sessionStorePath: undefined as string | undefined,
|
sessionStorePath: undefined as string | undefined,
|
||||||
@@ -290,7 +291,7 @@ vi.mock("../web/outbound.js", () => ({
|
|||||||
.mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }),
|
.mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }),
|
||||||
}));
|
}));
|
||||||
vi.mock("../commands/agent.js", () => ({
|
vi.mock("../commands/agent.js", () => ({
|
||||||
agentCommand: vi.fn().mockResolvedValue(undefined),
|
agentCommand,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
process.env.CLAWDIS_SKIP_PROVIDERS = "1";
|
process.env.CLAWDIS_SKIP_PROVIDERS = "1";
|
||||||
@@ -509,5 +510,3 @@ export async function waitForSystemEvent(timeoutMs = 2000) {
|
|||||||
}
|
}
|
||||||
throw new Error("timeout waiting for system event");
|
throw new Error("timeout waiting for system event");
|
||||||
}
|
}
|
||||||
|
|
||||||
export { agentCommand };
|
|
||||||
|
|||||||
Reference in New Issue
Block a user