feat: add process submit helper
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
- Tools: Firecrawl fallback now uses bot-circumvention + cache by default; remove basic HTML fallback when extraction fails.
|
- 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: 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 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.
|
- Status: trim `/status` to current-provider usage only and drop the OAuth/token block.
|
||||||
- Directory: unify `clawdbot directory` across channels and plugin channels.
|
- Directory: unify `clawdbot directory` across channels and plugin channels.
|
||||||
- UI: allow deleting sessions from the Control UI.
|
- UI: allow deleting sessions from the Control UI.
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ Send keys (tmux-style):
|
|||||||
{"tool":"process","action":"send-keys","sessionId":"<id>","keys":["Up","Up","Enter"]}
|
{"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):
|
Paste (bracketed by default):
|
||||||
```json
|
```json
|
||||||
{"tool":"process","action":"paste","sessionId":"<id>","text":"line1\nline2\n"}
|
{"tool":"process","action":"paste","sessionId":"<id>","text":"line1\nline2\n"}
|
||||||
|
|||||||
@@ -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");
|
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");
|
||||||
|
});
|
||||||
|
|||||||
@@ -58,11 +58,22 @@ export function createProcessTool(
|
|||||||
return {
|
return {
|
||||||
name: "process",
|
name: "process",
|
||||||
label: "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,
|
parameters: processSchema,
|
||||||
execute: async (_toolCallId, args) => {
|
execute: async (_toolCallId, args) => {
|
||||||
const params = args as {
|
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;
|
sessionId?: string;
|
||||||
data?: string;
|
data?: string;
|
||||||
keys?: 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": {
|
case "paste": {
|
||||||
if (!scopedSession) {
|
if (!scopedSession) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user