clawmatrix 0.3.1 → 0.4.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/cli/bin/clawmatrix.mjs +1006 -0
- package/cli/package.json +27 -0
- package/cli/skills/clawmatrix/SKILL.md +104 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -1
- package/src/acp-proxy.ts +416 -31
- package/src/cluster-service.ts +72 -2
- package/src/health-tracker.ts +40 -11
- package/src/index.ts +471 -28
- package/src/knowledge-sync.ts +18 -4
- package/src/model-proxy.ts +5 -0
- package/src/peer-manager.ts +33 -4
- package/src/tool-proxy.ts +40 -2
- package/src/tools/cluster-notify.ts +132 -0
- package/src/types.ts +1 -1
- package/src/cli.ts +0 -711
package/src/tool-proxy.ts
CHANGED
|
@@ -10,6 +10,9 @@ import type {
|
|
|
10
10
|
} from "./types.ts";
|
|
11
11
|
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
12
12
|
import { isLocalTool, executeLocally } from "./local-tools.ts";
|
|
13
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
13
16
|
|
|
14
17
|
const DEFAULT_TOOL_TIMEOUT = 30_000;
|
|
15
18
|
|
|
@@ -136,13 +139,41 @@ export class ToolProxy {
|
|
|
136
139
|
|
|
137
140
|
if (frame.payload.success && frame.payload.result) {
|
|
138
141
|
this.logger.info(`[clawmatrix] Tool response: id=${frame.id} from="${frame.from}" success`);
|
|
139
|
-
|
|
142
|
+
const result = this.extractInlineImage(frame.payload.result);
|
|
143
|
+
pending.resolve(result);
|
|
140
144
|
} else {
|
|
141
145
|
this.logger.warn(`[clawmatrix] Tool response: id=${frame.id} from="${frame.from}" failed: ${frame.payload.error}`);
|
|
142
146
|
pending.reject(new Error(frame.payload.error ?? "Remote tool execution failed"));
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
/**
|
|
151
|
+
* If the tool result contains inline base64 image data (mime: "image/*" + data),
|
|
152
|
+
* save it to a local temp file and replace `data` with `localPath`.
|
|
153
|
+
* This avoids flooding the LLM context with base64 text (saves ~tens of thousands of tokens).
|
|
154
|
+
*/
|
|
155
|
+
private extractInlineImage(result: Record<string, unknown>): Record<string, unknown> {
|
|
156
|
+
const mime = result.mime;
|
|
157
|
+
const data = result.data;
|
|
158
|
+
if (typeof mime !== "string" || !mime.startsWith("image/") || typeof data !== "string") {
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const ext = mime === "image/png" ? ".png" : mime === "image/webp" ? ".webp" : ".jpg";
|
|
164
|
+
const dir = join(tmpdir(), "clawmatrix-images");
|
|
165
|
+
mkdirSync(dir, { recursive: true });
|
|
166
|
+
const localPath = join(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`);
|
|
167
|
+
writeFileSync(localPath, Buffer.from(data, "base64"));
|
|
168
|
+
this.logger.info(`[clawmatrix] Saved inline image (${(data.length * 0.75 / 1024).toFixed(0)}KB) to ${localPath}`);
|
|
169
|
+
const { data: _stripped, ...rest } = result;
|
|
170
|
+
return { ...rest, localPath };
|
|
171
|
+
} catch (err) {
|
|
172
|
+
this.logger.warn(`[clawmatrix] Failed to extract inline image: ${err}`);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
146
177
|
// ── Incoming request: execute via local Gateway ────────────────
|
|
147
178
|
async handleRequest(frame: ToolProxyRequest): Promise<void> {
|
|
148
179
|
const { id, from, payload } = frame;
|
|
@@ -288,7 +319,14 @@ export class ToolProxy {
|
|
|
288
319
|
|
|
289
320
|
clearTimeout(pending.timer);
|
|
290
321
|
this.pendingBatch.delete(frame.id);
|
|
291
|
-
|
|
322
|
+
// Extract inline images from batch results
|
|
323
|
+
const results = frame.payload.results.map((item) => {
|
|
324
|
+
if (item.success && item.result) {
|
|
325
|
+
return { ...item, result: this.extractInlineImage(item.result) };
|
|
326
|
+
}
|
|
327
|
+
return item;
|
|
328
|
+
});
|
|
329
|
+
pending.resolve(results);
|
|
292
330
|
}
|
|
293
331
|
|
|
294
332
|
// ── Incoming batch request: execute sequentially via local Gateway ──
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getClusterRuntime } from "../cluster-service.ts";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export function createClusterNotifyTool(): AnyAgentTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "cluster_notify",
|
|
8
|
+
label: "Cluster Notify",
|
|
9
|
+
description:
|
|
10
|
+
"Push a notification to mobile devices in the cluster (triggers Dynamic Island / Live Activity on iOS). " +
|
|
11
|
+
"Use this when you are about to start a long-running task so the user can track progress on their phone. " +
|
|
12
|
+
'Actions: "start" begins a new activity, "update" changes detail/progress, "end" dismisses it.',
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
action: {
|
|
17
|
+
type: "string",
|
|
18
|
+
enum: ["start", "update", "end"],
|
|
19
|
+
description: 'Action to perform. Default: "start"',
|
|
20
|
+
},
|
|
21
|
+
taskId: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Task ID for update/end actions. Returned by start action. Omit for start to auto-generate.",
|
|
24
|
+
},
|
|
25
|
+
title: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Activity title (shown in Dynamic Island and lock screen)",
|
|
28
|
+
},
|
|
29
|
+
detail: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Activity detail text (e.g. current step description)",
|
|
32
|
+
},
|
|
33
|
+
progress: {
|
|
34
|
+
type: "number",
|
|
35
|
+
description: "Progress value 0.0 to 1.0 (optional, shows progress bar when provided)",
|
|
36
|
+
},
|
|
37
|
+
tool: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Current tool being executed (shown as a tag in the activity)",
|
|
40
|
+
},
|
|
41
|
+
success: {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
description: 'For "end" action: true for completed, false for failed. Default: true',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: [],
|
|
47
|
+
},
|
|
48
|
+
async execute(_toolCallId, params) {
|
|
49
|
+
const {
|
|
50
|
+
action = "start",
|
|
51
|
+
taskId: providedTaskId,
|
|
52
|
+
title,
|
|
53
|
+
detail,
|
|
54
|
+
progress,
|
|
55
|
+
tool,
|
|
56
|
+
success = true,
|
|
57
|
+
} = params as {
|
|
58
|
+
action?: "start" | "update" | "end";
|
|
59
|
+
taskId?: string;
|
|
60
|
+
title?: string;
|
|
61
|
+
detail?: string;
|
|
62
|
+
progress?: number;
|
|
63
|
+
tool?: string;
|
|
64
|
+
success?: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const runtime = getClusterRuntime();
|
|
69
|
+
const peers = runtime.peerManager.router.getAllPeers();
|
|
70
|
+
const mobileTargets = peers.filter((p) =>
|
|
71
|
+
p.tags.some((t) => t === "mobile" || t === "ios" || t === "phone"),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (mobileTargets.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text" as const, text: "No mobile peers connected" }],
|
|
77
|
+
details: { error: true },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const taskId = providedTaskId || randomUUID();
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
|
|
84
|
+
let status: string;
|
|
85
|
+
if (action === "start") status = "started";
|
|
86
|
+
else if (action === "end") status = success ? "completed" : "failed";
|
|
87
|
+
else status = "progress";
|
|
88
|
+
|
|
89
|
+
const frame = {
|
|
90
|
+
type: "task_activity" as const,
|
|
91
|
+
id: randomUUID(),
|
|
92
|
+
from: runtime.config.nodeId,
|
|
93
|
+
timestamp: now,
|
|
94
|
+
payload: {
|
|
95
|
+
taskId,
|
|
96
|
+
taskType: "notify" as const,
|
|
97
|
+
status,
|
|
98
|
+
agent: title || runtime.config.nodeId,
|
|
99
|
+
nodeId: runtime.config.nodeId,
|
|
100
|
+
title: title || runtime.config.nodeId,
|
|
101
|
+
detail: detail || (action === "end" ? (success ? "已完成" : "失败") : undefined),
|
|
102
|
+
startedAt: now,
|
|
103
|
+
elapsedMs: 0,
|
|
104
|
+
progress,
|
|
105
|
+
tool,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
for (const target of mobileTargets) {
|
|
110
|
+
runtime.peerManager.sendTo(target.nodeId, { ...frame, to: target.nodeId });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const targetNames = mobileTargets.map((t) => t.nodeId).join(", ");
|
|
114
|
+
return {
|
|
115
|
+
content: [{
|
|
116
|
+
type: "text" as const,
|
|
117
|
+
text: `Notification ${action}ed → ${targetNames} (taskId: ${taskId})`,
|
|
118
|
+
}],
|
|
119
|
+
details: { taskId, action, targets: mobileTargets.length },
|
|
120
|
+
};
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: "text" as const,
|
|
125
|
+
text: `Notify error: ${err instanceof Error ? err.message : String(err)}`,
|
|
126
|
+
}],
|
|
127
|
+
details: { error: true },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -620,7 +620,7 @@ export interface AcpSessionInfo {
|
|
|
620
620
|
description?: string; // first user message (for display in session list)
|
|
621
621
|
updatedAt?: string;
|
|
622
622
|
agent?: string; // which ACP agent (claude, codex, etc.)
|
|
623
|
-
status?: "active" | "idle"; //
|
|
623
|
+
status?: "busy" | "active" | "idle"; // busy = currently executing a prompt, active = daemon alive, idle = on disk only
|
|
624
624
|
}
|
|
625
625
|
|
|
626
626
|
export interface AcpListRequest extends ClusterFrame {
|