templates: add qmd semantic memory recall to AGENTS.md
This commit is contained in:
@@ -16,6 +16,7 @@ Key parameters:
|
|||||||
- `yieldMs` (default 20000): auto‑background after this delay
|
- `yieldMs` (default 20000): 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)
|
||||||
- `workdir`, `env`
|
- `workdir`, `env`
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
|
|||||||
10
docs/templates/AGENTS.md
vendored
10
docs/templates/AGENTS.md
vendored
@@ -28,6 +28,16 @@ You wake up fresh each session. These files are your continuity:
|
|||||||
|
|
||||||
Capture what matters. Decisions, context, things to remember. Skip the secrets unless asked to keep them.
|
Capture what matters. Decisions, context, things to remember. Skip the secrets unless asked to keep them.
|
||||||
|
|
||||||
|
### 🧠 Memory Recall - Use qmd!
|
||||||
|
When you need to remember something from the past, use `qmd` instead of grepping files:
|
||||||
|
```bash
|
||||||
|
qmd query "what happened at Christmas" # Semantic search with reranking
|
||||||
|
qmd search "specific phrase" # BM25 keyword search
|
||||||
|
qmd vsearch "conceptual question" # Pure vector similarity
|
||||||
|
```
|
||||||
|
Index your memory folder: `qmd index memory/`
|
||||||
|
Vectors + BM25 + reranking finds things even with different wording.
|
||||||
|
|
||||||
## Safety
|
## Safety
|
||||||
|
|
||||||
- Don't exfiltrate private data. Ever.
|
- Don't exfiltrate private data. Ever.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Core parameters:
|
|||||||
- `yieldMs` (auto-background after timeout, default 20000)
|
- `yieldMs` (auto-background after timeout, default 20000)
|
||||||
- `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)
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -88,6 +88,9 @@ 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
|
||||||
@@ -2158,6 +2161,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
@@ -2176,6 +2182,9 @@ 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'}
|
||||||
@@ -4804,6 +4813,8 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
@@ -4816,6 +4827,10 @@ 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,6 +20,7 @@ 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,
|
||||||
@@ -48,6 +49,7 @@ 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,4 +1,5 @@
|
|||||||
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
|
||||||
@@ -15,10 +16,15 @@ 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;
|
||||||
|
stdinMode: ProcessStdinMode;
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
maxOutputChars: number;
|
maxOutputChars: number;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
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,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
listRunningSessions,
|
listRunningSessions,
|
||||||
markBackgrounded,
|
markBackgrounded,
|
||||||
markExited,
|
markExited,
|
||||||
|
type ProcessStdinMode,
|
||||||
setJobTtlMs,
|
setJobTtlMs,
|
||||||
} from "./bash-process-registry.js";
|
} from "./bash-process-registry.js";
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +31,19 @@ const DEFAULT_MAX_OUTPUT = clampNumber(
|
|||||||
1_000,
|
1_000,
|
||||||
150_000,
|
150_000,
|
||||||
);
|
);
|
||||||
|
const DEFAULT_PTY_NAME = "xterm-256color";
|
||||||
|
|
||||||
|
type PtyModule = typeof import("node-pty");
|
||||||
|
let ptyModulePromise: Promise<PtyModule | null> | null = null;
|
||||||
|
|
||||||
|
async function loadPtyModule(): Promise<PtyModule | null> {
|
||||||
|
if (!ptyModulePromise) {
|
||||||
|
ptyModulePromise = import("node-pty")
|
||||||
|
.then((mod) => mod)
|
||||||
|
.catch(() => null);
|
||||||
|
}
|
||||||
|
return ptyModulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const stringEnum = (
|
const stringEnum = (
|
||||||
values: readonly string[],
|
values: readonly string[],
|
||||||
@@ -72,7 +87,7 @@ const bashSchema = Type.Object({
|
|||||||
),
|
),
|
||||||
stdinMode: Type.Optional(
|
stdinMode: Type.Optional(
|
||||||
stringEnum(["pipe", "pty"] as const, {
|
stringEnum(["pipe", "pty"] as const, {
|
||||||
description: "Only pipe is supported",
|
description: "stdin mode (pipe or pty when node-pty is available)",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -127,9 +142,6 @@ export function createBashTool(
|
|||||||
if (!params.command) {
|
if (!params.command) {
|
||||||
throw new Error("Provide a command to start.");
|
throw new Error("Provide a command to start.");
|
||||||
}
|
}
|
||||||
if (params.stdinMode && params.stdinMode !== "pipe") {
|
|
||||||
throw new Error('Only stdinMode "pipe" is supported right now.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const yieldWindow = params.background
|
const yieldWindow = params.background
|
||||||
? 0
|
? 0
|
||||||
@@ -146,21 +158,56 @@ 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 child: ChildProcessWithoutNullStreams = spawn(
|
const requestedStdinMode =
|
||||||
shell,
|
params.stdinMode === "pty" ? "pty" : "pipe";
|
||||||
[...shellArgs, params.command],
|
let stdinMode: ProcessStdinMode = requestedStdinMode;
|
||||||
{
|
let warning: string | null = null;
|
||||||
|
let child: ChildProcessWithoutNullStreams | undefined;
|
||||||
|
let pty: IPty | undefined;
|
||||||
|
|
||||||
|
if (stdinMode === "pty") {
|
||||||
|
const ptyModule = await loadPtyModule();
|
||||||
|
if (!ptyModule) {
|
||||||
|
warning =
|
||||||
|
"Warning: node-pty failed to load; falling back to pipe mode.";
|
||||||
|
stdinMode = "pipe";
|
||||||
|
} else {
|
||||||
|
const ptyEnv = {
|
||||||
|
...env,
|
||||||
|
TERM: env.TERM ?? DEFAULT_PTY_NAME,
|
||||||
|
} as Record<string, string>;
|
||||||
|
try {
|
||||||
|
pty = ptyModule.spawn(shell, [...shellArgs, params.command], {
|
||||||
|
cwd: workdir,
|
||||||
|
env: ptyEnv,
|
||||||
|
name: ptyEnv.TERM || DEFAULT_PTY_NAME,
|
||||||
|
cols: 120,
|
||||||
|
rows: 30,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
warning =
|
||||||
|
"Warning: node-pty failed to start; falling back to pipe mode.";
|
||||||
|
stdinMode = "pipe";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdinMode === "pipe") {
|
||||||
|
child = spawn(shell, [...shellArgs, params.command], {
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
env,
|
env,
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
child,
|
child,
|
||||||
|
pty,
|
||||||
|
pid: child?.pid ?? pty?.pid,
|
||||||
|
stdinMode,
|
||||||
startedAt,
|
startedAt,
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
maxOutputChars: maxOutput,
|
maxOutputChars: maxOutput,
|
||||||
@@ -190,9 +237,7 @@ export function createBashTool(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
if (child.pid) {
|
killSession(session);
|
||||||
killProcessTree(child.pid);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (signal?.aborted) onAbort();
|
if (signal?.aborted) onAbort();
|
||||||
@@ -212,33 +257,46 @@ export function createBashTool(
|
|||||||
const emitUpdate = () => {
|
const emitUpdate = () => {
|
||||||
if (!onUpdate) return;
|
if (!onUpdate) return;
|
||||||
const tailText = session.tail || session.aggregated;
|
const tailText = session.tail || session.aggregated;
|
||||||
|
const warningText = warning ? `${warning}\n\n` : "";
|
||||||
onUpdate({
|
onUpdate({
|
||||||
content: [{ type: "text", text: tailText || "" }],
|
content: [{ type: "text", text: warningText + (tailText || "") }],
|
||||||
details: {
|
details: {
|
||||||
status: "running",
|
status: "running",
|
||||||
sessionId,
|
sessionId,
|
||||||
pid: child.pid ?? undefined,
|
pid: session.pid ?? undefined,
|
||||||
startedAt,
|
startedAt,
|
||||||
tail: session.tail,
|
tail: session.tail,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
if (child) {
|
||||||
const str = sanitizeBinaryOutput(data.toString());
|
child.stdout.on("data", (data) => {
|
||||||
for (const chunk of chunkString(str)) {
|
const str = sanitizeBinaryOutput(data.toString());
|
||||||
appendOutput(session, "stdout", chunk);
|
for (const chunk of chunkString(str)) {
|
||||||
emitUpdate();
|
appendOutput(session, "stdout", chunk);
|
||||||
}
|
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) => {
|
||||||
@@ -249,14 +307,15 @@ export function createBashTool(
|
|||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text:
|
text:
|
||||||
`Command still running (session ${sessionId}, pid ${child.pid ?? "n/a"}). ` +
|
`${warning ? `${warning}\n\n` : ""}` +
|
||||||
|
`Command still running (session ${sessionId}, pid ${session.pid ?? "n/a"}). ` +
|
||||||
"Use process (list/poll/log/write/kill/clear/remove) for follow-up.",
|
"Use process (list/poll/log/write/kill/clear/remove) for follow-up.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
details: {
|
details: {
|
||||||
status: "running",
|
status: "running",
|
||||||
sessionId,
|
sessionId,
|
||||||
pid: child.pid ?? undefined,
|
pid: session.pid ?? undefined,
|
||||||
startedAt,
|
startedAt,
|
||||||
tail: session.tail,
|
tail: session.tail,
|
||||||
},
|
},
|
||||||
@@ -283,7 +342,10 @@ export function createBashTool(
|
|||||||
}, yieldWindow);
|
}, yieldWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
child.once("exit", (code, exitSignal) => {
|
const handleExit = (
|
||||||
|
code: number | null,
|
||||||
|
exitSignal: NodeJS.Signals | number | null,
|
||||||
|
) => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) clearTimeout(yieldTimer);
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
const durationMs = Date.now() - startedAt;
|
const durationMs = Date.now() - startedAt;
|
||||||
@@ -315,7 +377,14 @@ export function createBashTool(
|
|||||||
|
|
||||||
settle(() =>
|
settle(() =>
|
||||||
resolve({
|
resolve({
|
||||||
content: [{ type: "text", text: aggregated || "(no output)" }],
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text:
|
||||||
|
`${warning ? `${warning}\n\n` : ""}` +
|
||||||
|
(aggregated || "(no output)"),
|
||||||
|
},
|
||||||
|
],
|
||||||
details: {
|
details: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
exitCode: code ?? 0,
|
exitCode: code ?? 0,
|
||||||
@@ -324,14 +393,26 @@ export function createBashTool(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
child.once("error", (err) => {
|
if (child) {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
child.once("exit", (code, exitSignal) => {
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
handleExit(code, exitSignal);
|
||||||
markExited(session, null, null, "failed");
|
});
|
||||||
settle(() => reject(err));
|
|
||||||
});
|
child.once("error", (err) => {
|
||||||
|
if (yieldTimer) clearTimeout(yieldTimer);
|
||||||
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
|
markExited(session, null, null, "failed");
|
||||||
|
settle(() => reject(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pty) {
|
||||||
|
pty.onExit(({ exitCode, signal }) => {
|
||||||
|
handleExit(exitCode ?? null, signal ?? null);
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -383,7 +464,7 @@ export function createProcessTool(
|
|||||||
const running = listRunningSessions().map((s) => ({
|
const running = listRunningSessions().map((s) => ({
|
||||||
sessionId: s.id,
|
sessionId: s.id,
|
||||||
status: "running",
|
status: "running",
|
||||||
pid: s.child.pid ?? undefined,
|
pid: s.pid ?? undefined,
|
||||||
startedAt: s.startedAt,
|
startedAt: s.startedAt,
|
||||||
runtimeMs: Date.now() - s.startedAt,
|
runtimeMs: Date.now() - s.startedAt,
|
||||||
cwd: s.cwd,
|
cwd: s.cwd,
|
||||||
@@ -627,25 +708,43 @@ export function createProcessTool(
|
|||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!session.child.stdin || session.child.stdin.destroyed) {
|
if (session.stdinMode === "pty") {
|
||||||
return {
|
if (!session.pty || session.exited) {
|
||||||
content: [
|
return {
|
||||||
{
|
content: [
|
||||||
type: "text",
|
{
|
||||||
text: `Session ${params.sessionId} stdin is not writable.`,
|
type: "text",
|
||||||
},
|
text: `Session ${params.sessionId} stdin is not writable.`,
|
||||||
],
|
},
|
||||||
details: { status: "failed" },
|
],
|
||||||
};
|
details: { status: "failed" },
|
||||||
}
|
};
|
||||||
await new Promise<void>((resolve, reject) => {
|
}
|
||||||
session.child.stdin.write(params.data ?? "", (err) => {
|
session.pty.write(params.data ?? "");
|
||||||
if (err) reject(err);
|
if (params.eof) {
|
||||||
else resolve();
|
session.pty.write("\x04");
|
||||||
|
}
|
||||||
|
} 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) {
|
||||||
if (params.eof) {
|
session.child.stdin.end();
|
||||||
session.child.stdin.end();
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -687,9 +786,7 @@ export function createProcessTool(
|
|||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (session.child.pid) {
|
killSession(session);
|
||||||
killProcessTree(session.child.pid);
|
|
||||||
}
|
|
||||||
markExited(session, null, "SIGKILL", "failed");
|
markExited(session, null, "SIGKILL", "failed");
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -725,9 +822,7 @@ export function createProcessTool(
|
|||||||
|
|
||||||
case "remove": {
|
case "remove": {
|
||||||
if (session) {
|
if (session) {
|
||||||
if (session.child.pid) {
|
killSession(session);
|
||||||
killProcessTree(session.child.pid);
|
|
||||||
}
|
|
||||||
markExited(session, null, "SIGKILL", "failed");
|
markExited(session, null, "SIGKILL", "failed");
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -772,6 +867,25 @@ export function createProcessTool(
|
|||||||
|
|
||||||
export const processTool = createProcessTool();
|
export const processTool = createProcessTool();
|
||||||
|
|
||||||
|
function killSession(session: {
|
||||||
|
pid?: number;
|
||||||
|
stdinMode: ProcessStdinMode;
|
||||||
|
pty?: IPty;
|
||||||
|
child?: ChildProcessWithoutNullStreams;
|
||||||
|
}) {
|
||||||
|
const pid = session.pid ?? session.child?.pid ?? session.pty?.pid;
|
||||||
|
if (pid) {
|
||||||
|
killProcessTree(pid);
|
||||||
|
}
|
||||||
|
if (session.stdinMode === "pty") {
|
||||||
|
try {
|
||||||
|
session.pty?.kill();
|
||||||
|
} catch {
|
||||||
|
// ignore kill failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clampNumber(
|
function clampNumber(
|
||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
defaultValue: number,
|
defaultValue: number,
|
||||||
|
|||||||
Reference in New Issue
Block a user