feat: add process submit helper

This commit is contained in:
Peter Steinberger
2026-01-17 06:38:47 +00:00
parent 65a8a93854
commit 3dc4a96330
4 changed files with 108 additions and 2 deletions

View File

@@ -22,6 +22,7 @@
- Tools: Firecrawl fallback now uses bot-circumvention + cache by default; remove basic HTML fallback when extraction fails.
- Tools: default `exec` exit notifications and auto-migrate legacy `tools.bash` to `tools.exec`.
- Tools: add tmux-style `process send-keys` and bracketed paste helpers for PTY sessions.
- Tools: add `process submit` helper to send CR for PTY sessions.
- Status: trim `/status` to current-provider usage only and drop the OAuth/token block.
- Directory: unify `clawdbot directory` across channels and plugin channels.
- UI: allow deleting sessions from the Control UI.

View File

@@ -46,6 +46,11 @@ Send keys (tmux-style):
{"tool":"process","action":"send-keys","sessionId":"<id>","keys":["Up","Up","Enter"]}
```
Submit (send CR only):
```json
{"tool":"process","action":"submit","sessionId":"<id>"}
```
Paste (bracketed by default):
```json
{"tool":"process","action":"paste","sessionId":"<id>","text":"line1\nline2\n"}

View File

@@ -44,3 +44,36 @@ test("process send-keys encodes Enter for pty sessions", async () => {
throw new Error("PTY session did not exit after send-keys");
});
test("process submit sends CR for pty sessions", async () => {
const execTool = createExecTool();
const processTool = createProcessTool();
const result = await execTool.execute("toolcall", {
command:
"node -e \"process.stdin.on('data', d => { if (d.includes(13)) { process.stdout.write('submitted'); process.exit(0); } });\"",
pty: true,
background: true,
});
expect(result.details.status).toBe("running");
const sessionId = result.details.sessionId;
expect(sessionId).toBeTruthy();
await processTool.execute("toolcall", {
action: "submit",
sessionId,
});
for (let i = 0; i < 10; i += 1) {
await wait(50);
const poll = await processTool.execute("toolcall", { action: "poll", sessionId });
const details = poll.details as { status?: string; aggregated?: string };
if (details.status !== "running") {
expect(details.status).toBe("completed");
expect(details.aggregated ?? "").toContain("submitted");
return;
}
}
throw new Error("PTY session did not exit after submit");
});

View File

@@ -58,11 +58,22 @@ export function createProcessTool(
return {
name: "process",
label: "process",
description: "Manage running exec sessions: list, poll, log, write, send-keys, paste, kill.",
description:
"Manage running exec sessions: list, poll, log, write, send-keys, submit, paste, kill.",
parameters: processSchema,
execute: async (_toolCallId, args) => {
const params = args as {
action: "list" | "poll" | "log" | "write" | "send-keys" | "paste" | "kill" | "clear" | "remove";
action:
| "list"
| "poll"
| "log"
| "write"
| "send-keys"
| "submit"
| "paste"
| "kill"
| "clear"
| "remove";
sessionId?: string;
data?: string;
keys?: string[];
@@ -429,6 +440,62 @@ export function createProcessTool(
};
}
case "submit": {
if (!scopedSession) {
return {
content: [
{
type: "text",
text: `No active session found for ${params.sessionId}`,
},
],
details: { status: "failed" },
};
}
if (!scopedSession.backgrounded) {
return {
content: [
{
type: "text",
text: `Session ${params.sessionId} is not backgrounded.`,
},
],
details: { status: "failed" },
};
}
const stdin = scopedSession.stdin ?? scopedSession.child?.stdin;
if (!stdin || stdin.destroyed) {
return {
content: [
{
type: "text",
text: `Session ${params.sessionId} stdin is not writable.`,
},
],
details: { status: "failed" },
};
}
await new Promise<void>((resolve, reject) => {
stdin.write("\r", (err) => {
if (err) reject(err);
else resolve();
});
});
return {
content: [
{
type: "text",
text: `Submitted session ${params.sessionId} (sent CR).`,
},
],
details: {
status: "running",
sessionId: params.sessionId,
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,
},
};
}
case "paste": {
if (!scopedSession) {
return {