multiclaws 0.3.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/README.md +36 -0
- package/README.zh-CN.md +36 -0
- package/dist/gateway/handlers.d.ts +3 -0
- package/dist/gateway/handlers.js +172 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +446 -0
- package/dist/infra/gateway-client.d.ts +27 -0
- package/dist/infra/gateway-client.js +136 -0
- package/dist/infra/json-store.d.ts +4 -0
- package/dist/infra/json-store.js +57 -0
- package/dist/infra/logger.d.ts +13 -0
- package/dist/infra/logger.js +18 -0
- package/dist/infra/rate-limiter.d.ts +19 -0
- package/dist/infra/rate-limiter.js +69 -0
- package/dist/infra/tailscale.d.ts +19 -0
- package/dist/infra/tailscale.js +120 -0
- package/dist/infra/telemetry.d.ts +3 -0
- package/dist/infra/telemetry.js +17 -0
- package/dist/service/a2a-adapter.d.ts +44 -0
- package/dist/service/a2a-adapter.js +208 -0
- package/dist/service/agent-profile.d.ts +13 -0
- package/dist/service/agent-profile.js +38 -0
- package/dist/service/agent-registry.d.ts +26 -0
- package/dist/service/agent-registry.js +100 -0
- package/dist/service/multiclaws-service.d.ts +88 -0
- package/dist/service/multiclaws-service.js +708 -0
- package/dist/task/tracker.d.ts +43 -0
- package/dist/task/tracker.js +186 -0
- package/dist/team/team-store.d.ts +39 -0
- package/dist/team/team-store.js +146 -0
- package/dist/types/openclaw.d.ts +86 -0
- package/dist/types/openclaw.js +2 -0
- package/openclaw.plugin.json +34 -0
- package/package.json +56 -0
- package/skills/multiclaws/SKILL.md +164 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type TaskStatus = "queued" | "running" | "completed" | "failed";
|
|
2
|
+
export type TaskRecord = {
|
|
3
|
+
taskId: string;
|
|
4
|
+
fromPeerId: string;
|
|
5
|
+
toPeerId: string;
|
|
6
|
+
task: string;
|
|
7
|
+
context?: string;
|
|
8
|
+
status: TaskStatus;
|
|
9
|
+
createdAtMs: number;
|
|
10
|
+
updatedAtMs: number;
|
|
11
|
+
result?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class TaskTracker {
|
|
15
|
+
private readonly filePath;
|
|
16
|
+
private readonly ttlMs;
|
|
17
|
+
private readonly maxTasks;
|
|
18
|
+
private readonly store;
|
|
19
|
+
private pruneTimer;
|
|
20
|
+
private persistPending;
|
|
21
|
+
constructor(opts?: {
|
|
22
|
+
ttlMs?: number;
|
|
23
|
+
maxTasks?: number;
|
|
24
|
+
filePath?: string;
|
|
25
|
+
});
|
|
26
|
+
create(params: {
|
|
27
|
+
fromPeerId: string;
|
|
28
|
+
toPeerId: string;
|
|
29
|
+
task: string;
|
|
30
|
+
context?: string;
|
|
31
|
+
}): TaskRecord;
|
|
32
|
+
update(taskId: string, patch: Partial<Omit<TaskRecord, "taskId" | "createdAtMs">>): TaskRecord | null;
|
|
33
|
+
get(taskId: string): TaskRecord | null;
|
|
34
|
+
list(): TaskRecord[];
|
|
35
|
+
destroy(): void;
|
|
36
|
+
/** Sync load at startup — runs once before the event loop is busy. */
|
|
37
|
+
private loadStoreSync;
|
|
38
|
+
/** Coalesce rapid writes into a single async flush. */
|
|
39
|
+
private schedulePersist;
|
|
40
|
+
private persistAsync;
|
|
41
|
+
private prune;
|
|
42
|
+
private evictOldest;
|
|
43
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TaskTracker = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const MAX_TASKS = 10_000;
|
|
13
|
+
const PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
14
|
+
function emptyStore() {
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
tasks: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function normalizeTask(task) {
|
|
21
|
+
if (!task ||
|
|
22
|
+
typeof task.taskId !== "string" ||
|
|
23
|
+
typeof task.fromPeerId !== "string" ||
|
|
24
|
+
typeof task.toPeerId !== "string" ||
|
|
25
|
+
typeof task.task !== "string" ||
|
|
26
|
+
typeof task.status !== "string" ||
|
|
27
|
+
typeof task.createdAtMs !== "number" ||
|
|
28
|
+
typeof task.updatedAtMs !== "number") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
taskId: task.taskId,
|
|
33
|
+
fromPeerId: task.fromPeerId,
|
|
34
|
+
toPeerId: task.toPeerId,
|
|
35
|
+
task: task.task,
|
|
36
|
+
context: typeof task.context === "string" ? task.context : undefined,
|
|
37
|
+
status: task.status,
|
|
38
|
+
createdAtMs: task.createdAtMs,
|
|
39
|
+
updatedAtMs: task.updatedAtMs,
|
|
40
|
+
result: typeof task.result === "string" ? task.result : undefined,
|
|
41
|
+
error: typeof task.error === "string" ? task.error : undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function normalizeStore(raw) {
|
|
45
|
+
if (raw.version !== 1 || !Array.isArray(raw.tasks)) {
|
|
46
|
+
return emptyStore();
|
|
47
|
+
}
|
|
48
|
+
const tasks = [];
|
|
49
|
+
for (const task of raw.tasks) {
|
|
50
|
+
const normalized = normalizeTask(task);
|
|
51
|
+
if (normalized) {
|
|
52
|
+
tasks.push(normalized);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
version: 1,
|
|
57
|
+
tasks,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
class TaskTracker {
|
|
61
|
+
filePath;
|
|
62
|
+
ttlMs;
|
|
63
|
+
maxTasks;
|
|
64
|
+
store;
|
|
65
|
+
pruneTimer = null;
|
|
66
|
+
persistPending = false;
|
|
67
|
+
constructor(opts) {
|
|
68
|
+
this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
|
|
69
|
+
this.maxTasks = opts?.maxTasks ?? MAX_TASKS;
|
|
70
|
+
this.filePath = opts?.filePath ?? ".openclaw/multiclaws/tasks.json";
|
|
71
|
+
// Sync load at startup is acceptable (runs once)
|
|
72
|
+
this.store = this.loadStoreSync();
|
|
73
|
+
this.pruneTimer = setInterval(() => this.prune(), PRUNE_INTERVAL_MS);
|
|
74
|
+
if (this.pruneTimer && typeof this.pruneTimer === "object" && "unref" in this.pruneTimer) {
|
|
75
|
+
this.pruneTimer.unref();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
create(params) {
|
|
79
|
+
if (this.store.tasks.length >= this.maxTasks) {
|
|
80
|
+
this.prune();
|
|
81
|
+
}
|
|
82
|
+
if (this.store.tasks.length >= this.maxTasks) {
|
|
83
|
+
this.evictOldest();
|
|
84
|
+
}
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const record = {
|
|
87
|
+
taskId: (0, node_crypto_1.randomUUID)(),
|
|
88
|
+
fromPeerId: params.fromPeerId,
|
|
89
|
+
toPeerId: params.toPeerId,
|
|
90
|
+
task: params.task,
|
|
91
|
+
context: params.context,
|
|
92
|
+
status: "queued",
|
|
93
|
+
createdAtMs: now,
|
|
94
|
+
updatedAtMs: now,
|
|
95
|
+
};
|
|
96
|
+
this.store.tasks.push(record);
|
|
97
|
+
this.schedulePersist();
|
|
98
|
+
return record;
|
|
99
|
+
}
|
|
100
|
+
update(taskId, patch) {
|
|
101
|
+
const index = this.store.tasks.findIndex((entry) => entry.taskId === taskId);
|
|
102
|
+
if (index < 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const next = {
|
|
106
|
+
...this.store.tasks[index],
|
|
107
|
+
...patch,
|
|
108
|
+
updatedAtMs: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
this.store.tasks[index] = next;
|
|
111
|
+
this.schedulePersist();
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
get(taskId) {
|
|
115
|
+
return this.store.tasks.find((entry) => entry.taskId === taskId) ?? null;
|
|
116
|
+
}
|
|
117
|
+
list() {
|
|
118
|
+
return [...this.store.tasks].sort((a, b) => b.updatedAtMs - a.updatedAtMs);
|
|
119
|
+
}
|
|
120
|
+
destroy() {
|
|
121
|
+
if (this.pruneTimer) {
|
|
122
|
+
clearInterval(this.pruneTimer);
|
|
123
|
+
this.pruneTimer = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Sync load at startup — runs once before the event loop is busy. */
|
|
127
|
+
loadStoreSync() {
|
|
128
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(this.filePath), { recursive: true });
|
|
129
|
+
try {
|
|
130
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(this.filePath, "utf8"));
|
|
131
|
+
return normalizeStore(raw);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
const store = emptyStore();
|
|
135
|
+
node_fs_1.default.writeFileSync(this.filePath, JSON.stringify(store, null, 2), "utf8");
|
|
136
|
+
return store;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Coalesce rapid writes into a single async flush. */
|
|
140
|
+
schedulePersist() {
|
|
141
|
+
if (this.persistPending)
|
|
142
|
+
return;
|
|
143
|
+
this.persistPending = true;
|
|
144
|
+
queueMicrotask(() => {
|
|
145
|
+
this.persistPending = false;
|
|
146
|
+
void this.persistAsync();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async persistAsync() {
|
|
150
|
+
try {
|
|
151
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(this.filePath), { recursive: true });
|
|
152
|
+
const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
153
|
+
await promises_1.default.writeFile(tmp, JSON.stringify(this.store, null, 2), "utf8");
|
|
154
|
+
await promises_1.default.rename(tmp, this.filePath);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// best-effort persistence — in-memory state is authoritative
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
prune() {
|
|
161
|
+
const cutoff = Date.now() - this.ttlMs;
|
|
162
|
+
const before = this.store.tasks.length;
|
|
163
|
+
this.store.tasks = this.store.tasks.filter((task) => {
|
|
164
|
+
if (task.updatedAtMs >= cutoff) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return task.status !== "completed" && task.status !== "failed";
|
|
168
|
+
});
|
|
169
|
+
if (this.store.tasks.length !== before) {
|
|
170
|
+
this.schedulePersist();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
evictOldest() {
|
|
174
|
+
const removable = [...this.store.tasks]
|
|
175
|
+
.filter((task) => task.status === "completed" || task.status === "failed")
|
|
176
|
+
.sort((a, b) => a.updatedAtMs - b.updatedAtMs)
|
|
177
|
+
.slice(0, Math.max(1, Math.floor(this.maxTasks / 4)));
|
|
178
|
+
if (removable.length === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const removeIds = new Set(removable.map((entry) => entry.taskId));
|
|
182
|
+
this.store.tasks = this.store.tasks.filter((entry) => !removeIds.has(entry.taskId));
|
|
183
|
+
this.schedulePersist();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.TaskTracker = TaskTracker;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type TeamMember = {
|
|
2
|
+
url: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
joinedAtMs: number;
|
|
6
|
+
};
|
|
7
|
+
export type TeamRecord = {
|
|
8
|
+
teamId: string;
|
|
9
|
+
teamName: string;
|
|
10
|
+
selfUrl: string;
|
|
11
|
+
members: TeamMember[];
|
|
12
|
+
createdAtMs: number;
|
|
13
|
+
};
|
|
14
|
+
export type InvitePayload = {
|
|
15
|
+
/** teamId */
|
|
16
|
+
t: string;
|
|
17
|
+
/** seed URL */
|
|
18
|
+
u: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function encodeInvite(teamId: string, seedUrl: string): string;
|
|
21
|
+
export declare function decodeInvite(code: string): InvitePayload;
|
|
22
|
+
export declare class TeamStore {
|
|
23
|
+
private readonly filePath;
|
|
24
|
+
constructor(filePath: string);
|
|
25
|
+
private readStore;
|
|
26
|
+
createTeam(params: {
|
|
27
|
+
teamName: string;
|
|
28
|
+
selfUrl: string;
|
|
29
|
+
selfName: string;
|
|
30
|
+
selfDescription?: string;
|
|
31
|
+
}): Promise<TeamRecord>;
|
|
32
|
+
getTeam(teamId: string): Promise<TeamRecord | null>;
|
|
33
|
+
listTeams(): Promise<TeamRecord[]>;
|
|
34
|
+
getFirstTeam(): Promise<TeamRecord | null>;
|
|
35
|
+
addMember(teamId: string, member: TeamMember): Promise<boolean>;
|
|
36
|
+
removeMember(teamId: string, memberUrl: string): Promise<boolean>;
|
|
37
|
+
deleteTeam(teamId: string): Promise<boolean>;
|
|
38
|
+
saveTeam(team: TeamRecord): Promise<void>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TeamStore = void 0;
|
|
4
|
+
exports.encodeInvite = encodeInvite;
|
|
5
|
+
exports.decodeInvite = decodeInvite;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const json_store_1 = require("../infra/json-store");
|
|
8
|
+
function emptyStore() {
|
|
9
|
+
return { version: 1, teams: [] };
|
|
10
|
+
}
|
|
11
|
+
function normalizeStore(raw) {
|
|
12
|
+
if (raw.version !== 1 || !Array.isArray(raw.teams)) {
|
|
13
|
+
return emptyStore();
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
teams: raw.teams.filter((t) => t &&
|
|
18
|
+
typeof t.teamId === "string" &&
|
|
19
|
+
typeof t.teamName === "string" &&
|
|
20
|
+
Array.isArray(t.members)),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// ── Invite code helpers ──────────────────────────────────────────────
|
|
24
|
+
const INVITE_PREFIX = "mc:";
|
|
25
|
+
function encodeInvite(teamId, seedUrl) {
|
|
26
|
+
const payload = { t: teamId, u: seedUrl };
|
|
27
|
+
return INVITE_PREFIX + Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
28
|
+
}
|
|
29
|
+
function decodeInvite(code) {
|
|
30
|
+
const trimmed = code.trim();
|
|
31
|
+
const body = trimmed.startsWith(INVITE_PREFIX)
|
|
32
|
+
? trimmed.slice(INVITE_PREFIX.length)
|
|
33
|
+
: trimmed;
|
|
34
|
+
try {
|
|
35
|
+
const json = Buffer.from(body, "base64url").toString("utf8");
|
|
36
|
+
const parsed = JSON.parse(json);
|
|
37
|
+
if (typeof parsed.t !== "string" || typeof parsed.u !== "string") {
|
|
38
|
+
throw new Error("invalid invite payload");
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error("invalid invite code");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ── TeamStore ────────────────────────────────────────────────────────
|
|
47
|
+
class TeamStore {
|
|
48
|
+
filePath;
|
|
49
|
+
constructor(filePath) {
|
|
50
|
+
this.filePath = filePath;
|
|
51
|
+
}
|
|
52
|
+
async readStore() {
|
|
53
|
+
const store = await (0, json_store_1.readJsonWithFallback)(this.filePath, emptyStore());
|
|
54
|
+
return normalizeStore(store);
|
|
55
|
+
}
|
|
56
|
+
async createTeam(params) {
|
|
57
|
+
return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
58
|
+
const store = await this.readStore();
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const record = {
|
|
61
|
+
teamId: (0, node_crypto_1.randomUUID)(),
|
|
62
|
+
teamName: params.teamName,
|
|
63
|
+
selfUrl: params.selfUrl,
|
|
64
|
+
members: [{ url: params.selfUrl, name: params.selfName, description: params.selfDescription, joinedAtMs: now }],
|
|
65
|
+
createdAtMs: now,
|
|
66
|
+
};
|
|
67
|
+
store.teams.push(record);
|
|
68
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
69
|
+
return record;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async getTeam(teamId) {
|
|
73
|
+
const store = await this.readStore();
|
|
74
|
+
return store.teams.find((t) => t.teamId === teamId) ?? null;
|
|
75
|
+
}
|
|
76
|
+
async listTeams() {
|
|
77
|
+
const store = await this.readStore();
|
|
78
|
+
return [...store.teams];
|
|
79
|
+
}
|
|
80
|
+
async getFirstTeam() {
|
|
81
|
+
const store = await this.readStore();
|
|
82
|
+
return store.teams[0] ?? null;
|
|
83
|
+
}
|
|
84
|
+
async addMember(teamId, member) {
|
|
85
|
+
return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
86
|
+
const store = await this.readStore();
|
|
87
|
+
const team = store.teams.find((t) => t.teamId === teamId);
|
|
88
|
+
if (!team)
|
|
89
|
+
return false;
|
|
90
|
+
const normalizedUrl = member.url.replace(/\/+$/, "");
|
|
91
|
+
const existing = team.members.findIndex((m) => m.url.replace(/\/+$/, "") === normalizedUrl);
|
|
92
|
+
if (existing >= 0) {
|
|
93
|
+
team.members[existing].name = member.name;
|
|
94
|
+
if (member.description !== undefined) {
|
|
95
|
+
team.members[existing].description = member.description;
|
|
96
|
+
}
|
|
97
|
+
team.members[existing].joinedAtMs = member.joinedAtMs;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
team.members.push({ ...member, url: normalizedUrl });
|
|
101
|
+
}
|
|
102
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async removeMember(teamId, memberUrl) {
|
|
107
|
+
return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
108
|
+
const store = await this.readStore();
|
|
109
|
+
const team = store.teams.find((t) => t.teamId === teamId);
|
|
110
|
+
if (!team)
|
|
111
|
+
return false;
|
|
112
|
+
const normalizedUrl = memberUrl.replace(/\/+$/, "");
|
|
113
|
+
const before = team.members.length;
|
|
114
|
+
team.members = team.members.filter((m) => m.url.replace(/\/+$/, "") !== normalizedUrl);
|
|
115
|
+
if (team.members.length === before)
|
|
116
|
+
return false;
|
|
117
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async deleteTeam(teamId) {
|
|
122
|
+
return await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
123
|
+
const store = await this.readStore();
|
|
124
|
+
const before = store.teams.length;
|
|
125
|
+
store.teams = store.teams.filter((t) => t.teamId !== teamId);
|
|
126
|
+
if (store.teams.length === before)
|
|
127
|
+
return false;
|
|
128
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async saveTeam(team) {
|
|
133
|
+
await (0, json_store_1.withJsonLock)(this.filePath, emptyStore(), async () => {
|
|
134
|
+
const store = await this.readStore();
|
|
135
|
+
const idx = store.teams.findIndex((t) => t.teamId === team.teamId);
|
|
136
|
+
if (idx >= 0) {
|
|
137
|
+
store.teams[idx] = team;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
store.teams.push(team);
|
|
141
|
+
}
|
|
142
|
+
await (0, json_store_1.writeJsonAtomically)(this.filePath, store);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.TeamStore = TeamStore;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type GatewayRespond = (ok: boolean, payload?: unknown, error?: {
|
|
2
|
+
code?: string;
|
|
3
|
+
message?: string;
|
|
4
|
+
details?: unknown;
|
|
5
|
+
}) => void;
|
|
6
|
+
export type GatewayRequestHandler = (opts: {
|
|
7
|
+
params: Record<string, unknown>;
|
|
8
|
+
respond: GatewayRespond;
|
|
9
|
+
}) => void | Promise<void>;
|
|
10
|
+
export type PluginServiceContext = {
|
|
11
|
+
stateDir: string;
|
|
12
|
+
logger: {
|
|
13
|
+
info: (message: string) => void;
|
|
14
|
+
warn: (message: string) => void;
|
|
15
|
+
error: (message: string) => void;
|
|
16
|
+
debug?: (message: string) => void;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type PluginService = {
|
|
20
|
+
id: string;
|
|
21
|
+
start: (ctx: PluginServiceContext) => void | Promise<void>;
|
|
22
|
+
stop?: (ctx?: PluginServiceContext) => void | Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
export type PluginHookMessageContext = {
|
|
25
|
+
channelId: string;
|
|
26
|
+
accountId?: string;
|
|
27
|
+
conversationId?: string;
|
|
28
|
+
};
|
|
29
|
+
export type PluginHookMessageEvent = {
|
|
30
|
+
from: string;
|
|
31
|
+
content: string;
|
|
32
|
+
timestamp?: number;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
};
|
|
35
|
+
export type PluginHookGatewayStartEvent = {
|
|
36
|
+
port: number;
|
|
37
|
+
};
|
|
38
|
+
export type PluginHookGatewayStopEvent = {
|
|
39
|
+
reason?: string;
|
|
40
|
+
};
|
|
41
|
+
export type PluginTool = {
|
|
42
|
+
name: string;
|
|
43
|
+
description: string;
|
|
44
|
+
parameters: Record<string, unknown>;
|
|
45
|
+
execute: (toolCallId: string, args: Record<string, unknown>) => Promise<{
|
|
46
|
+
content: Array<{
|
|
47
|
+
type: "text";
|
|
48
|
+
text: string;
|
|
49
|
+
}>;
|
|
50
|
+
details?: unknown;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
53
|
+
export type OpenClawGatewayConfig = {
|
|
54
|
+
port?: number;
|
|
55
|
+
auth?: {
|
|
56
|
+
mode?: string;
|
|
57
|
+
token?: string;
|
|
58
|
+
password?: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export type OpenClawPluginApi = {
|
|
62
|
+
config?: {
|
|
63
|
+
plugins?: Record<string, unknown>;
|
|
64
|
+
gateway?: OpenClawGatewayConfig;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
pluginConfig?: Record<string, unknown>;
|
|
68
|
+
logger: {
|
|
69
|
+
info: (message: string) => void;
|
|
70
|
+
warn: (message: string) => void;
|
|
71
|
+
error: (message: string) => void;
|
|
72
|
+
debug?: (message: string) => void;
|
|
73
|
+
};
|
|
74
|
+
registerService: (service: PluginService) => void;
|
|
75
|
+
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;
|
|
76
|
+
registerTool: (tool: PluginTool) => void;
|
|
77
|
+
registerHttpRoute: (route: {
|
|
78
|
+
path: string;
|
|
79
|
+
auth?: "plugin" | "gateway";
|
|
80
|
+
handler: (req: unknown, res: {
|
|
81
|
+
statusCode: number;
|
|
82
|
+
end: (body?: string) => void;
|
|
83
|
+
}) => void;
|
|
84
|
+
}) => void;
|
|
85
|
+
on: <K extends "message_received" | "gateway_start" | "gateway_stop">(name: K, handler: K extends "message_received" ? (event: PluginHookMessageEvent, ctx: PluginHookMessageContext) => void | Promise<void> : K extends "gateway_start" ? (event: PluginHookGatewayStartEvent) => void | Promise<void> : (event: PluginHookGatewayStopEvent) => void | Promise<void>) => void;
|
|
86
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "multiclaws",
|
|
3
|
+
"name": "MultiClaws",
|
|
4
|
+
"description": "MultiClaws plugin for multi-instance collaboration using A2A protocol",
|
|
5
|
+
"version": "0.3.0",
|
|
6
|
+
"skills": ["./skills/multiclaws"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"port": {
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"minimum": 1,
|
|
14
|
+
"maximum": 65535,
|
|
15
|
+
"default": 3100
|
|
16
|
+
},
|
|
17
|
+
"displayName": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"minLength": 1
|
|
20
|
+
},
|
|
21
|
+
"selfUrl": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Publicly reachable URL for this agent. Auto-detected from hostname if not set."
|
|
24
|
+
},
|
|
25
|
+
"telemetry": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"properties": {
|
|
29
|
+
"consoleExporter": { "type": "boolean", "default": false }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "multiclaws",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "MultiClaws plugin for OpenClaw collaboration via A2A protocol",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./dist/index.js"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"skills",
|
|
15
|
+
"openclaw.plugin.json",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"clean": "rm -rf dist"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"openclaw",
|
|
26
|
+
"plugin",
|
|
27
|
+
"multiclaws",
|
|
28
|
+
"a2a",
|
|
29
|
+
"collaboration"
|
|
30
|
+
],
|
|
31
|
+
"author": "Eric Sun",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/EricSun0218/multiclaws.git"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@a2a-js/sdk": "^0.3.10",
|
|
39
|
+
"@opentelemetry/api": "^1.9.0",
|
|
40
|
+
"@opentelemetry/sdk-trace-base": "^2.6.0",
|
|
41
|
+
"@opentelemetry/sdk-trace-node": "^2.6.0",
|
|
42
|
+
"express": "^5.2.1",
|
|
43
|
+
"opossum": "^9.0.0",
|
|
44
|
+
"p-retry": "^7.1.1",
|
|
45
|
+
"proper-lockfile": "^4.1.2",
|
|
46
|
+
"zod": "^4.3.6"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/express": "^5.0.6",
|
|
50
|
+
"@types/node": "^24.3.0",
|
|
51
|
+
"@types/opossum": "^8.1.9",
|
|
52
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
53
|
+
"typescript": "^5.9.2",
|
|
54
|
+
"vitest": "^3.2.4"
|
|
55
|
+
}
|
|
56
|
+
}
|