clawmatrix 0.1.0 → 0.1.1
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/package.json +3 -1
- package/src/cluster-service.ts +2 -1
- package/src/compat.ts +62 -0
- package/src/handoff.ts +4 -3
- package/src/model-proxy.ts +87 -54
- package/src/peer-manager.ts +42 -38
- package/src/tool-proxy.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmatrix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Decentralized mesh cluster plugin for OpenClaw — inter-gateway communication, model proxy, task handoff, and tool proxy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,10 +31,12 @@
|
|
|
31
31
|
"prepublishOnly": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"ws": "^8.19.0",
|
|
34
35
|
"zod": "^4.3.6"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/bun": "latest",
|
|
39
|
+
"@types/ws": "^8.18.1",
|
|
38
40
|
"openclaw": "^2026.3.2"
|
|
39
41
|
},
|
|
40
42
|
"peerDependencies": {
|
package/src/cluster-service.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
PluginLogger,
|
|
5
5
|
} from "openclaw/plugin-sdk";
|
|
6
6
|
import type { ClawMatrixConfig } from "./config.ts";
|
|
7
|
+
import { spawnProcess } from "./compat.ts";
|
|
7
8
|
import { PeerManager } from "./peer-manager.ts";
|
|
8
9
|
import { HandoffManager } from "./handoff.ts";
|
|
9
10
|
import { ModelProxy } from "./model-proxy.ts";
|
|
@@ -124,7 +125,7 @@ export class ClusterRuntime {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// Fire-and-forget: inject message via openclaw agent
|
|
127
|
-
|
|
128
|
+
spawnProcess(["openclaw", "agent", "--message", message], {
|
|
128
129
|
stdout: "ignore",
|
|
129
130
|
stderr: "ignore",
|
|
130
131
|
});
|
package/src/compat.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime compatibility layer — provides Node.js-compatible alternatives
|
|
3
|
+
* for Bun-specific APIs so the plugin works inside the OpenClaw gateway
|
|
4
|
+
* (which runs on Node.js).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn as cpSpawn } from "node:child_process";
|
|
8
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
|
|
10
|
+
export interface SpawnResult {
|
|
11
|
+
exitCode: number;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Spawn a subprocess and collect stdout/stderr. */
|
|
17
|
+
export function spawnProcess(
|
|
18
|
+
cmd: string[],
|
|
19
|
+
opts?: { cwd?: string; stdout?: "pipe" | "ignore"; stderr?: "pipe" | "ignore" },
|
|
20
|
+
): { exited: Promise<number>; stdout: ReadableStream | null; stderr: ReadableStream | null; kill: () => void } {
|
|
21
|
+
const child = cpSpawn(cmd[0]!, cmd.slice(1), {
|
|
22
|
+
cwd: opts?.cwd,
|
|
23
|
+
stdio: [
|
|
24
|
+
"ignore",
|
|
25
|
+
opts?.stdout === "ignore" ? "ignore" : "pipe",
|
|
26
|
+
opts?.stderr === "ignore" ? "ignore" : "pipe",
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const exited = new Promise<number>((resolve, reject) => {
|
|
31
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
32
|
+
child.on("error", reject);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function nodeStreamToWeb(stream: import("node:stream").Readable | null): ReadableStream | null {
|
|
36
|
+
if (!stream) return null;
|
|
37
|
+
return new ReadableStream({
|
|
38
|
+
start(controller) {
|
|
39
|
+
stream.on("data", (chunk: Buffer) => controller.enqueue(chunk));
|
|
40
|
+
stream.on("end", () => controller.close());
|
|
41
|
+
stream.on("error", (err) => controller.error(err));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
exited,
|
|
48
|
+
stdout: nodeStreamToWeb(child.stdout),
|
|
49
|
+
stderr: nodeStreamToWeb(child.stderr),
|
|
50
|
+
kill: () => child.kill(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read a file as text (replaces Bun.file().text()). */
|
|
55
|
+
export async function readFileText(path: string): Promise<string> {
|
|
56
|
+
return readFile(path, "utf-8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Write text to a file (replaces Bun.write()). */
|
|
60
|
+
export async function writeFileText(path: string, content: string): Promise<void> {
|
|
61
|
+
await writeFile(path, content, "utf-8");
|
|
62
|
+
}
|
package/src/handoff.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PeerManager } from "./peer-manager.ts";
|
|
2
2
|
import type { ClawMatrixConfig } from "./config.ts";
|
|
3
|
+
import { spawnProcess } from "./compat.ts";
|
|
3
4
|
import type {
|
|
4
5
|
HandoffRequest,
|
|
5
6
|
HandoffResponse,
|
|
@@ -147,16 +148,16 @@ export class HandoffManager {
|
|
|
147
148
|
? `${payload.task}\n\nContext:\n${payload.context}`
|
|
148
149
|
: payload.task;
|
|
149
150
|
|
|
150
|
-
const proc =
|
|
151
|
+
const proc = spawnProcess(["openclaw", "agent", "--message", message], {
|
|
151
152
|
stdout: "pipe",
|
|
152
153
|
stderr: "pipe",
|
|
153
154
|
});
|
|
154
155
|
|
|
155
|
-
const stdout = await new Response(proc.stdout).text();
|
|
156
|
+
const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
|
|
156
157
|
const exitCode = await proc.exited;
|
|
157
158
|
|
|
158
159
|
if (exitCode !== 0) {
|
|
159
|
-
const stderr = await new Response(proc.stderr).text();
|
|
160
|
+
const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
|
|
160
161
|
throw new Error(`Agent exited with code ${exitCode}: ${stderr}`);
|
|
161
162
|
}
|
|
162
163
|
|
package/src/model-proxy.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createServer, type Server } from "node:http";
|
|
1
2
|
import type { PeerManager } from "./peer-manager.ts";
|
|
2
3
|
import type { ClawMatrixConfig } from "./config.ts";
|
|
3
4
|
import type {
|
|
@@ -21,7 +22,7 @@ export class ModelProxy {
|
|
|
21
22
|
private config: ClawMatrixConfig;
|
|
22
23
|
private peerManager: PeerManager;
|
|
23
24
|
private pending = new Map<string, PendingModelReq>();
|
|
24
|
-
private httpServer:
|
|
25
|
+
private httpServer: Server | null = null;
|
|
25
26
|
|
|
26
27
|
constructor(config: ClawMatrixConfig, peerManager: PeerManager) {
|
|
27
28
|
this.config = config;
|
|
@@ -30,26 +31,28 @@ export class ModelProxy {
|
|
|
30
31
|
|
|
31
32
|
/** Start the local HTTP proxy server for OpenAI-compatible requests. */
|
|
32
33
|
start() {
|
|
33
|
-
this.httpServer =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
34
|
+
this.httpServer = createServer(async (req, res) => {
|
|
35
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
36
|
+
|
|
37
|
+
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
|
38
|
+
const body = await this.readBody(req);
|
|
39
|
+
const response = await this.handleChatCompletion(body);
|
|
40
|
+
this.sendResponse(res, response);
|
|
41
|
+
} else if (url.pathname === "/v1/models" && req.method === "GET") {
|
|
42
|
+
const response = this.handleListModels();
|
|
43
|
+
this.sendResponse(res, response);
|
|
44
|
+
} else {
|
|
45
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
46
|
+
res.end("Not Found");
|
|
47
|
+
}
|
|
47
48
|
});
|
|
49
|
+
|
|
50
|
+
this.httpServer.listen(this.config.proxyPort, "127.0.0.1");
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
stop() {
|
|
51
54
|
if (this.httpServer) {
|
|
52
|
-
this.httpServer.
|
|
55
|
+
this.httpServer.close();
|
|
53
56
|
this.httpServer = null;
|
|
54
57
|
}
|
|
55
58
|
for (const [, pending] of this.pending) {
|
|
@@ -59,8 +62,38 @@ export class ModelProxy {
|
|
|
59
62
|
this.pending.clear();
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
private readBody(req: import("node:http").IncomingMessage): Promise<string> {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const chunks: Buffer[] = [];
|
|
68
|
+
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
69
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
70
|
+
req.on("error", reject);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private sendResponse(res: import("node:http").ServerResponse, response: { status: number; headers: Record<string, string>; body: string | ReadableStream }) {
|
|
75
|
+
res.writeHead(response.status, response.headers);
|
|
76
|
+
if (typeof response.body === "string") {
|
|
77
|
+
res.end(response.body);
|
|
78
|
+
} else {
|
|
79
|
+
// Stream response
|
|
80
|
+
const reader = response.body.getReader();
|
|
81
|
+
const pump = (): void => {
|
|
82
|
+
reader.read().then(({ done, value }) => {
|
|
83
|
+
if (done) {
|
|
84
|
+
res.end();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
res.write(value);
|
|
88
|
+
pump();
|
|
89
|
+
}).catch(() => res.end());
|
|
90
|
+
};
|
|
91
|
+
pump();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
62
95
|
// ── HTTP handlers ──────────────────────────────────────────────
|
|
63
|
-
private async handleChatCompletion(
|
|
96
|
+
private async handleChatCompletion(rawBody: string): Promise<{ status: number; headers: Record<string, string>; body: string | ReadableStream }> {
|
|
64
97
|
let body: {
|
|
65
98
|
model: string;
|
|
66
99
|
messages: unknown[];
|
|
@@ -70,21 +103,23 @@ export class ModelProxy {
|
|
|
70
103
|
};
|
|
71
104
|
|
|
72
105
|
try {
|
|
73
|
-
body =
|
|
106
|
+
body = JSON.parse(rawBody);
|
|
74
107
|
} catch {
|
|
75
|
-
return
|
|
108
|
+
return {
|
|
76
109
|
status: 400,
|
|
77
110
|
headers: { "Content-Type": "application/json" },
|
|
78
|
-
|
|
111
|
+
body: JSON.stringify({ error: "Invalid JSON" }),
|
|
112
|
+
};
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
const modelId = body.model;
|
|
82
116
|
const route = this.peerManager.router.resolveModel(modelId);
|
|
83
117
|
if (!route) {
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
return {
|
|
119
|
+
status: 404,
|
|
120
|
+
headers: { "Content-Type": "application/json" },
|
|
121
|
+
body: JSON.stringify({ error: { message: `Model "${modelId}" not found in cluster` } }),
|
|
122
|
+
};
|
|
88
123
|
}
|
|
89
124
|
|
|
90
125
|
const stream = body.stream ?? false;
|
|
@@ -116,7 +151,7 @@ export class ModelProxy {
|
|
|
116
151
|
requestId: string,
|
|
117
152
|
targetNodeId: string,
|
|
118
153
|
frame: ModelRequest,
|
|
119
|
-
):
|
|
154
|
+
): { status: number; headers: Record<string, string>; body: ReadableStream } {
|
|
120
155
|
const encoder = new TextEncoder();
|
|
121
156
|
|
|
122
157
|
const readable = new ReadableStream({
|
|
@@ -159,20 +194,22 @@ export class ModelProxy {
|
|
|
159
194
|
},
|
|
160
195
|
});
|
|
161
196
|
|
|
162
|
-
return
|
|
197
|
+
return {
|
|
198
|
+
status: 200,
|
|
163
199
|
headers: {
|
|
164
200
|
"Content-Type": "text/event-stream",
|
|
165
201
|
"Cache-Control": "no-cache",
|
|
166
|
-
Connection: "keep-alive",
|
|
202
|
+
"Connection": "keep-alive",
|
|
167
203
|
},
|
|
168
|
-
|
|
204
|
+
body: readable,
|
|
205
|
+
};
|
|
169
206
|
}
|
|
170
207
|
|
|
171
208
|
private async handleNonStreamRequest(
|
|
172
209
|
requestId: string,
|
|
173
210
|
targetNodeId: string,
|
|
174
211
|
frame: ModelRequest,
|
|
175
|
-
): Promise<
|
|
212
|
+
): Promise<{ status: number; headers: Record<string, string>; body: string }> {
|
|
176
213
|
try {
|
|
177
214
|
const result = await new Promise<ModelResponse["payload"]>(
|
|
178
215
|
(resolve, reject) => {
|
|
@@ -199,15 +236,17 @@ export class ModelProxy {
|
|
|
199
236
|
);
|
|
200
237
|
|
|
201
238
|
if (!result.success) {
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
return {
|
|
240
|
+
status: 502,
|
|
241
|
+
headers: { "Content-Type": "application/json" },
|
|
242
|
+
body: JSON.stringify({ error: { message: result.error } }),
|
|
243
|
+
};
|
|
206
244
|
}
|
|
207
245
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
246
|
+
return {
|
|
247
|
+
status: 200,
|
|
248
|
+
headers: { "Content-Type": "application/json" },
|
|
249
|
+
body: JSON.stringify({
|
|
211
250
|
id: `chatcmpl-${requestId}`,
|
|
212
251
|
object: "chat.completion",
|
|
213
252
|
created: Math.floor(Date.now() / 1000),
|
|
@@ -227,17 +266,17 @@ export class ModelProxy {
|
|
|
227
266
|
}
|
|
228
267
|
: undefined,
|
|
229
268
|
}),
|
|
230
|
-
|
|
231
|
-
);
|
|
269
|
+
};
|
|
232
270
|
} catch (err) {
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
271
|
+
return {
|
|
272
|
+
status: 502,
|
|
273
|
+
headers: { "Content-Type": "application/json" },
|
|
274
|
+
body: JSON.stringify({ error: { message: err instanceof Error ? err.message : String(err) } }),
|
|
275
|
+
};
|
|
237
276
|
}
|
|
238
277
|
}
|
|
239
278
|
|
|
240
|
-
private handleListModels():
|
|
279
|
+
private handleListModels(): { status: number; headers: Record<string, string>; body: string } {
|
|
241
280
|
const models = this.peerManager.router
|
|
242
281
|
.getAllPeers()
|
|
243
282
|
.flatMap((p) =>
|
|
@@ -249,10 +288,11 @@ export class ModelProxy {
|
|
|
249
288
|
})),
|
|
250
289
|
);
|
|
251
290
|
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
291
|
+
return {
|
|
292
|
+
status: 200,
|
|
293
|
+
headers: { "Content-Type": "application/json" },
|
|
294
|
+
body: JSON.stringify({ object: "list", data: models }),
|
|
295
|
+
};
|
|
256
296
|
}
|
|
257
297
|
|
|
258
298
|
// ── Incoming frame handlers ────────────────────────────────────
|
|
@@ -273,7 +313,6 @@ export class ModelProxy {
|
|
|
273
313
|
|
|
274
314
|
try {
|
|
275
315
|
if (frame.payload.done) {
|
|
276
|
-
// Send final chunk with usage if available
|
|
277
316
|
if (frame.payload.usage) {
|
|
278
317
|
const usageChunk = {
|
|
279
318
|
id: `chatcmpl-${frame.id}`,
|
|
@@ -310,7 +349,6 @@ export class ModelProxy {
|
|
|
310
349
|
);
|
|
311
350
|
}
|
|
312
351
|
} catch {
|
|
313
|
-
// controller may be closed
|
|
314
352
|
clearTimeout(pending.timer);
|
|
315
353
|
this.pending.delete(frame.id);
|
|
316
354
|
}
|
|
@@ -320,7 +358,6 @@ export class ModelProxy {
|
|
|
320
358
|
async handleModelRequest(frame: ModelRequest): Promise<void> {
|
|
321
359
|
const { id, from, payload } = frame;
|
|
322
360
|
|
|
323
|
-
// Find the model in our local config
|
|
324
361
|
const model = this.config.models.find((m) => m.id === payload.model);
|
|
325
362
|
if (!model) {
|
|
326
363
|
this.peerManager.sendTo(from, {
|
|
@@ -335,9 +372,6 @@ export class ModelProxy {
|
|
|
335
372
|
}
|
|
336
373
|
|
|
337
374
|
try {
|
|
338
|
-
// Use OpenClaw's API to run the model locally
|
|
339
|
-
// This is done by calling the local gateway's /v1/chat/completions endpoint
|
|
340
|
-
// The local OpenClaw gateway handles provider routing
|
|
341
375
|
const localUrl = `http://127.0.0.1:${process.env.OPENCLAW_GATEWAY_PORT ?? 3000}/v1/chat/completions`;
|
|
342
376
|
|
|
343
377
|
const response = await fetch(localUrl, {
|
|
@@ -353,7 +387,6 @@ export class ModelProxy {
|
|
|
353
387
|
});
|
|
354
388
|
|
|
355
389
|
if (payload.stream) {
|
|
356
|
-
// Read SSE stream and forward as model_stream frames
|
|
357
390
|
const reader = response.body?.getReader();
|
|
358
391
|
if (!reader) throw new Error("No response body");
|
|
359
392
|
|
package/src/peer-manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type
|
|
2
|
+
import { createServer, type IncomingMessage, type Server } from "node:http";
|
|
3
|
+
import { WebSocketServer, WebSocket as WsWebSocket } from "ws";
|
|
3
4
|
import type { ClawMatrixConfig, PeerConfig } from "./config.ts";
|
|
4
5
|
import { Connection } from "./connection.ts";
|
|
5
6
|
import type { WsTransport } from "./connection.ts";
|
|
@@ -25,12 +26,13 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
25
26
|
readonly router: Router;
|
|
26
27
|
private config: ClawMatrixConfig;
|
|
27
28
|
private localCapabilities: NodeCapabilities;
|
|
28
|
-
private
|
|
29
|
+
private httpServer: Server | null = null;
|
|
30
|
+
private wss: WebSocketServer | null = null;
|
|
29
31
|
private reconnectTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
30
32
|
private reconnectAttempts = new Map<string, number>();
|
|
31
33
|
private stopped = false;
|
|
32
|
-
/** Map from
|
|
33
|
-
private inboundConnections = new Map<
|
|
34
|
+
/** Map from ws WebSocket to Connection for inbound connections. */
|
|
35
|
+
private inboundConnections = new Map<WsWebSocket, Connection>();
|
|
34
36
|
|
|
35
37
|
constructor(config: ClawMatrixConfig) {
|
|
36
38
|
super();
|
|
@@ -76,50 +78,52 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
76
78
|
conn.close(1000, "shutdown");
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
if (this.
|
|
80
|
-
this.
|
|
81
|
-
this.
|
|
81
|
+
if (this.wss) {
|
|
82
|
+
this.wss.close();
|
|
83
|
+
this.wss = null;
|
|
84
|
+
}
|
|
85
|
+
if (this.httpServer) {
|
|
86
|
+
this.httpServer.close();
|
|
87
|
+
this.httpServer = null;
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
// ── Inbound WS server (
|
|
91
|
+
// ── Inbound WS server (node:http + ws) ──────────────────────────
|
|
86
92
|
private startListening() {
|
|
87
93
|
const port = this.config.listenPort;
|
|
88
94
|
const hostname = this.config.listenHost;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
|
|
96
|
+
this.httpServer = createServer((_req, res) => {
|
|
97
|
+
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
98
|
+
res.end("WebSocket upgrade required");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
102
|
+
|
|
103
|
+
this.wss.on("connection", (ws) => {
|
|
104
|
+
this.handleInboundOpen(ws);
|
|
105
|
+
|
|
106
|
+
ws.on("message", (data) => {
|
|
107
|
+
const conn = this.inboundConnections.get(ws);
|
|
108
|
+
if (conn) {
|
|
109
|
+
conn.feedMessage(typeof data === "string" ? data : String(data));
|
|
97
110
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
conn.feedMessage(typeof message === "string" ? message : Buffer.from(message));
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
close(ws, code, reason) {
|
|
111
|
-
const conn = self.inboundConnections.get(ws);
|
|
112
|
-
if (conn) {
|
|
113
|
-
conn.feedClose(code, reason);
|
|
114
|
-
self.inboundConnections.delete(ws);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
ws.on("close", (code, reason) => {
|
|
114
|
+
const conn = this.inboundConnections.get(ws);
|
|
115
|
+
if (conn) {
|
|
116
|
+
conn.feedClose(code, reason.toString());
|
|
117
|
+
this.inboundConnections.delete(ws);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
118
120
|
});
|
|
121
|
+
|
|
122
|
+
this.httpServer.listen(port, hostname);
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
private handleInboundOpen(ws:
|
|
122
|
-
// Wrap
|
|
125
|
+
private handleInboundOpen(ws: WsWebSocket) {
|
|
126
|
+
// Wrap ws WebSocket into our WsTransport interface
|
|
123
127
|
const transport: WsTransport = {
|
|
124
128
|
send(data: string) {
|
|
125
129
|
ws.send(data);
|
package/src/tool-proxy.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
2
2
|
import { join, resolve, normalize } from "node:path";
|
|
3
3
|
import type { PeerManager } from "./peer-manager.ts";
|
|
4
4
|
import type { ClawMatrixConfig, ToolProxyConfig } from "./config.ts";
|
|
5
|
+
import { spawnProcess, readFileText, writeFileText } from "./compat.ts";
|
|
5
6
|
import type { ToolProxyRequest, ToolProxyResponse } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
const TOOL_TIMEOUT = 30_000;
|
|
@@ -198,7 +199,7 @@ export class ToolProxy {
|
|
|
198
199
|
const cwd = (params.cwd as string) ?? process.cwd();
|
|
199
200
|
const timeout = (params.timeout as number) ?? TOOL_TIMEOUT;
|
|
200
201
|
|
|
201
|
-
const proc =
|
|
202
|
+
const proc = spawnProcess(["sh", "-c", command], {
|
|
202
203
|
cwd,
|
|
203
204
|
stdout: "pipe",
|
|
204
205
|
stderr: "pipe",
|
|
@@ -207,8 +208,8 @@ export class ToolProxy {
|
|
|
207
208
|
const timeoutId = setTimeout(() => proc.kill(), timeout);
|
|
208
209
|
|
|
209
210
|
const [stdout, stderr] = await Promise.all([
|
|
210
|
-
new Response(proc.stdout).text(),
|
|
211
|
-
new Response(proc.stderr).text(),
|
|
211
|
+
proc.stdout ? new Response(proc.stdout).text() : "",
|
|
212
|
+
proc.stderr ? new Response(proc.stderr).text() : "",
|
|
212
213
|
]);
|
|
213
214
|
const exitCode = await proc.exited;
|
|
214
215
|
clearTimeout(timeoutId);
|
|
@@ -229,8 +230,7 @@ export class ToolProxy {
|
|
|
229
230
|
if (!path) throw new Error("Missing path");
|
|
230
231
|
this.validatePath(path, tpConfig);
|
|
231
232
|
|
|
232
|
-
const
|
|
233
|
-
const content = await file.text();
|
|
233
|
+
const content = await readFileText(path);
|
|
234
234
|
return {
|
|
235
235
|
content: content.slice(0, tpConfig.maxOutputBytes),
|
|
236
236
|
};
|
|
@@ -245,7 +245,7 @@ export class ToolProxy {
|
|
|
245
245
|
if (!path || content === undefined) throw new Error("Missing path or content");
|
|
246
246
|
this.validatePath(path, tpConfig);
|
|
247
247
|
|
|
248
|
-
await
|
|
248
|
+
await writeFileText(path, content);
|
|
249
249
|
return { success: true };
|
|
250
250
|
}
|
|
251
251
|
|