@oyasmi/pipiclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +247 -0
- package/dist/agent.d.ts +18 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +938 -0
- package/dist/agent.js.map +1 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +45 -0
- package/dist/commands.js.map +1 -0
- package/dist/context.d.ts +139 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +432 -0
- package/dist/context.js.map +1 -0
- package/dist/delivery.d.ts +4 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +221 -0
- package/dist/delivery.js.map +1 -0
- package/dist/dingtalk.d.ts +109 -0
- package/dist/dingtalk.d.ts.map +1 -0
- package/dist/dingtalk.js +655 -0
- package/dist/dingtalk.js.map +1 -0
- package/dist/events.d.ts +51 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +287 -0
- package/dist/events.js.map +1 -0
- package/dist/log.d.ts +33 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +188 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +298 -0
- package/dist/main.js.map +1 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +10 -0
- package/dist/paths.js.map +1 -0
- package/dist/sandbox.d.ts +34 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +180 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/shell-escape.d.ts +6 -0
- package/dist/shell-escape.d.ts.map +1 -0
- package/dist/shell-escape.js +8 -0
- package/dist/shell-escape.js.map +1 -0
- package/dist/store.d.ts +41 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +110 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/attach.d.ts +14 -0
- package/dist/tools/attach.d.ts.map +1 -0
- package/dist/tools/attach.js +35 -0
- package/dist/tools/attach.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +78 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +11 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +129 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +132 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/truncate.d.ts +57 -0
- package/dist/tools/truncate.d.ts.map +1 -0
- package/dist/tools/truncate.js +184 -0
- package/dist/tools/truncate.js.map +1 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +31 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type SandboxConfig = {
|
|
2
|
+
type: "host";
|
|
3
|
+
} | {
|
|
4
|
+
type: "docker";
|
|
5
|
+
container: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseSandboxArg(value: string): SandboxConfig;
|
|
8
|
+
export declare function validateSandbox(config: SandboxConfig): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Create an executor that runs commands either on host or in Docker container
|
|
11
|
+
*/
|
|
12
|
+
export declare function createExecutor(config: SandboxConfig): Executor;
|
|
13
|
+
export interface Executor {
|
|
14
|
+
/**
|
|
15
|
+
* Execute a bash command
|
|
16
|
+
*/
|
|
17
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Get the workspace path prefix for this executor
|
|
20
|
+
* Host: returns the actual path
|
|
21
|
+
* Docker: returns /workspace
|
|
22
|
+
*/
|
|
23
|
+
getWorkspacePath(hostPath: string): string;
|
|
24
|
+
}
|
|
25
|
+
export interface ExecOptions {
|
|
26
|
+
timeout?: number;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
export interface ExecResult {
|
|
30
|
+
stdout: string;
|
|
31
|
+
stderr: string;
|
|
32
|
+
code: number;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAc5D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B1E;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,CAK9D;AAED,MAAM,WAAW,QAAQ;IACxB;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAElE;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3C;AAED,MAAM,WAAW,WAAW;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb","sourcesContent":["import { spawn } from \"child_process\";\nimport { shellEscape } from \"./shell-escape.js\";\n\nexport type SandboxConfig = { type: \"host\" } | { type: \"docker\"; container: string };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n\tif (value === \"host\") {\n\t\treturn { type: \"host\" };\n\t}\n\tif (value.startsWith(\"docker:\")) {\n\t\tconst container = value.slice(\"docker:\".length);\n\t\tif (!container) {\n\t\t\tconsole.error(\"Error: docker sandbox requires container name (e.g., docker:mom-sandbox)\");\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { type: \"docker\", container };\n\t}\n\tconsole.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);\n\tprocess.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n\tif (config.type === \"host\") {\n\t\treturn;\n\t}\n\n\t// Check if Docker is available\n\ttry {\n\t\tawait execSimple(\"docker\", [\"--version\"]);\n\t} catch {\n\t\tconsole.error(\"Error: Docker is not installed or not in PATH\");\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if container exists and is running\n\ttry {\n\t\tconst result = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", config.container]);\n\t\tif (result.trim() !== \"true\") {\n\t\t\tconsole.error(`Error: Container '${config.container}' is not running.`);\n\t\t\tconsole.error(`Start it with: docker start ${config.container}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t} catch {\n\t\tconsole.error(`Error: Container '${config.container}' does not exist.`);\n\t\tconsole.error(\"Create it with: ./docker.sh create <data-dir>\");\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log(` Docker container '${config.container}' is running.`);\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d;\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d;\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) resolve(stdout);\n\t\t\telse reject(new Error(stderr || `Exit code ${code}`));\n\t\t});\n\t});\n}\n\n/**\n * Create an executor that runs commands either on host or in Docker container\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n\tif (config.type === \"host\") {\n\t\treturn new HostExecutor();\n\t}\n\treturn new DockerExecutor(config.container);\n}\n\nexport interface Executor {\n\t/**\n\t * Execute a bash command\n\t */\n\texec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n\t/**\n\t * Get the workspace path prefix for this executor\n\t * Host: returns the actual path\n\t * Docker: returns /workspace\n\t */\n\tgetWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n\ttimeout?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\nclass HostExecutor implements Executor {\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n\t\t\tconst shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n\t\t\tconst child = spawn(shell, [...shellArgs, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\tconst timeoutHandle =\n\t\t\t\toptions?.timeout && options.timeout > 0\n\t\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\t\tkillProcessTree(child.pid!);\n\t\t\t\t\t\t}, options.timeout * 1000)\n\t\t\t\t\t: undefined;\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\tif (options?.signal) {\n\t\t\t\tif (options.signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\toptions.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild.stdout?.on(\"data\", (data) => {\n\t\t\t\tstdout += data.toString();\n\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.stderr?.on(\"data\", (data) => {\n\t\t\t\tstderr += data.toString();\n\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (options?.signal) {\n\t\t\t\t\toptions.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t\t});\n\t\t});\n\t}\n\n\tgetWorkspacePath(hostPath: string): string {\n\t\treturn hostPath;\n\t}\n}\n\nclass DockerExecutor implements Executor {\n\tconstructor(private container: string) {}\n\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\t// Wrap command for docker exec\n\t\tconst dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n\t\tconst hostExecutor = new HostExecutor();\n\t\treturn hostExecutor.exec(dockerCmd, options);\n\t}\n\n\tgetWorkspacePath(_hostPath: string): string {\n\t\t// Docker container sees /workspace\n\t\treturn \"/workspace\";\n\t}\n}\n\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t} else {\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { shellEscape } from "./shell-escape.js";
|
|
3
|
+
export function parseSandboxArg(value) {
|
|
4
|
+
if (value === "host") {
|
|
5
|
+
return { type: "host" };
|
|
6
|
+
}
|
|
7
|
+
if (value.startsWith("docker:")) {
|
|
8
|
+
const container = value.slice("docker:".length);
|
|
9
|
+
if (!container) {
|
|
10
|
+
console.error("Error: docker sandbox requires container name (e.g., docker:mom-sandbox)");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return { type: "docker", container };
|
|
14
|
+
}
|
|
15
|
+
console.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
export async function validateSandbox(config) {
|
|
19
|
+
if (config.type === "host") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Check if Docker is available
|
|
23
|
+
try {
|
|
24
|
+
await execSimple("docker", ["--version"]);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
console.error("Error: Docker is not installed or not in PATH");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// Check if container exists and is running
|
|
31
|
+
try {
|
|
32
|
+
const result = await execSimple("docker", ["inspect", "-f", "{{.State.Running}}", config.container]);
|
|
33
|
+
if (result.trim() !== "true") {
|
|
34
|
+
console.error(`Error: Container '${config.container}' is not running.`);
|
|
35
|
+
console.error(`Start it with: docker start ${config.container}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
console.error(`Error: Container '${config.container}' does not exist.`);
|
|
41
|
+
console.error("Create it with: ./docker.sh create <data-dir>");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
console.log(` Docker container '${config.container}' is running.`);
|
|
45
|
+
}
|
|
46
|
+
function execSimple(cmd, args) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
49
|
+
let stdout = "";
|
|
50
|
+
let stderr = "";
|
|
51
|
+
child.stdout?.on("data", (d) => {
|
|
52
|
+
stdout += d;
|
|
53
|
+
});
|
|
54
|
+
child.stderr?.on("data", (d) => {
|
|
55
|
+
stderr += d;
|
|
56
|
+
});
|
|
57
|
+
child.on("close", (code) => {
|
|
58
|
+
if (code === 0)
|
|
59
|
+
resolve(stdout);
|
|
60
|
+
else
|
|
61
|
+
reject(new Error(stderr || `Exit code ${code}`));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create an executor that runs commands either on host or in Docker container
|
|
67
|
+
*/
|
|
68
|
+
export function createExecutor(config) {
|
|
69
|
+
if (config.type === "host") {
|
|
70
|
+
return new HostExecutor();
|
|
71
|
+
}
|
|
72
|
+
return new DockerExecutor(config.container);
|
|
73
|
+
}
|
|
74
|
+
class HostExecutor {
|
|
75
|
+
async exec(command, options) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const shell = process.platform === "win32" ? "cmd" : "sh";
|
|
78
|
+
const shellArgs = process.platform === "win32" ? ["/c"] : ["-c"];
|
|
79
|
+
const child = spawn(shell, [...shellArgs, command], {
|
|
80
|
+
detached: true,
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
+
});
|
|
83
|
+
let stdout = "";
|
|
84
|
+
let stderr = "";
|
|
85
|
+
let timedOut = false;
|
|
86
|
+
const timeoutHandle = options?.timeout && options.timeout > 0
|
|
87
|
+
? setTimeout(() => {
|
|
88
|
+
timedOut = true;
|
|
89
|
+
killProcessTree(child.pid);
|
|
90
|
+
}, options.timeout * 1000)
|
|
91
|
+
: undefined;
|
|
92
|
+
const onAbort = () => {
|
|
93
|
+
if (child.pid)
|
|
94
|
+
killProcessTree(child.pid);
|
|
95
|
+
};
|
|
96
|
+
if (options?.signal) {
|
|
97
|
+
if (options.signal.aborted) {
|
|
98
|
+
onAbort();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
child.stdout?.on("data", (data) => {
|
|
105
|
+
stdout += data.toString();
|
|
106
|
+
if (stdout.length > 10 * 1024 * 1024) {
|
|
107
|
+
stdout = stdout.slice(0, 10 * 1024 * 1024);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
child.stderr?.on("data", (data) => {
|
|
111
|
+
stderr += data.toString();
|
|
112
|
+
if (stderr.length > 10 * 1024 * 1024) {
|
|
113
|
+
stderr = stderr.slice(0, 10 * 1024 * 1024);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
child.on("close", (code) => {
|
|
117
|
+
if (timeoutHandle)
|
|
118
|
+
clearTimeout(timeoutHandle);
|
|
119
|
+
if (options?.signal) {
|
|
120
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
121
|
+
}
|
|
122
|
+
if (options?.signal?.aborted) {
|
|
123
|
+
reject(new Error(`${stdout}\n${stderr}\nCommand aborted`.trim()));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (timedOut) {
|
|
127
|
+
reject(new Error(`${stdout}\n${stderr}\nCommand timed out after ${options?.timeout} seconds`.trim()));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
getWorkspacePath(hostPath) {
|
|
135
|
+
return hostPath;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
class DockerExecutor {
|
|
139
|
+
container;
|
|
140
|
+
constructor(container) {
|
|
141
|
+
this.container = container;
|
|
142
|
+
}
|
|
143
|
+
async exec(command, options) {
|
|
144
|
+
// Wrap command for docker exec
|
|
145
|
+
const dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;
|
|
146
|
+
const hostExecutor = new HostExecutor();
|
|
147
|
+
return hostExecutor.exec(dockerCmd, options);
|
|
148
|
+
}
|
|
149
|
+
getWorkspacePath(_hostPath) {
|
|
150
|
+
// Docker container sees /workspace
|
|
151
|
+
return "/workspace";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function killProcessTree(pid) {
|
|
155
|
+
if (process.platform === "win32") {
|
|
156
|
+
try {
|
|
157
|
+
spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
158
|
+
stdio: "ignore",
|
|
159
|
+
detached: true,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Ignore errors
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
try {
|
|
168
|
+
process.kill(-pid, "SIGKILL");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, "SIGKILL");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Process already dead
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=sandbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"child_process\";\nimport { shellEscape } from \"./shell-escape.js\";\n\nexport type SandboxConfig = { type: \"host\" } | { type: \"docker\"; container: string };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n\tif (value === \"host\") {\n\t\treturn { type: \"host\" };\n\t}\n\tif (value.startsWith(\"docker:\")) {\n\t\tconst container = value.slice(\"docker:\".length);\n\t\tif (!container) {\n\t\t\tconsole.error(\"Error: docker sandbox requires container name (e.g., docker:mom-sandbox)\");\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { type: \"docker\", container };\n\t}\n\tconsole.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);\n\tprocess.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n\tif (config.type === \"host\") {\n\t\treturn;\n\t}\n\n\t// Check if Docker is available\n\ttry {\n\t\tawait execSimple(\"docker\", [\"--version\"]);\n\t} catch {\n\t\tconsole.error(\"Error: Docker is not installed or not in PATH\");\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if container exists and is running\n\ttry {\n\t\tconst result = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", config.container]);\n\t\tif (result.trim() !== \"true\") {\n\t\t\tconsole.error(`Error: Container '${config.container}' is not running.`);\n\t\t\tconsole.error(`Start it with: docker start ${config.container}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t} catch {\n\t\tconsole.error(`Error: Container '${config.container}' does not exist.`);\n\t\tconsole.error(\"Create it with: ./docker.sh create <data-dir>\");\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log(` Docker container '${config.container}' is running.`);\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d;\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d;\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) resolve(stdout);\n\t\t\telse reject(new Error(stderr || `Exit code ${code}`));\n\t\t});\n\t});\n}\n\n/**\n * Create an executor that runs commands either on host or in Docker container\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n\tif (config.type === \"host\") {\n\t\treturn new HostExecutor();\n\t}\n\treturn new DockerExecutor(config.container);\n}\n\nexport interface Executor {\n\t/**\n\t * Execute a bash command\n\t */\n\texec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n\t/**\n\t * Get the workspace path prefix for this executor\n\t * Host: returns the actual path\n\t * Docker: returns /workspace\n\t */\n\tgetWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n\ttimeout?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\nclass HostExecutor implements Executor {\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n\t\t\tconst shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n\t\t\tconst child = spawn(shell, [...shellArgs, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\tconst timeoutHandle =\n\t\t\t\toptions?.timeout && options.timeout > 0\n\t\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\t\tkillProcessTree(child.pid!);\n\t\t\t\t\t\t}, options.timeout * 1000)\n\t\t\t\t\t: undefined;\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\tif (options?.signal) {\n\t\t\t\tif (options.signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\toptions.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild.stdout?.on(\"data\", (data) => {\n\t\t\t\tstdout += data.toString();\n\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.stderr?.on(\"data\", (data) => {\n\t\t\t\tstderr += data.toString();\n\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (options?.signal) {\n\t\t\t\t\toptions.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t\t});\n\t\t});\n\t}\n\n\tgetWorkspacePath(hostPath: string): string {\n\t\treturn hostPath;\n\t}\n}\n\nclass DockerExecutor implements Executor {\n\tconstructor(private container: string) {}\n\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\t// Wrap command for docker exec\n\t\tconst dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n\t\tconst hostExecutor = new HostExecutor();\n\t\treturn hostExecutor.exec(dockerCmd, options);\n\t}\n\n\tgetWorkspacePath(_hostPath: string): string {\n\t\t// Docker container sees /workspace\n\t\treturn \"/workspace\";\n\t}\n}\n\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t} else {\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-escape.d.ts","sourceRoot":"","sources":["../src/shell-escape.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C","sourcesContent":["/**\n * Shell-escape a string for safe use in sh -c commands.\n * Wraps in single quotes and escapes internal single quotes.\n */\nexport function shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-escape.js","sourceRoot":"","sources":["../src/shell-escape.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,CAAS,EAAU;IAC9C,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAAA,CACvC","sourcesContent":["/**\n * Shell-escape a string for safe use in sh -c commands.\n * Wraps in single quotes and escapes internal single quotes.\n */\nexport function shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface LoggedMessage {
|
|
2
|
+
date: string;
|
|
3
|
+
ts: string;
|
|
4
|
+
user: string;
|
|
5
|
+
userName?: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
text: string;
|
|
8
|
+
isBot: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ChannelStoreConfig {
|
|
11
|
+
workingDir: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class ChannelStore {
|
|
14
|
+
private workingDir;
|
|
15
|
+
private recentlyLogged;
|
|
16
|
+
constructor(config: ChannelStoreConfig);
|
|
17
|
+
/**
|
|
18
|
+
* Get or create the directory for a channel/DM
|
|
19
|
+
*/
|
|
20
|
+
getChannelDir(channelId: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Log a message to the channel's log.jsonl
|
|
23
|
+
* Returns false if message was already logged (duplicate)
|
|
24
|
+
*/
|
|
25
|
+
logMessage(channelId: string, message: LoggedMessage): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Rotate log file if it exceeds 1MB.
|
|
28
|
+
* Keeps one backup (log.jsonl.1) and resets the sync offset.
|
|
29
|
+
*/
|
|
30
|
+
private rotateIfNeeded;
|
|
31
|
+
/**
|
|
32
|
+
* Log a bot response
|
|
33
|
+
*/
|
|
34
|
+
logBotResponse(channelId: string, text: string, ts: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Get the timestamp of the last logged message for a channel
|
|
37
|
+
* Returns null if no log exists
|
|
38
|
+
*/
|
|
39
|
+
getLastTimestamp(channelId: string): string | null;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAOrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAwB5E;IAED;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBjD;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from "fs";
|
|
2
|
+
import { appendFile, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
export class ChannelStore {
|
|
5
|
+
workingDir;
|
|
6
|
+
// Track recently logged message timestamps to prevent duplicates
|
|
7
|
+
recentlyLogged = new Map();
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.workingDir = config.workingDir;
|
|
10
|
+
// Ensure working directory exists
|
|
11
|
+
if (!existsSync(this.workingDir)) {
|
|
12
|
+
mkdirSync(this.workingDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get or create the directory for a channel/DM
|
|
17
|
+
*/
|
|
18
|
+
getChannelDir(channelId) {
|
|
19
|
+
const dir = join(this.workingDir, channelId);
|
|
20
|
+
if (!existsSync(dir)) {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Log a message to the channel's log.jsonl
|
|
27
|
+
* Returns false if message was already logged (duplicate)
|
|
28
|
+
*/
|
|
29
|
+
async logMessage(channelId, message) {
|
|
30
|
+
// Check for duplicate (same channel + timestamp)
|
|
31
|
+
const dedupeKey = `${channelId}:${message.ts}`;
|
|
32
|
+
if (this.recentlyLogged.has(dedupeKey)) {
|
|
33
|
+
return false; // Already logged
|
|
34
|
+
}
|
|
35
|
+
// Mark as logged and schedule cleanup after 60 seconds
|
|
36
|
+
this.recentlyLogged.set(dedupeKey, Date.now());
|
|
37
|
+
setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);
|
|
38
|
+
const logPath = join(this.getChannelDir(channelId), "log.jsonl");
|
|
39
|
+
// Rotate if file exceeds size limit
|
|
40
|
+
this.rotateIfNeeded(logPath);
|
|
41
|
+
// Ensure message has a date field
|
|
42
|
+
if (!message.date) {
|
|
43
|
+
message.date = new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
46
|
+
await appendFile(logPath, line, "utf-8");
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Rotate log file if it exceeds 1MB.
|
|
51
|
+
* Keeps one backup (log.jsonl.1) and resets the sync offset.
|
|
52
|
+
*/
|
|
53
|
+
rotateIfNeeded(logPath) {
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(logPath))
|
|
56
|
+
return;
|
|
57
|
+
const stats = statSync(logPath);
|
|
58
|
+
if (stats.size > 1_000_000) {
|
|
59
|
+
renameSync(logPath, `${logPath}.1`);
|
|
60
|
+
// Reset sync offset since log.jsonl was replaced
|
|
61
|
+
const syncOffsetPath = join(dirname(logPath), ".sync-offset");
|
|
62
|
+
try {
|
|
63
|
+
writeFile(syncOffsetPath, "0", "utf-8").catch(() => { });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* ignore */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore rotation errors
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Log a bot response
|
|
76
|
+
*/
|
|
77
|
+
async logBotResponse(channelId, text, ts) {
|
|
78
|
+
await this.logMessage(channelId, {
|
|
79
|
+
date: new Date().toISOString(),
|
|
80
|
+
ts,
|
|
81
|
+
user: "bot",
|
|
82
|
+
text,
|
|
83
|
+
isBot: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the timestamp of the last logged message for a channel
|
|
88
|
+
* Returns null if no log exists
|
|
89
|
+
*/
|
|
90
|
+
getLastTimestamp(channelId) {
|
|
91
|
+
const logPath = join(this.workingDir, channelId, "log.jsonl");
|
|
92
|
+
if (!existsSync(logPath)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(logPath, "utf-8");
|
|
97
|
+
const lines = content.trim().split("\n");
|
|
98
|
+
if (lines.length === 0 || lines[0] === "") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const lastLine = lines[lines.length - 1];
|
|
102
|
+
const message = JSON.parse(lastLine);
|
|
103
|
+
return message.ts;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
|
+
declare const attachSchema: import("@sinclair/typebox").TObject<{
|
|
3
|
+
label: import("@sinclair/typebox").TString;
|
|
4
|
+
path: import("@sinclair/typebox").TString;
|
|
5
|
+
title: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
6
|
+
}>;
|
|
7
|
+
export type UploadFunction = (filePath: string, title?: string) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Create the attach tool. If no uploadFn is provided, the tool will throw
|
|
10
|
+
* an informative error guiding the LLM to use alternative approaches.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createAttachTool(uploadFn?: UploadFunction): AgentTool<typeof attachSchema>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=attach.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../../src/tools/attach.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAI7D,QAAA,MAAM,YAAY;;;;EAIhB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC,OAAO,YAAY,CAAC,CAiC1F","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { basename, resolve as resolvePath } from \"path\";\n\nconst attachSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what you're sharing (shown to user)\" }),\n\tpath: Type.String({ description: \"Path to the file to attach\" }),\n\ttitle: Type.Optional(Type.String({ description: \"Title for the file (defaults to filename)\" })),\n});\n\nexport type UploadFunction = (filePath: string, title?: string) => Promise<void>;\n\n/**\n * Create the attach tool. If no uploadFn is provided, the tool will throw\n * an informative error guiding the LLM to use alternative approaches.\n */\nexport function createAttachTool(uploadFn?: UploadFunction): AgentTool<typeof attachSchema> {\n\treturn {\n\t\tname: \"attach\",\n\t\tlabel: \"attach\",\n\t\tdescription:\n\t\t\t\"Attach a file to your response. Use this to share files, images, or documents with the user. Only files from /workspace/ can be attached.\",\n\t\tparameters: attachSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, title }: { label: string; path: string; title?: string },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tif (!uploadFn) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"File upload is not supported in DingTalk mode. Output file content as text instead, or use bash to host files.\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t}\n\n\t\t\tconst absolutePath = resolvePath(path);\n\t\t\tconst fileName = title || basename(absolutePath);\n\n\t\t\tawait uploadFn(absolutePath, fileName);\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: `Attached file: ${fileName}` }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { basename, resolve as resolvePath } from "path";
|
|
3
|
+
const attachSchema = Type.Object({
|
|
4
|
+
label: Type.String({ description: "Brief description of what you're sharing (shown to user)" }),
|
|
5
|
+
path: Type.String({ description: "Path to the file to attach" }),
|
|
6
|
+
title: Type.Optional(Type.String({ description: "Title for the file (defaults to filename)" })),
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Create the attach tool. If no uploadFn is provided, the tool will throw
|
|
10
|
+
* an informative error guiding the LLM to use alternative approaches.
|
|
11
|
+
*/
|
|
12
|
+
export function createAttachTool(uploadFn) {
|
|
13
|
+
return {
|
|
14
|
+
name: "attach",
|
|
15
|
+
label: "attach",
|
|
16
|
+
description: "Attach a file to your response. Use this to share files, images, or documents with the user. Only files from /workspace/ can be attached.",
|
|
17
|
+
parameters: attachSchema,
|
|
18
|
+
execute: async (_toolCallId, { path, title }, signal) => {
|
|
19
|
+
if (!uploadFn) {
|
|
20
|
+
throw new Error("File upload is not supported in DingTalk mode. Output file content as text instead, or use bash to host files.");
|
|
21
|
+
}
|
|
22
|
+
if (signal?.aborted) {
|
|
23
|
+
throw new Error("Operation aborted");
|
|
24
|
+
}
|
|
25
|
+
const absolutePath = resolvePath(path);
|
|
26
|
+
const fileName = title || basename(absolutePath);
|
|
27
|
+
await uploadFn(absolutePath, fileName);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: `Attached file: ${fileName}` }],
|
|
30
|
+
details: undefined,
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=attach.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.js","sourceRoot":"","sources":["../../src/tools/attach.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,MAAM,CAAC;AAExD,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IAChC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC;IAC/F,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;IAChE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC,CAAC;CAC/F,CAAC,CAAC;AAIH;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAyB,EAAkC;IAC3F,OAAO;QACN,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,QAAQ;QACf,WAAW,EACV,2IAA2I;QAC5I,UAAU,EAAE,YAAY;QACxB,OAAO,EAAE,KAAK,EACb,WAAmB,EACnB,EAAE,IAAI,EAAE,KAAK,EAAmD,EAChE,MAAoB,EACnB,EAAE,CAAC;YACJ,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CACd,gHAAgH,CAChH,CAAC;YACH,CAAC;YAED,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACrB,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YACtC,CAAC;YAED,MAAM,YAAY,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,QAAQ,GAAG,KAAK,IAAI,QAAQ,CAAC,YAAY,CAAC,CAAC;YAEjD,MAAM,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;YAEvC,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,kBAAkB,QAAQ,EAAE,EAAE,CAAC;gBACxE,OAAO,EAAE,SAAS;aAClB,CAAC;QAAA,CACF;KACD,CAAC;AAAA,CACF","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport { basename, resolve as resolvePath } from \"path\";\n\nconst attachSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what you're sharing (shown to user)\" }),\n\tpath: Type.String({ description: \"Path to the file to attach\" }),\n\ttitle: Type.Optional(Type.String({ description: \"Title for the file (defaults to filename)\" })),\n});\n\nexport type UploadFunction = (filePath: string, title?: string) => Promise<void>;\n\n/**\n * Create the attach tool. If no uploadFn is provided, the tool will throw\n * an informative error guiding the LLM to use alternative approaches.\n */\nexport function createAttachTool(uploadFn?: UploadFunction): AgentTool<typeof attachSchema> {\n\treturn {\n\t\tname: \"attach\",\n\t\tlabel: \"attach\",\n\t\tdescription:\n\t\t\t\"Attach a file to your response. Use this to share files, images, or documents with the user. Only files from /workspace/ can be attached.\",\n\t\tparameters: attachSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ path, title }: { label: string; path: string; title?: string },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\tif (!uploadFn) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"File upload is not supported in DingTalk mode. Output file content as text instead, or use bash to host files.\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Operation aborted\");\n\t\t\t}\n\n\t\t\tconst absolutePath = resolvePath(path);\n\t\t\tconst fileName = title || basename(absolutePath);\n\n\t\t\tawait uploadFn(absolutePath, fileName);\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: `Attached file: ${fileName}` }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { Executor } from "../sandbox.js";
|
|
3
|
+
declare const bashSchema: import("@sinclair/typebox").TObject<{
|
|
4
|
+
label: import("@sinclair/typebox").TString;
|
|
5
|
+
command: import("@sinclair/typebox").TString;
|
|
6
|
+
timeout: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function createBashTool(executor: Executor): AgentTool<typeof bashSchema>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=bash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAW9C,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAOH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAoE/E","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\nimport type { Executor } from \"../sandbox.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `mom-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tlabel: Type.String({ description: \"Brief description of what this command does (shown to user)\" }),\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport function createBashTool(executor: Executor): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\texecute: async (\n\t\t\t_toolCallId: string,\n\t\t\t{ command, timeout }: { label: string; command: string; timeout?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t) => {\n\t\t\t// Track output for potential temp file writing\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\n\t\t\tconst result = await executor.exec(command, { timeout, signal });\n\t\t\tlet output = \"\";\n\t\t\tif (result.stdout) output += result.stdout;\n\t\t\tif (result.stderr) {\n\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\toutput += result.stderr;\n\t\t\t}\n\n\t\t\tconst totalBytes = Buffer.byteLength(output, \"utf-8\");\n\n\t\t\t// Write to temp file if output exceeds limit\n\t\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\ttempFileStream.write(output);\n\t\t\t\ttempFileStream.end();\n\t\t\t}\n\n\t\t\t// Apply tail truncation\n\t\t\tconst truncation = truncateTail(output);\n\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t// Build details with truncation info\n\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\tif (truncation.truncated) {\n\t\t\t\tdetails = {\n\t\t\t\t\ttruncation,\n\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t};\n\n\t\t\t\t// Build actionable notice\n\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t// Edge case: last line alone > 50KB\n\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(output.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t} else {\n\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tthrow new Error(`${outputText}\\n\\nCommand exited with code ${result.code}`.trim());\n\t\t\t}\n\n\t\t\treturn { content: [{ type: \"text\", text: outputText }], details };\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
|
|
7
|
+
/**
|
|
8
|
+
* Generate a unique temp file path for bash output
|
|
9
|
+
*/
|
|
10
|
+
function getTempFilePath() {
|
|
11
|
+
const id = randomBytes(8).toString("hex");
|
|
12
|
+
return join(tmpdir(), `mom-bash-${id}.log`);
|
|
13
|
+
}
|
|
14
|
+
const bashSchema = Type.Object({
|
|
15
|
+
label: Type.String({ description: "Brief description of what this command does (shown to user)" }),
|
|
16
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
17
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
18
|
+
});
|
|
19
|
+
export function createBashTool(executor) {
|
|
20
|
+
return {
|
|
21
|
+
name: "bash",
|
|
22
|
+
label: "bash",
|
|
23
|
+
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
|
24
|
+
parameters: bashSchema,
|
|
25
|
+
execute: async (_toolCallId, { command, timeout }, signal) => {
|
|
26
|
+
// Track output for potential temp file writing
|
|
27
|
+
let tempFilePath;
|
|
28
|
+
let tempFileStream;
|
|
29
|
+
const result = await executor.exec(command, { timeout, signal });
|
|
30
|
+
let output = "";
|
|
31
|
+
if (result.stdout)
|
|
32
|
+
output += result.stdout;
|
|
33
|
+
if (result.stderr) {
|
|
34
|
+
if (output)
|
|
35
|
+
output += "\n";
|
|
36
|
+
output += result.stderr;
|
|
37
|
+
}
|
|
38
|
+
const totalBytes = Buffer.byteLength(output, "utf-8");
|
|
39
|
+
// Write to temp file if output exceeds limit
|
|
40
|
+
if (totalBytes > DEFAULT_MAX_BYTES) {
|
|
41
|
+
tempFilePath = getTempFilePath();
|
|
42
|
+
tempFileStream = createWriteStream(tempFilePath);
|
|
43
|
+
tempFileStream.write(output);
|
|
44
|
+
tempFileStream.end();
|
|
45
|
+
}
|
|
46
|
+
// Apply tail truncation
|
|
47
|
+
const truncation = truncateTail(output);
|
|
48
|
+
let outputText = truncation.content || "(no output)";
|
|
49
|
+
// Build details with truncation info
|
|
50
|
+
let details;
|
|
51
|
+
if (truncation.truncated) {
|
|
52
|
+
details = {
|
|
53
|
+
truncation,
|
|
54
|
+
fullOutputPath: tempFilePath,
|
|
55
|
+
};
|
|
56
|
+
// Build actionable notice
|
|
57
|
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
58
|
+
const endLine = truncation.totalLines;
|
|
59
|
+
if (truncation.lastLinePartial) {
|
|
60
|
+
// Edge case: last line alone > 50KB
|
|
61
|
+
const lastLineSize = formatSize(Buffer.byteLength(output.split("\n").pop() || "", "utf-8"));
|
|
62
|
+
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
|
63
|
+
}
|
|
64
|
+
else if (truncation.truncatedBy === "lines") {
|
|
65
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (result.code !== 0) {
|
|
72
|
+
throw new Error(`${outputText}\n\nCommand exited with code ${result.code}`.trim());
|
|
73
|
+
}
|
|
74
|
+
return { content: [{ type: "text", text: outputText }], details };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=bash.js.map
|