clawmatrix 0.1.20 → 0.1.22
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 +1 -1
- package/src/cli.ts +12 -6
- package/src/cluster-service.ts +2 -1
- package/src/config.ts +1 -0
- package/src/debug.ts +1 -1
- package/src/index.ts +26 -18
- package/src/knowledge-sync.ts +93 -45
- package/src/model-proxy.ts +442 -210
- package/src/router.ts +10 -0
- package/src/tool-proxy.ts +13 -1
- package/src/tools/cluster-peers.ts +1 -1
- package/src/types.ts +3 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -95,7 +95,9 @@ export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
|
95
95
|
models: Array<{ id: string }>;
|
|
96
96
|
tags: string[];
|
|
97
97
|
connected: boolean;
|
|
98
|
+
status: "direct" | "relay" | "unreachable";
|
|
98
99
|
latencyMs: number;
|
|
100
|
+
reachableVia: string | null;
|
|
99
101
|
}>;
|
|
100
102
|
|
|
101
103
|
if (!peers || peers.length === 0) {
|
|
@@ -105,9 +107,9 @@ export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
const
|
|
109
|
-
const countStr = `${
|
|
110
|
-
const countColor =
|
|
110
|
+
const reachable = peers.filter((p) => p.connected).length;
|
|
111
|
+
const countStr = `${reachable}/${peers.length} reachable`;
|
|
112
|
+
const countColor = reachable === peers.length ? green : reachable > 0 ? yellow : red;
|
|
111
113
|
|
|
112
114
|
console.log(` ${bar}`);
|
|
113
115
|
console.log(` ${cyan("◆")} ${bold("Peers")} ${countColor(countStr)}`);
|
|
@@ -115,10 +117,14 @@ export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
|
115
117
|
|
|
116
118
|
for (let i = 0; i < peers.length; i++) {
|
|
117
119
|
const peer = peers[i];
|
|
118
|
-
const dot = peer.
|
|
120
|
+
const dot = peer.status === "direct" ? green("●") : peer.status === "relay" ? yellow("●") : red("○");
|
|
119
121
|
const latency = peer.connected && peer.latencyMs > 0 ? dim(` ${peer.latencyMs}ms`) : "";
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
+
const statusLabel = peer.status === "relay"
|
|
123
|
+
? yellow(` relay via ${peer.reachableVia}`)
|
|
124
|
+
: peer.status === "unreachable"
|
|
125
|
+
? red(" unreachable")
|
|
126
|
+
: "";
|
|
127
|
+
console.log(` ${bar} ${dot} ${bold(peer.nodeId)}${statusLabel}${latency}`);
|
|
122
128
|
|
|
123
129
|
if (peer.tags.length > 0) {
|
|
124
130
|
console.log(` ${bar} ${lbl("Tags")}${peer.tags.join(dim(", "))}`);
|
package/src/cluster-service.ts
CHANGED
|
@@ -65,7 +65,7 @@ export class ClusterRuntime {
|
|
|
65
65
|
this.peerManager = new PeerManager(config, openclawVersion);
|
|
66
66
|
this.handoffManager = new HandoffManager(config, this.peerManager);
|
|
67
67
|
this.modelProxy = new ModelProxy(config, this.peerManager, gatewayInfo, openclawConfig);
|
|
68
|
-
this.toolProxy = new ToolProxy(config, this.peerManager, gatewayInfo);
|
|
68
|
+
this.toolProxy = new ToolProxy(config, this.peerManager, gatewayInfo, logger);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
start() {
|
|
@@ -101,6 +101,7 @@ export class ClusterRuntime {
|
|
|
101
101
|
storePath: path.join(stateDir, "knowledge.automerge"),
|
|
102
102
|
nodeId: this.config.nodeId,
|
|
103
103
|
debounce: this.config.knowledge.debounce ?? 5000,
|
|
104
|
+
maxFileSize: this.config.knowledge.maxFileSize ?? 512 * 1024,
|
|
104
105
|
peerManager: this.peerManager,
|
|
105
106
|
});
|
|
106
107
|
this.knowledgeSync.start().then(() => {
|
package/src/config.ts
CHANGED
|
@@ -80,6 +80,7 @@ const ProxyModelGroupSchema = z.object({
|
|
|
80
80
|
const KnowledgeConfigSchema = z.object({
|
|
81
81
|
enabled: z.boolean().default(false),
|
|
82
82
|
debounce: z.number().default(5000),
|
|
83
|
+
maxFileSize: z.number().default(512 * 1024),
|
|
83
84
|
}).optional();
|
|
84
85
|
|
|
85
86
|
const WebConfigSchema = z.object({
|
package/src/debug.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -120,15 +120,19 @@ const plugin = {
|
|
|
120
120
|
agents: config.agents.map((a) => ({ id: a.id, description: a.description })),
|
|
121
121
|
models: config.models.map((m) => ({ id: m.id })),
|
|
122
122
|
tags: config.tags,
|
|
123
|
-
peers: peers.map((p) =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
peers: peers.map((p) => {
|
|
124
|
+
const status = runtime.peerManager.router.getPeerStatus(p);
|
|
125
|
+
return {
|
|
126
|
+
nodeId: p.nodeId,
|
|
127
|
+
agents: p.agents,
|
|
128
|
+
models: p.models,
|
|
129
|
+
tags: p.tags,
|
|
130
|
+
connected: status !== "unreachable",
|
|
131
|
+
status,
|
|
132
|
+
reachableVia: p.reachableVia,
|
|
133
|
+
latencyMs: p.latencyMs,
|
|
134
|
+
};
|
|
135
|
+
}),
|
|
132
136
|
});
|
|
133
137
|
} catch {
|
|
134
138
|
respond(false, { error: "ClawMatrix service not running" });
|
|
@@ -141,15 +145,19 @@ const plugin = {
|
|
|
141
145
|
({ respond }: GatewayRequestHandlerOptions) => {
|
|
142
146
|
try {
|
|
143
147
|
const runtime = getClusterRuntime();
|
|
144
|
-
const peers = runtime.peerManager.router.getAllPeers().map((p) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
const peers = runtime.peerManager.router.getAllPeers().map((p) => {
|
|
149
|
+
const status = runtime.peerManager.router.getPeerStatus(p);
|
|
150
|
+
return {
|
|
151
|
+
nodeId: p.nodeId,
|
|
152
|
+
agents: p.agents,
|
|
153
|
+
models: p.models,
|
|
154
|
+
tags: p.tags,
|
|
155
|
+
connected: status !== "unreachable",
|
|
156
|
+
status,
|
|
157
|
+
reachableVia: p.reachableVia,
|
|
158
|
+
latencyMs: p.latencyMs,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
153
161
|
respond(true, peers);
|
|
154
162
|
} catch {
|
|
155
163
|
respond(true, []);
|
package/src/knowledge-sync.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Automerge from "@automerge/automerge";
|
|
2
2
|
import { watch, type FSWatcher } from "node:fs";
|
|
3
|
-
import { readdir, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { readdir, readFile, stat as fsStat, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import ignore, { type Ignore } from "ignore";
|
|
6
6
|
|
|
@@ -23,18 +23,33 @@ export interface KnowledgeSyncOptions {
|
|
|
23
23
|
nodeId: string;
|
|
24
24
|
/** Debounce interval in ms for fs changes. */
|
|
25
25
|
debounce: number;
|
|
26
|
+
/** Max file size in bytes. Files larger than this are skipped. */
|
|
27
|
+
maxFileSize: number;
|
|
26
28
|
/** PeerManager for sending frames. */
|
|
27
29
|
peerManager: PeerManager;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const TAG = "knowledge";
|
|
31
33
|
|
|
34
|
+
async function streamToString(stream: ReadableStream | null): Promise<string> {
|
|
35
|
+
if (!stream) return "";
|
|
36
|
+
const reader = stream.getReader();
|
|
37
|
+
const chunks: Uint8Array[] = [];
|
|
38
|
+
for (;;) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done) break;
|
|
41
|
+
chunks.push(value);
|
|
42
|
+
}
|
|
43
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
export class KnowledgeSync {
|
|
33
47
|
private doc: Automerge.Doc<KnowledgeDoc>;
|
|
34
48
|
private syncStates = new Map<string, Automerge.SyncState>();
|
|
35
49
|
private watcher: FSWatcher | null = null;
|
|
36
50
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
37
|
-
|
|
51
|
+
/** Paths currently being written by exportToFs — suppressed from fs watcher. */
|
|
52
|
+
private writingPaths = new Set<string>();
|
|
38
53
|
private opts: KnowledgeSyncOptions;
|
|
39
54
|
private ig: Ignore = ignore();
|
|
40
55
|
|
|
@@ -74,7 +89,9 @@ export class KnowledgeSync {
|
|
|
74
89
|
|
|
75
90
|
// Start watching for file changes
|
|
76
91
|
this.watcher = watch(this.opts.workspacePath, { recursive: true }, (_event, filename) => {
|
|
77
|
-
if (
|
|
92
|
+
if (!filename) return;
|
|
93
|
+
// Ignore files currently being written by export
|
|
94
|
+
if (this.writingPaths.has(filename)) return;
|
|
78
95
|
// Ignore hidden files
|
|
79
96
|
if (filename.startsWith(".")) return;
|
|
80
97
|
// Ignore gitignored files
|
|
@@ -193,8 +210,11 @@ export class KnowledgeSync {
|
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
for (const relPath of Object.keys(docFiles)) {
|
|
213
|
+
// Only treat as deleted if the file is genuinely gone, not just skipped by size limit
|
|
196
214
|
if (!(relPath in currentFiles)) {
|
|
197
|
-
|
|
215
|
+
const absPath = path.join(this.opts.workspacePath, relPath);
|
|
216
|
+
const exists = await fsStat(absPath).then(() => true, () => false);
|
|
217
|
+
if (!exists) deleted.push(relPath);
|
|
198
218
|
}
|
|
199
219
|
}
|
|
200
220
|
|
|
@@ -208,19 +228,14 @@ export class KnowledgeSync {
|
|
|
208
228
|
(deleted.length > 0 ? ` deleted=[${deleted.join(",")}]` : ""),
|
|
209
229
|
);
|
|
210
230
|
|
|
211
|
-
// Update automerge doc
|
|
231
|
+
// Update automerge doc — only write dirty files to minimize change history
|
|
212
232
|
this.doc = Automerge.change(this.doc, (d) => {
|
|
213
233
|
if (!d.files) {
|
|
214
234
|
(d as KnowledgeDoc).files = {};
|
|
215
235
|
}
|
|
216
|
-
for (const [
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (const relPath of Object.keys(d.files)) {
|
|
220
|
-
if (!(relPath in currentFiles)) {
|
|
221
|
-
delete d.files[relPath];
|
|
222
|
-
}
|
|
223
|
-
}
|
|
236
|
+
for (const p of added) d.files[p] = currentFiles[p];
|
|
237
|
+
for (const p of modified) d.files[p] = currentFiles[p];
|
|
238
|
+
for (const p of deleted) delete d.files[p];
|
|
224
239
|
});
|
|
225
240
|
|
|
226
241
|
await this.saveDoc();
|
|
@@ -286,47 +301,47 @@ export class KnowledgeSync {
|
|
|
286
301
|
await this.walkDir(base, relPath, result);
|
|
287
302
|
} else if (entry.isFile()) {
|
|
288
303
|
if (this.isIgnored(relPath)) continue;
|
|
289
|
-
const
|
|
304
|
+
const filePath = path.join(base, relPath);
|
|
305
|
+
const st = await fsStat(filePath);
|
|
306
|
+
if (st.size > this.opts.maxFileSize) continue;
|
|
307
|
+
const content = await readFile(filePath, "utf-8");
|
|
290
308
|
result[relPath] = content;
|
|
291
309
|
}
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
312
|
|
|
295
313
|
private async exportToFs() {
|
|
296
|
-
this.
|
|
297
|
-
|
|
298
|
-
const docFiles = this.doc.files ?? {};
|
|
299
|
-
const currentFiles = await this.readWorkspaceFiles();
|
|
300
|
-
|
|
301
|
-
let written = 0;
|
|
314
|
+
const docFiles = this.doc.files ?? {};
|
|
315
|
+
const currentFiles = await this.readWorkspaceFiles();
|
|
302
316
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
317
|
+
let written = 0;
|
|
318
|
+
|
|
319
|
+
for (const [relPath, content] of Object.entries(docFiles)) {
|
|
320
|
+
// Don't export files that would be gitignored
|
|
321
|
+
if (this.isIgnored(relPath)) continue;
|
|
322
|
+
if (currentFiles[relPath] !== content) {
|
|
323
|
+
this.writingPaths.add(relPath);
|
|
324
|
+
const absPath = path.join(this.opts.workspacePath, relPath);
|
|
325
|
+
await mkdir(path.dirname(absPath), { recursive: true });
|
|
326
|
+
await writeFile(absPath, content, "utf-8");
|
|
327
|
+
setTimeout(() => this.writingPaths.delete(relPath), 500);
|
|
328
|
+
written++;
|
|
312
329
|
}
|
|
330
|
+
}
|
|
313
331
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
332
|
+
let removed = 0;
|
|
333
|
+
for (const relPath of Object.keys(currentFiles)) {
|
|
334
|
+
if (!(relPath in docFiles)) {
|
|
335
|
+
this.writingPaths.add(relPath);
|
|
336
|
+
const absPath = path.join(this.opts.workspacePath, relPath);
|
|
337
|
+
await unlink(absPath).catch(() => {});
|
|
338
|
+
setTimeout(() => this.writingPaths.delete(relPath), 500);
|
|
339
|
+
removed++;
|
|
321
340
|
}
|
|
341
|
+
}
|
|
322
342
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
} finally {
|
|
327
|
-
setTimeout(() => {
|
|
328
|
-
this.suppressWatch = false;
|
|
329
|
-
}, 500);
|
|
343
|
+
if (written > 0 || removed > 0) {
|
|
344
|
+
debug(TAG, `exported to filesystem: ${written} written, ${removed} removed`);
|
|
330
345
|
}
|
|
331
346
|
}
|
|
332
347
|
|
|
@@ -353,6 +368,34 @@ export class KnowledgeSync {
|
|
|
353
368
|
} else {
|
|
354
369
|
debug(TAG, "git repo already initialized");
|
|
355
370
|
}
|
|
371
|
+
|
|
372
|
+
// Ensure local git user config exists (so commits don't fail on unconfigured machines)
|
|
373
|
+
const nameCheck = spawnProcess(["git", "config", "user.name"], {
|
|
374
|
+
cwd: this.opts.workspacePath,
|
|
375
|
+
stdout: "pipe",
|
|
376
|
+
stderr: "pipe",
|
|
377
|
+
});
|
|
378
|
+
if ((await nameCheck.exited) !== 0) {
|
|
379
|
+
const setName = spawnProcess(["git", "config", "user.name", "clawmatrix"], {
|
|
380
|
+
cwd: this.opts.workspacePath,
|
|
381
|
+
stdout: "pipe",
|
|
382
|
+
stderr: "pipe",
|
|
383
|
+
});
|
|
384
|
+
await setName.exited;
|
|
385
|
+
}
|
|
386
|
+
const emailCheck = spawnProcess(["git", "config", "user.email"], {
|
|
387
|
+
cwd: this.opts.workspacePath,
|
|
388
|
+
stdout: "pipe",
|
|
389
|
+
stderr: "pipe",
|
|
390
|
+
});
|
|
391
|
+
if ((await emailCheck.exited) !== 0) {
|
|
392
|
+
const setEmail = spawnProcess(["git", "config", "user.email", "clawmatrix@local"], {
|
|
393
|
+
cwd: this.opts.workspacePath,
|
|
394
|
+
stdout: "pipe",
|
|
395
|
+
stderr: "pipe",
|
|
396
|
+
});
|
|
397
|
+
await setEmail.exited;
|
|
398
|
+
}
|
|
356
399
|
} catch {
|
|
357
400
|
debug(TAG, "git not available, skipping git integration");
|
|
358
401
|
}
|
|
@@ -386,8 +429,13 @@ export class KnowledgeSync {
|
|
|
386
429
|
stderr: "pipe",
|
|
387
430
|
},
|
|
388
431
|
);
|
|
389
|
-
await commit.exited;
|
|
390
|
-
|
|
432
|
+
const commitCode = await commit.exited;
|
|
433
|
+
if (commitCode !== 0) {
|
|
434
|
+
const stderr = await streamToString(commit.stderr);
|
|
435
|
+
debug(TAG, `git commit failed (exit ${commitCode}): ${stderr}`);
|
|
436
|
+
} else {
|
|
437
|
+
debug(TAG, `git commit: ${message}`);
|
|
438
|
+
}
|
|
391
439
|
} catch (err) {
|
|
392
440
|
debug(TAG, `git commit failed: ${err}`);
|
|
393
441
|
}
|