@oh-my-pi/pi-mom 1.337.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 +277 -0
- package/README.md +517 -0
- package/dev.sh +30 -0
- package/docker.sh +95 -0
- package/docs/artifacts-server.md +475 -0
- package/docs/events.md +307 -0
- package/docs/new.md +977 -0
- package/docs/sandbox.md +153 -0
- package/docs/slack-bot-minimal-guide.md +399 -0
- package/docs/v86.md +319 -0
- package/package.json +44 -0
- package/scripts/migrate-timestamps.ts +121 -0
- package/src/agent.ts +860 -0
- package/src/context.ts +636 -0
- package/src/download.ts +117 -0
- package/src/events.ts +383 -0
- package/src/log.ts +271 -0
- package/src/main.ts +332 -0
- package/src/sandbox.ts +215 -0
- package/src/slack.ts +623 -0
- package/src/store.ts +234 -0
- package/src/tools/attach.ts +47 -0
- package/src/tools/bash.ts +99 -0
- package/src/tools/edit.ts +165 -0
- package/src/tools/index.ts +19 -0
- package/src/tools/read.ts +165 -0
- package/src/tools/truncate.ts +236 -0
- package/src/tools/write.ts +45 -0
- package/tsconfig.build.json +9 -0
package/src/sandbox.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export type SandboxConfig = { type: "host" } | { type: "docker"; container: string };
|
|
2
|
+
|
|
3
|
+
export function parseSandboxArg(value: string): SandboxConfig {
|
|
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
|
+
|
|
19
|
+
export async function validateSandbox(config: SandboxConfig): Promise<void> {
|
|
20
|
+
if (config.type === "host") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if Docker is available
|
|
25
|
+
try {
|
|
26
|
+
await execSimple("docker", ["--version"]);
|
|
27
|
+
} catch {
|
|
28
|
+
console.error("Error: Docker is not installed or not in PATH");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if container exists and is running
|
|
33
|
+
try {
|
|
34
|
+
const result = await execSimple("docker", ["inspect", "-f", "{{.State.Running}}", config.container]);
|
|
35
|
+
if (result.trim() !== "true") {
|
|
36
|
+
console.error(`Error: Container '${config.container}' is not running.`);
|
|
37
|
+
console.error(`Start it with: docker start ${config.container}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
console.error(`Error: Container '${config.container}' does not exist.`);
|
|
42
|
+
console.error("Create it with: ./docker.sh create <data-dir>");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(` Docker container '${config.container}' is running.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function execSimple(cmd: string, args: string[]): Promise<string> {
|
|
50
|
+
const proc = Bun.spawn([cmd, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
51
|
+
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
52
|
+
const code = await proc.exited;
|
|
53
|
+
if (code === 0) return stdout;
|
|
54
|
+
throw new Error(stderr || `Exit code ${code}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create an executor that runs commands either on host or in Docker container
|
|
59
|
+
*/
|
|
60
|
+
export function createExecutor(config: SandboxConfig): Executor {
|
|
61
|
+
if (config.type === "host") {
|
|
62
|
+
return new HostExecutor();
|
|
63
|
+
}
|
|
64
|
+
return new DockerExecutor(config.container);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Executor {
|
|
68
|
+
/**
|
|
69
|
+
* Execute a bash command
|
|
70
|
+
*/
|
|
71
|
+
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the workspace path prefix for this executor
|
|
75
|
+
* Host: returns the actual path
|
|
76
|
+
* Docker: returns /workspace
|
|
77
|
+
*/
|
|
78
|
+
getWorkspacePath(hostPath: string): string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ExecOptions {
|
|
82
|
+
timeout?: number;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ExecResult {
|
|
87
|
+
stdout: string;
|
|
88
|
+
stderr: string;
|
|
89
|
+
code: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class HostExecutor implements Executor {
|
|
93
|
+
async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
|
|
94
|
+
const shell = process.platform === "win32" ? "cmd" : "sh";
|
|
95
|
+
const shellArgs = process.platform === "win32" ? ["/c"] : ["-c"];
|
|
96
|
+
|
|
97
|
+
let timedOut = false;
|
|
98
|
+
let proc: ReturnType<typeof Bun.spawn> | null = null;
|
|
99
|
+
|
|
100
|
+
const timeoutHandle =
|
|
101
|
+
options?.timeout && options.timeout > 0
|
|
102
|
+
? setTimeout(() => {
|
|
103
|
+
timedOut = true;
|
|
104
|
+
if (proc) killProcessTree(proc.pid);
|
|
105
|
+
}, options.timeout * 1000)
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
const onAbort = () => {
|
|
109
|
+
if (proc) killProcessTree(proc.pid);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (options?.signal) {
|
|
113
|
+
if (options.signal.aborted) {
|
|
114
|
+
onAbort();
|
|
115
|
+
} else {
|
|
116
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
proc = Bun.spawn([shell, ...shellArgs, command], {
|
|
121
|
+
stdout: "pipe",
|
|
122
|
+
stderr: "pipe",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const MAX_BYTES = 10 * 1024 * 1024;
|
|
126
|
+
|
|
127
|
+
// Stream and truncate stdout/stderr
|
|
128
|
+
const readStream = async (stream: ReadableStream<Uint8Array>): Promise<string> => {
|
|
129
|
+
const reader = stream.getReader();
|
|
130
|
+
const decoder = new TextDecoder();
|
|
131
|
+
let result = "";
|
|
132
|
+
try {
|
|
133
|
+
while (true) {
|
|
134
|
+
const { done, value } = await reader.read();
|
|
135
|
+
if (done) break;
|
|
136
|
+
result += decoder.decode(value, { stream: true });
|
|
137
|
+
if (result.length > MAX_BYTES) {
|
|
138
|
+
result = result.slice(0, MAX_BYTES);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
reader.releaseLock();
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const [stdout, stderr] = await Promise.all([
|
|
149
|
+
readStream(proc.stdout as ReadableStream<Uint8Array>),
|
|
150
|
+
readStream(proc.stderr as ReadableStream<Uint8Array>),
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const code = await proc.exited;
|
|
154
|
+
|
|
155
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
156
|
+
if (options?.signal) {
|
|
157
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options?.signal?.aborted) {
|
|
161
|
+
throw new Error(`${stdout}\n${stderr}\nCommand aborted`.trim());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (timedOut) {
|
|
165
|
+
throw new Error(`${stdout}\n${stderr}\nCommand timed out after ${options?.timeout} seconds`.trim());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { stdout, stderr, code };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getWorkspacePath(hostPath: string): string {
|
|
172
|
+
return hostPath;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class DockerExecutor implements Executor {
|
|
177
|
+
constructor(private container: string) {}
|
|
178
|
+
|
|
179
|
+
async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
|
|
180
|
+
// Wrap command for docker exec
|
|
181
|
+
const dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;
|
|
182
|
+
const hostExecutor = new HostExecutor();
|
|
183
|
+
return hostExecutor.exec(dockerCmd, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getWorkspacePath(_hostPath: string): string {
|
|
187
|
+
// Docker container sees /workspace
|
|
188
|
+
return "/workspace";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function killProcessTree(pid: number): void {
|
|
193
|
+
if (process.platform === "win32") {
|
|
194
|
+
try {
|
|
195
|
+
Bun.spawn(["taskkill", "/F", "/T", "/PID", String(pid)], { stdout: "ignore", stderr: "ignore" });
|
|
196
|
+
} catch {
|
|
197
|
+
// Ignore errors
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(-pid, "SIGKILL");
|
|
202
|
+
} catch {
|
|
203
|
+
try {
|
|
204
|
+
process.kill(pid, "SIGKILL");
|
|
205
|
+
} catch {
|
|
206
|
+
// Process already dead
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function shellEscape(s: string): string {
|
|
213
|
+
// Escape for passing to sh -c
|
|
214
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
215
|
+
}
|