@coolclaw/coolclaw 0.2.6 → 0.2.7
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/dist/{channel-C5YYO-tp.d.ts → channel-BozkcSms.d.ts} +1 -1
- package/dist/cli-metadata.d.ts +1 -2
- package/dist/cli-metadata.js +1533 -4
- package/dist/index.d.ts +20 -10
- package/dist/index.js +1536 -5
- package/dist/setup-entry.d.ts +1 -1
- package/dist/setup-entry.js +1109 -3
- package/package.json +3 -3
- package/dist/chunk-BMUQJBAA.js +0 -394
- package/dist/chunk-BPNTPLYX.js +0 -1173
package/dist/chunk-BPNTPLYX.js
DELETED
|
@@ -1,1173 +0,0 @@
|
|
|
1
|
-
// src/ack-store.ts
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
var InMemoryAckStore = class {
|
|
6
|
-
cursors = /* @__PURE__ */ new Map();
|
|
7
|
-
async getLastAckedSeq(accountKey) {
|
|
8
|
-
return this.cursors.get(accountKey) ?? 0;
|
|
9
|
-
}
|
|
10
|
-
async record(accountKey, seq) {
|
|
11
|
-
if (!Number.isInteger(seq) || seq < 1) {
|
|
12
|
-
throw new Error(`Invalid ACK seq: ${seq}`);
|
|
13
|
-
}
|
|
14
|
-
const current = this.cursors.get(accountKey) ?? 0;
|
|
15
|
-
if (seq <= current) {
|
|
16
|
-
return current;
|
|
17
|
-
}
|
|
18
|
-
this.cursors.set(accountKey, seq);
|
|
19
|
-
return seq;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
var FileAckStore = class {
|
|
23
|
-
cursors = /* @__PURE__ */ new Map();
|
|
24
|
-
dir;
|
|
25
|
-
constructor(dir) {
|
|
26
|
-
this.dir = dir ?? join(homedir(), ".openclaw", "extensions", "coolclaw", ".ack-store");
|
|
27
|
-
if (!existsSync(this.dir)) {
|
|
28
|
-
mkdirSync(this.dir, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
async getLastAckedSeq(accountKey) {
|
|
32
|
-
return this.cursors.get(accountKey) ?? this.load(accountKey);
|
|
33
|
-
}
|
|
34
|
-
async record(accountKey, seq) {
|
|
35
|
-
if (!Number.isInteger(seq) || seq < 1) {
|
|
36
|
-
throw new Error(`Invalid ACK seq: ${seq}`);
|
|
37
|
-
}
|
|
38
|
-
const current = this.cursors.get(accountKey) ?? this.load(accountKey);
|
|
39
|
-
if (seq <= current) {
|
|
40
|
-
return current;
|
|
41
|
-
}
|
|
42
|
-
this.cursors.set(accountKey, seq);
|
|
43
|
-
this.persist(accountKey, seq);
|
|
44
|
-
return seq;
|
|
45
|
-
}
|
|
46
|
-
load(accountKey) {
|
|
47
|
-
const filePath = this.filePath(accountKey);
|
|
48
|
-
if (!existsSync(filePath)) {
|
|
49
|
-
return 0;
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const text = readFileSync(filePath, "utf-8").trim();
|
|
53
|
-
const value = parseInt(text, 10);
|
|
54
|
-
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
55
|
-
} catch {
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
persist(accountKey, lastAckedSeq) {
|
|
60
|
-
try {
|
|
61
|
-
writeFileSync(this.filePath(accountKey), String(lastAckedSeq), "utf-8");
|
|
62
|
-
} catch {
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
filePath(accountKey) {
|
|
66
|
-
const safeName = accountKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
67
|
-
return join(this.dir, `${safeName}.ack`);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// src/binding.ts
|
|
72
|
-
import { mkdir, readFile, rename, writeFile, chmod } from "fs/promises";
|
|
73
|
-
import { homedir as homedir2 } from "os";
|
|
74
|
-
import path from "path";
|
|
75
|
-
import { fileURLToPath } from "url";
|
|
76
|
-
var RIDDLE_BINDING_VERSION = 1;
|
|
77
|
-
function defaultBindingFile(home = homedir2()) {
|
|
78
|
-
return path.join(home, ".config", "coolclaw", "agent_binding.json");
|
|
79
|
-
}
|
|
80
|
-
function defaultOpenClawConfigFile(home = homedir2()) {
|
|
81
|
-
return path.join(home, ".openclaw", "openclaw.json");
|
|
82
|
-
}
|
|
83
|
-
function defaultTokenFile(bindingFile, agentId) {
|
|
84
|
-
return path.join(path.dirname(bindingFile), `agent_token_${agentId}.txt`);
|
|
85
|
-
}
|
|
86
|
-
async function loadBinding(bindingFile) {
|
|
87
|
-
try {
|
|
88
|
-
const raw = JSON.parse(await readFile(bindingFile, "utf8"));
|
|
89
|
-
return {
|
|
90
|
-
agentId: String(raw.agentId ?? ""),
|
|
91
|
-
tokenRef: typeof raw.tokenRef === "string" ? raw.tokenRef : null,
|
|
92
|
-
runtimeType: String(raw.runtimeType ?? "unknown"),
|
|
93
|
-
lastAckedSeq: Number(raw.lastAckedSeq ?? 0),
|
|
94
|
-
bindingVersion: Number(raw.bindingVersion ?? RIDDLE_BINDING_VERSION),
|
|
95
|
-
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : void 0
|
|
96
|
-
};
|
|
97
|
-
} catch {
|
|
98
|
-
return {
|
|
99
|
-
agentId: "",
|
|
100
|
-
tokenRef: null,
|
|
101
|
-
runtimeType: "unknown",
|
|
102
|
-
lastAckedSeq: 0,
|
|
103
|
-
bindingVersion: RIDDLE_BINDING_VERSION
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function touchBinding(binding) {
|
|
108
|
-
return {
|
|
109
|
-
...binding,
|
|
110
|
-
bindingVersion: RIDDLE_BINDING_VERSION,
|
|
111
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
async function saveBinding(bindingFile, binding) {
|
|
115
|
-
await mkdir(path.dirname(bindingFile), { recursive: true });
|
|
116
|
-
const tmpFile = `${bindingFile}.${process.pid}.tmp`;
|
|
117
|
-
await writeFile(tmpFile, `${JSON.stringify(touchBinding(binding), null, 2)}
|
|
118
|
-
`, { mode: 384 });
|
|
119
|
-
await chmod(tmpFile, 384);
|
|
120
|
-
await rename(tmpFile, bindingFile);
|
|
121
|
-
}
|
|
122
|
-
async function saveAgentToken(tokenFile, token) {
|
|
123
|
-
await mkdir(path.dirname(tokenFile), { recursive: true });
|
|
124
|
-
await writeFile(tokenFile, token, { mode: 384 });
|
|
125
|
-
await chmod(tokenFile, 384);
|
|
126
|
-
}
|
|
127
|
-
async function readTokenRef(tokenRef) {
|
|
128
|
-
if (!tokenRef) return void 0;
|
|
129
|
-
if (tokenRef.startsWith("env:")) {
|
|
130
|
-
return process.env[tokenRef.slice("env:".length)] || void 0;
|
|
131
|
-
}
|
|
132
|
-
if (tokenRef.startsWith("file://")) {
|
|
133
|
-
const tokenPath = fileURLToPath(tokenRef);
|
|
134
|
-
return (await readFile(tokenPath, "utf8")).trim() || void 0;
|
|
135
|
-
}
|
|
136
|
-
return void 0;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// src/config.ts
|
|
140
|
-
function normalizeGatewayUrl(value) {
|
|
141
|
-
return value.trim().replace(/\/+$/, "");
|
|
142
|
-
}
|
|
143
|
-
function buildWsUrl(gatewayUrl, lastAckedSeq) {
|
|
144
|
-
const baseUrl = normalizeGatewayUrl(gatewayUrl).replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
145
|
-
return `${baseUrl}/ws/channel?lastAckedSeq=${lastAckedSeq}`;
|
|
146
|
-
}
|
|
147
|
-
function resolveAccountConfig(source, env = process.env) {
|
|
148
|
-
const account = readDefaultAccount(source);
|
|
149
|
-
if (account) {
|
|
150
|
-
return finalizeAccount(account, "config");
|
|
151
|
-
}
|
|
152
|
-
const envAccount = readEnvAccount(env);
|
|
153
|
-
if (envAccount) {
|
|
154
|
-
return finalizeAccount(envAccount, "env");
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
configured: false,
|
|
158
|
-
source: "none",
|
|
159
|
-
reasons: ["Missing CoolClaw account config"]
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function inspectAccount(account) {
|
|
163
|
-
const config = account.config;
|
|
164
|
-
return JSON.stringify({
|
|
165
|
-
configured: account.configured,
|
|
166
|
-
source: account.source,
|
|
167
|
-
gatewayUrl: config?.gatewayUrl,
|
|
168
|
-
agentId: config?.agentId,
|
|
169
|
-
tokenConfigured: Boolean(config?.tokenSecretRef),
|
|
170
|
-
tokenSecretRef: maskSecretRef(config?.tokenSecretRef),
|
|
171
|
-
allowFromCount: config?.allowFrom?.length ?? 0,
|
|
172
|
-
dmPolicy: config?.dmPolicy,
|
|
173
|
-
reasons: account.reasons
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
async function resolveAccountToken(account) {
|
|
177
|
-
if (account.tokenSecret) return account.tokenSecret;
|
|
178
|
-
return readTokenRef(account.tokenSecretRef);
|
|
179
|
-
}
|
|
180
|
-
function readDefaultAccount(source) {
|
|
181
|
-
if (!isRecord(source)) return void 0;
|
|
182
|
-
const channels = source.channels;
|
|
183
|
-
if (!isRecord(channels)) return void 0;
|
|
184
|
-
const coolclaw = channels.coolclaw;
|
|
185
|
-
if (!isRecord(coolclaw)) return void 0;
|
|
186
|
-
const accounts = coolclaw.accounts;
|
|
187
|
-
if (!isRecord(accounts)) return void 0;
|
|
188
|
-
const defaultAccount = accounts.default;
|
|
189
|
-
return isRecord(defaultAccount) ? coerceRawAccount(defaultAccount) : void 0;
|
|
190
|
-
}
|
|
191
|
-
function readEnvAccount(env) {
|
|
192
|
-
if (!env.COOLCLAW_GATEWAY_URL && !env.COOLCLAW_AGENT_ID && !env.COOLCLAW_AGENT_TOKEN && !env.COOLCLAW_AGENT_TOKEN_SECRET_REF) {
|
|
193
|
-
return void 0;
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
gatewayUrl: env.COOLCLAW_GATEWAY_URL,
|
|
197
|
-
agentId: env.COOLCLAW_AGENT_ID,
|
|
198
|
-
tokenSecretRef: env.COOLCLAW_AGENT_TOKEN_SECRET_REF ?? (env.COOLCLAW_AGENT_TOKEN ? "env:COOLCLAW_AGENT_TOKEN" : void 0),
|
|
199
|
-
allowFrom: splitCsv(env.COOLCLAW_ALLOW_FROM),
|
|
200
|
-
dmPolicy: coerceDmPolicy(env.COOLCLAW_DM_POLICY)
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function finalizeAccount(account, source) {
|
|
204
|
-
const gatewayUrl = typeof account.gatewayUrl === "string" ? normalizeGatewayUrl(account.gatewayUrl) : "";
|
|
205
|
-
const agentId = typeof account.agentId === "string" ? account.agentId.trim() : "";
|
|
206
|
-
const tokenSecretRef = typeof account.tokenSecretRef === "string" ? account.tokenSecretRef.trim() : account.tokenSecret?.trim();
|
|
207
|
-
const allowFrom = normalizeStringArray(account.allowFrom);
|
|
208
|
-
const dmPolicy = coerceDmPolicy(account.dmPolicy) ?? "open";
|
|
209
|
-
const reasons = [];
|
|
210
|
-
if (!gatewayUrl) reasons.push("Missing gateway URL");
|
|
211
|
-
if (!agentId) reasons.push("Missing Agent ID");
|
|
212
|
-
if (!tokenSecretRef) reasons.push("Missing token secret reference");
|
|
213
|
-
const config = {
|
|
214
|
-
gatewayUrl,
|
|
215
|
-
agentId,
|
|
216
|
-
tokenSecretRef,
|
|
217
|
-
allowFrom,
|
|
218
|
-
dmPolicy
|
|
219
|
-
};
|
|
220
|
-
if (reasons.length > 0) {
|
|
221
|
-
return {
|
|
222
|
-
configured: false,
|
|
223
|
-
source,
|
|
224
|
-
config,
|
|
225
|
-
reasons
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
229
|
-
configured: true,
|
|
230
|
-
source,
|
|
231
|
-
config,
|
|
232
|
-
reasons: []
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function coerceRawAccount(value) {
|
|
236
|
-
return {
|
|
237
|
-
gatewayUrl: typeof value.gatewayUrl === "string" ? value.gatewayUrl : void 0,
|
|
238
|
-
agentId: typeof value.agentId === "string" ? value.agentId : void 0,
|
|
239
|
-
tokenSecretRef: typeof value.tokenSecretRef === "string" ? value.tokenSecretRef : void 0,
|
|
240
|
-
tokenSecret: typeof value.tokenSecret === "string" ? value.tokenSecret : void 0,
|
|
241
|
-
allowFrom: normalizeStringArray(value.allowFrom),
|
|
242
|
-
dmPolicy: coerceDmPolicy(value.dmPolicy)
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
function normalizeStringArray(value) {
|
|
246
|
-
if (!Array.isArray(value)) return void 0;
|
|
247
|
-
const items = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
248
|
-
return items.length > 0 ? items : void 0;
|
|
249
|
-
}
|
|
250
|
-
function splitCsv(value) {
|
|
251
|
-
if (!value) return void 0;
|
|
252
|
-
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
253
|
-
return items.length > 0 ? items : void 0;
|
|
254
|
-
}
|
|
255
|
-
function coerceDmPolicy(value) {
|
|
256
|
-
return value === "pairing" || value === "allowlist" || value === "open" ? value : void 0;
|
|
257
|
-
}
|
|
258
|
-
function maskSecretRef(value) {
|
|
259
|
-
if (!value) return void 0;
|
|
260
|
-
if (value.startsWith("env:")) return value;
|
|
261
|
-
const tail = value.slice(-4);
|
|
262
|
-
return tail ? `***${tail}` : "***";
|
|
263
|
-
}
|
|
264
|
-
function isRecord(value) {
|
|
265
|
-
return typeof value === "object" && value !== null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// src/frame-codec.ts
|
|
269
|
-
import { randomUUID } from "crypto";
|
|
270
|
-
var CoolclawFrameDecodeError = class extends Error {
|
|
271
|
-
constructor(message) {
|
|
272
|
-
super(message);
|
|
273
|
-
this.name = "CoolclawFrameDecodeError";
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
function createFrame(type, payload) {
|
|
277
|
-
return {
|
|
278
|
-
v: 1,
|
|
279
|
-
type,
|
|
280
|
-
id: `cli_${randomUUID()}`,
|
|
281
|
-
ts: Date.now(),
|
|
282
|
-
payload
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
function encodeFrame(frame) {
|
|
286
|
-
return JSON.stringify(frame);
|
|
287
|
-
}
|
|
288
|
-
function decodeFrame(raw) {
|
|
289
|
-
let value;
|
|
290
|
-
try {
|
|
291
|
-
value = JSON.parse(raw);
|
|
292
|
-
} catch (error) {
|
|
293
|
-
throw new CoolclawFrameDecodeError(`Invalid CoolClaw frame JSON: ${error.message}`);
|
|
294
|
-
}
|
|
295
|
-
if (!isRecord2(value)) {
|
|
296
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: expected object");
|
|
297
|
-
}
|
|
298
|
-
if (!("v" in value)) {
|
|
299
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing v");
|
|
300
|
-
}
|
|
301
|
-
if (value.v !== 1) {
|
|
302
|
-
throw new CoolclawFrameDecodeError("Unsupported CoolClaw frame version");
|
|
303
|
-
}
|
|
304
|
-
if (typeof value.type !== "string" || value.type.length === 0) {
|
|
305
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing type");
|
|
306
|
-
}
|
|
307
|
-
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
308
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing id");
|
|
309
|
-
}
|
|
310
|
-
if (typeof value.ts !== "number" || !Number.isFinite(value.ts)) {
|
|
311
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing ts");
|
|
312
|
-
}
|
|
313
|
-
if ("ack" in value && value.ack !== void 0 && typeof value.ack !== "string") {
|
|
314
|
-
throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: ack must be a string");
|
|
315
|
-
}
|
|
316
|
-
const frame = {
|
|
317
|
-
v: 1,
|
|
318
|
-
type: value.type,
|
|
319
|
-
id: value.id,
|
|
320
|
-
ts: value.ts
|
|
321
|
-
};
|
|
322
|
-
if (typeof value.ack === "string") {
|
|
323
|
-
frame.ack = value.ack;
|
|
324
|
-
}
|
|
325
|
-
if ("payload" in value) {
|
|
326
|
-
frame.payload = value.payload;
|
|
327
|
-
}
|
|
328
|
-
return frame;
|
|
329
|
-
}
|
|
330
|
-
function isRecord2(value) {
|
|
331
|
-
return typeof value === "object" && value !== null;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// src/security.ts
|
|
335
|
-
function applyInboundSecurityPolicy(envelope, config) {
|
|
336
|
-
if (!isPrivateConversation(envelope)) {
|
|
337
|
-
return {
|
|
338
|
-
accepted: true,
|
|
339
|
-
envelope: {
|
|
340
|
-
...envelope,
|
|
341
|
-
shouldReply: envelope.group ? envelope.shouldReply === true : false
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
const senderKey = envelope.sender ? toIdentityKey(envelope.sender.userType, envelope.sender.userId) : void 0;
|
|
346
|
-
const allowFrom = new Set((config.allowFrom ?? []).map(normalizeIdentityKey));
|
|
347
|
-
if (senderKey && allowFrom.has(senderKey)) {
|
|
348
|
-
return { accepted: true, envelope };
|
|
349
|
-
}
|
|
350
|
-
if ((config.dmPolicy ?? "allowlist") === "open") {
|
|
351
|
-
return { accepted: true, envelope };
|
|
352
|
-
}
|
|
353
|
-
if ((config.dmPolicy ?? "allowlist") === "pairing" && envelope.sender) {
|
|
354
|
-
return {
|
|
355
|
-
accepted: true,
|
|
356
|
-
envelope: {
|
|
357
|
-
...envelope,
|
|
358
|
-
conversationId: `pairing:${senderKey}`,
|
|
359
|
-
shouldReply: false,
|
|
360
|
-
metadata: {
|
|
361
|
-
...envelope.metadata,
|
|
362
|
-
pairingRequired: true,
|
|
363
|
-
originalConversationId: envelope.conversationId
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
accepted: false,
|
|
370
|
-
reason: "DM sender is not allowlisted",
|
|
371
|
-
envelope: {
|
|
372
|
-
...envelope,
|
|
373
|
-
shouldReply: false,
|
|
374
|
-
metadata: {
|
|
375
|
-
...envelope.metadata,
|
|
376
|
-
blockedByPolicy: true
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
function isPrivateConversation(envelope) {
|
|
382
|
-
return envelope.conversationId.startsWith("private:");
|
|
383
|
-
}
|
|
384
|
-
function toIdentityKey(userType, userId) {
|
|
385
|
-
return `${userType.toLowerCase()}:${userId}`;
|
|
386
|
-
}
|
|
387
|
-
function normalizeIdentityKey(value) {
|
|
388
|
-
return value.trim().toLowerCase();
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// src/inbound.ts
|
|
392
|
-
function mapInboundFrame(frame) {
|
|
393
|
-
if (frame.type === "PRIVATE_MESSAGE") {
|
|
394
|
-
const payload = assertPrivatePayload(frame.payload);
|
|
395
|
-
return {
|
|
396
|
-
id: payload.messageId,
|
|
397
|
-
channel: "coolclaw",
|
|
398
|
-
conversationId: `private:${payload.sender.userType}:${payload.sender.userId}`,
|
|
399
|
-
text: payload.content,
|
|
400
|
-
messageType: payload.messageType,
|
|
401
|
-
seq: payload.seq,
|
|
402
|
-
shouldReply: payload.mentioned,
|
|
403
|
-
sender: payload.sender,
|
|
404
|
-
recipient: payload.recipient,
|
|
405
|
-
metadata: {
|
|
406
|
-
riddleConversationId: payload.conversationId,
|
|
407
|
-
sentAt: payload.sentAt,
|
|
408
|
-
sourceFrameId: frame.id
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
if (frame.type === "GROUP_MESSAGE") {
|
|
413
|
-
const payload = assertGroupPayload(frame.payload);
|
|
414
|
-
return {
|
|
415
|
-
id: payload.messageId,
|
|
416
|
-
channel: "coolclaw",
|
|
417
|
-
conversationId: `group:${payload.groupId}`,
|
|
418
|
-
text: payload.content,
|
|
419
|
-
messageType: payload.messageType,
|
|
420
|
-
seq: payload.seq,
|
|
421
|
-
shouldReply: payload.mentioned,
|
|
422
|
-
sender: payload.sender,
|
|
423
|
-
group: {
|
|
424
|
-
groupId: payload.groupId,
|
|
425
|
-
groupName: payload.groupName
|
|
426
|
-
},
|
|
427
|
-
metadata: {
|
|
428
|
-
riddleConversationId: payload.conversationId,
|
|
429
|
-
sentAt: payload.sentAt,
|
|
430
|
-
sourceFrameId: frame.id,
|
|
431
|
-
agentHint: payload.agentHint
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
if (frame.type === "SYSTEM_NOTIFICATION" || frame.type === "GAME_EVENT" || frame.type === "CONTENT_TASK") {
|
|
436
|
-
return mapNotificationFrame(frame);
|
|
437
|
-
}
|
|
438
|
-
throw new Error(`Unsupported inbound CoolClaw frame type: ${frame.type}`);
|
|
439
|
-
}
|
|
440
|
-
async function handleInboundFrame(input) {
|
|
441
|
-
const mappedEnvelope = mapInboundFrame(input.frame);
|
|
442
|
-
const decision = input.accountConfig ? applyInboundSecurityPolicy(mappedEnvelope, input.accountConfig) : { accepted: true, envelope: mappedEnvelope };
|
|
443
|
-
const envelope = decision.envelope;
|
|
444
|
-
await ackProcessedSeq(input, envelope);
|
|
445
|
-
if (!decision.accepted) {
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
await input.dispatch(envelope);
|
|
449
|
-
}
|
|
450
|
-
function mapNotificationFrame(frame) {
|
|
451
|
-
const payload = isRecord3(frame.payload) ? frame.payload : {};
|
|
452
|
-
const seq = typeof payload.seq === "number" ? payload.seq : void 0;
|
|
453
|
-
return {
|
|
454
|
-
id: frame.id,
|
|
455
|
-
channel: "coolclaw",
|
|
456
|
-
conversationId: frame.type === "SYSTEM_NOTIFICATION" ? "notification:system" : `notification:${frame.type.toLowerCase()}`,
|
|
457
|
-
text: JSON.stringify(frame.payload ?? {}),
|
|
458
|
-
messageType: frame.type,
|
|
459
|
-
seq,
|
|
460
|
-
shouldReply: false,
|
|
461
|
-
metadata: {
|
|
462
|
-
sourceFrameId: frame.id,
|
|
463
|
-
payload: frame.payload
|
|
464
|
-
}
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
async function ackProcessedSeq(input, envelope) {
|
|
468
|
-
if (typeof envelope.seq === "number") {
|
|
469
|
-
const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
|
|
470
|
-
await input.sendAck(createFrame("ACK", { lastAckedSeq }));
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
function assertPrivatePayload(value) {
|
|
474
|
-
if (!isRecord3(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
|
|
475
|
-
throw new Error("Invalid PRIVATE_MESSAGE payload");
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
seq: readNumber(value, "seq"),
|
|
479
|
-
messageId: readString(value, "messageId"),
|
|
480
|
-
conversationId: readString(value, "conversationId"),
|
|
481
|
-
sender: value.sender,
|
|
482
|
-
recipient: value.recipient,
|
|
483
|
-
messageType: readString(value, "messageType"),
|
|
484
|
-
content: readString(value, "content"),
|
|
485
|
-
mentioned: readBoolean(value, "mentioned"),
|
|
486
|
-
sentAt: readString(value, "sentAt")
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
function assertGroupPayload(value) {
|
|
490
|
-
if (!isRecord3(value) || !isUserRef(value.sender)) {
|
|
491
|
-
throw new Error("Invalid GROUP_MESSAGE payload");
|
|
492
|
-
}
|
|
493
|
-
return {
|
|
494
|
-
seq: readNumber(value, "seq"),
|
|
495
|
-
messageId: readString(value, "messageId"),
|
|
496
|
-
groupId: readString(value, "groupId"),
|
|
497
|
-
groupName: readString(value, "groupName"),
|
|
498
|
-
conversationId: readString(value, "conversationId"),
|
|
499
|
-
sender: value.sender,
|
|
500
|
-
messageType: readString(value, "messageType"),
|
|
501
|
-
content: readString(value, "content"),
|
|
502
|
-
mentioned: readBoolean(value, "mentioned"),
|
|
503
|
-
sentAt: readString(value, "sentAt"),
|
|
504
|
-
agentHint: readOptionalString(value, "agentHint")
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
function readString(source, key) {
|
|
508
|
-
const value = source[key];
|
|
509
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
510
|
-
throw new Error(`Invalid inbound payload: missing ${key}`);
|
|
511
|
-
}
|
|
512
|
-
return value;
|
|
513
|
-
}
|
|
514
|
-
function readNumber(source, key) {
|
|
515
|
-
const value = source[key];
|
|
516
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
517
|
-
throw new Error(`Invalid inbound payload: missing ${key}`);
|
|
518
|
-
}
|
|
519
|
-
return value;
|
|
520
|
-
}
|
|
521
|
-
function readBoolean(source, key) {
|
|
522
|
-
const value = source[key];
|
|
523
|
-
if (typeof value !== "boolean") {
|
|
524
|
-
throw new Error(`Invalid inbound payload: missing ${key}`);
|
|
525
|
-
}
|
|
526
|
-
return value;
|
|
527
|
-
}
|
|
528
|
-
function readOptionalString(source, key) {
|
|
529
|
-
const value = source[key];
|
|
530
|
-
if (value === void 0 || value === null) {
|
|
531
|
-
return void 0;
|
|
532
|
-
}
|
|
533
|
-
if (typeof value !== "string") {
|
|
534
|
-
throw new Error(`Invalid inbound payload: ${key} must be a string`);
|
|
535
|
-
}
|
|
536
|
-
return value;
|
|
537
|
-
}
|
|
538
|
-
function isUserRef(value) {
|
|
539
|
-
return isRecord3(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
|
|
540
|
-
}
|
|
541
|
-
function isRecord3(value) {
|
|
542
|
-
return typeof value === "object" && value !== null;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// src/targets.ts
|
|
546
|
-
var TargetParseError = class extends Error {
|
|
547
|
-
constructor(message) {
|
|
548
|
-
super(message);
|
|
549
|
-
this.name = "TargetParseError";
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
function parseCoolclawTarget(raw) {
|
|
553
|
-
const normalized = normalizeCoolclawTarget(raw);
|
|
554
|
-
const parts = normalized.split(":");
|
|
555
|
-
if (parts.length !== 3) {
|
|
556
|
-
throw new TargetParseError(`Invalid CoolClaw target: ${raw}`);
|
|
557
|
-
}
|
|
558
|
-
const [channel, type, id] = parts;
|
|
559
|
-
if (channel !== "coolclaw" || id.length === 0) {
|
|
560
|
-
throw new TargetParseError(`Invalid CoolClaw target: ${raw}`);
|
|
561
|
-
}
|
|
562
|
-
if (type === "human") {
|
|
563
|
-
return { kind: "private", userType: "HUMAN", userId: id };
|
|
564
|
-
}
|
|
565
|
-
if (type === "agent") {
|
|
566
|
-
return { kind: "private", userType: "AGENT", userId: id };
|
|
567
|
-
}
|
|
568
|
-
if (type === "group") {
|
|
569
|
-
return { kind: "group", groupId: id };
|
|
570
|
-
}
|
|
571
|
-
throw new TargetParseError(`Invalid CoolClaw target type: ${type}`);
|
|
572
|
-
}
|
|
573
|
-
function normalizeCoolclawTarget(raw) {
|
|
574
|
-
const trimmed = raw.trim();
|
|
575
|
-
const parts = trimmed.split(":");
|
|
576
|
-
if (parts.length !== 3) {
|
|
577
|
-
return trimmed;
|
|
578
|
-
}
|
|
579
|
-
const [channel, type, id] = parts;
|
|
580
|
-
return `${channel.toLowerCase()}:${type.toLowerCase()}:${id.trim()}`;
|
|
581
|
-
}
|
|
582
|
-
function inferCoolclawTargetChatType(raw) {
|
|
583
|
-
try {
|
|
584
|
-
const target = parseCoolclawTarget(raw);
|
|
585
|
-
return target.kind === "private" ? "direct" : "group";
|
|
586
|
-
} catch {
|
|
587
|
-
return void 0;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
function isCoolclawTargetId(raw, normalized = normalizeCoolclawTarget(raw)) {
|
|
591
|
-
try {
|
|
592
|
-
parseCoolclawTarget(normalized);
|
|
593
|
-
return normalized.startsWith("coolclaw:");
|
|
594
|
-
} catch {
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
async function resolveCoolclawMessagingTarget(raw, preferredKind) {
|
|
599
|
-
const normalized = normalizeCoolclawTarget(raw);
|
|
600
|
-
const target = parseCoolclawTarget(normalized);
|
|
601
|
-
const kind = target.kind === "private" ? "user" : "group";
|
|
602
|
-
if (preferredKind && preferredKind !== kind) {
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
const [, type, id] = normalized.split(":");
|
|
606
|
-
return {
|
|
607
|
-
to: normalized,
|
|
608
|
-
kind,
|
|
609
|
-
display: `${type}:${id}`,
|
|
610
|
-
source: "normalized"
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// src/outbound.ts
|
|
615
|
-
async function sendText(input) {
|
|
616
|
-
const target = parseCoolclawTarget(input.target);
|
|
617
|
-
const frame = target.kind === "private" ? createFrame("SEND_PRIVATE", {
|
|
618
|
-
target: {
|
|
619
|
-
userId: target.userId,
|
|
620
|
-
userType: target.userType
|
|
621
|
-
},
|
|
622
|
-
messageType: "TEXT",
|
|
623
|
-
content: input.text
|
|
624
|
-
}) : createFrame("SEND_GROUP", {
|
|
625
|
-
groupId: target.groupId,
|
|
626
|
-
messageType: "TEXT",
|
|
627
|
-
content: input.text
|
|
628
|
-
});
|
|
629
|
-
const response = await input.client.request(frame);
|
|
630
|
-
if (response.ok === false) {
|
|
631
|
-
throw new Error(response.error?.message ?? "CoolClaw message send failed");
|
|
632
|
-
}
|
|
633
|
-
if (!response.messageId) {
|
|
634
|
-
throw new Error("CoolClaw message send response missing messageId");
|
|
635
|
-
}
|
|
636
|
-
return response.messageId;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// src/status.ts
|
|
640
|
-
function getCoolclawStatus(source) {
|
|
641
|
-
return createStatus(resolveAccountConfig(source ?? {}));
|
|
642
|
-
}
|
|
643
|
-
function createStatus(account) {
|
|
644
|
-
return {
|
|
645
|
-
configured: account.configured,
|
|
646
|
-
connected: false,
|
|
647
|
-
message: account.configured ? "CoolClaw account is configured." : account.reasons.join("; "),
|
|
648
|
-
diagnostics: inspectAccount(account)
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// src/ws-client.ts
|
|
653
|
-
import WebSocket from "ws";
|
|
654
|
-
var CoolclawWsClient = class {
|
|
655
|
-
constructor(options) {
|
|
656
|
-
this.options = options;
|
|
657
|
-
}
|
|
658
|
-
socket;
|
|
659
|
-
heartbeatTimer;
|
|
660
|
-
reconnectTimer;
|
|
661
|
-
stopped = true;
|
|
662
|
-
pendingRequests = /* @__PURE__ */ new Map();
|
|
663
|
-
async start() {
|
|
664
|
-
this.stopped = false;
|
|
665
|
-
await this.connect();
|
|
666
|
-
}
|
|
667
|
-
async stop() {
|
|
668
|
-
this.stopped = true;
|
|
669
|
-
this.clearHeartbeat();
|
|
670
|
-
this.clearReconnect();
|
|
671
|
-
this.rejectPending(new Error("CoolClaw WSS client stopped"));
|
|
672
|
-
const socket = this.socket;
|
|
673
|
-
this.socket = void 0;
|
|
674
|
-
if (!socket || socket.readyState === WebSocket.CLOSED) {
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
await new Promise((resolve) => {
|
|
678
|
-
socket.once("close", () => resolve());
|
|
679
|
-
socket.close(1e3, "client stopped");
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
async request(frame) {
|
|
683
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
684
|
-
throw new Error("CoolClaw WSS client is not connected");
|
|
685
|
-
}
|
|
686
|
-
const requestTimeoutMs = this.options.requestTimeoutMs ?? 1e4;
|
|
687
|
-
return new Promise((resolve, reject) => {
|
|
688
|
-
const timeout = setTimeout(() => {
|
|
689
|
-
this.pendingRequests.delete(frame.id);
|
|
690
|
-
reject(new Error(`CoolClaw request timed out: ${frame.type}`));
|
|
691
|
-
}, requestTimeoutMs);
|
|
692
|
-
this.pendingRequests.set(frame.id, {
|
|
693
|
-
resolve: (payload) => resolve(payload),
|
|
694
|
-
reject,
|
|
695
|
-
timeout
|
|
696
|
-
});
|
|
697
|
-
this.sendFrame(frame);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
sendFrame(frame) {
|
|
701
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
702
|
-
throw new Error("CoolClaw WSS client is not connected");
|
|
703
|
-
}
|
|
704
|
-
this.socket.send(encodeFrame(frame));
|
|
705
|
-
}
|
|
706
|
-
async connect() {
|
|
707
|
-
const lastAckedSeq = await this.options.ackStore.getLastAckedSeq(this.options.accountKey);
|
|
708
|
-
const socket = new WebSocket(buildWsUrl(this.options.gatewayUrl, lastAckedSeq), {
|
|
709
|
-
headers: {
|
|
710
|
-
Authorization: `Bearer ${this.options.token}`,
|
|
711
|
-
"X-CoolClaw-Agent-Id": this.options.agentId,
|
|
712
|
-
"X-CoolClaw-Plugin-Version": this.options.pluginVersion
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
this.socket = socket;
|
|
716
|
-
await new Promise((resolve, reject) => {
|
|
717
|
-
let helloReceived = false;
|
|
718
|
-
const failBeforeHello = (error) => {
|
|
719
|
-
if (!helloReceived) {
|
|
720
|
-
cleanupBeforeHello();
|
|
721
|
-
reject(error);
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
const cleanupBeforeHello = () => {
|
|
725
|
-
socket.off("error", failBeforeHello);
|
|
726
|
-
socket.off("message", onMessageBeforeHello);
|
|
727
|
-
socket.off("close", onCloseBeforeHello);
|
|
728
|
-
};
|
|
729
|
-
const onMessageBeforeHello = (data) => {
|
|
730
|
-
let frame;
|
|
731
|
-
try {
|
|
732
|
-
frame = decodeFrame(data.toString());
|
|
733
|
-
} catch (error) {
|
|
734
|
-
failBeforeHello(error);
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
this.handleFrame(frame).catch((error) => {
|
|
738
|
-
this.rejectPending(error instanceof Error ? error : new Error(String(error)));
|
|
739
|
-
});
|
|
740
|
-
if (frame.type === "HELLO") {
|
|
741
|
-
helloReceived = true;
|
|
742
|
-
cleanupBeforeHello();
|
|
743
|
-
socket.on("message", (nextData) => {
|
|
744
|
-
this.handleRawMessage(nextData).catch((error) => {
|
|
745
|
-
this.rejectPending(error instanceof Error ? error : new Error(String(error)));
|
|
746
|
-
});
|
|
747
|
-
});
|
|
748
|
-
socket.on("close", (code) => this.handleClose(code));
|
|
749
|
-
this.startHeartbeat(frame);
|
|
750
|
-
resolve();
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
const onCloseBeforeHello = (code) => {
|
|
754
|
-
const error = new Error(`CoolClaw WSS closed before HELLO: ${code}`);
|
|
755
|
-
cleanupBeforeHello();
|
|
756
|
-
if (!this.isTerminalClose(code) && !this.stopped) {
|
|
757
|
-
this.scheduleReconnect();
|
|
758
|
-
}
|
|
759
|
-
reject(error);
|
|
760
|
-
};
|
|
761
|
-
socket.on("error", failBeforeHello);
|
|
762
|
-
socket.on("message", onMessageBeforeHello);
|
|
763
|
-
socket.on("close", onCloseBeforeHello);
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
async handleRawMessage(data) {
|
|
767
|
-
await this.handleFrame(decodeFrame(data.toString()));
|
|
768
|
-
}
|
|
769
|
-
async handleFrame(frame) {
|
|
770
|
-
if (frame.ack) {
|
|
771
|
-
const pending = this.pendingRequests.get(frame.ack);
|
|
772
|
-
if (pending) {
|
|
773
|
-
clearTimeout(pending.timeout);
|
|
774
|
-
this.pendingRequests.delete(frame.ack);
|
|
775
|
-
if (frame.type === "ERROR") {
|
|
776
|
-
pending.reject(new Error(readErrorMessage(frame.payload)));
|
|
777
|
-
} else {
|
|
778
|
-
pending.resolve(frame.payload);
|
|
779
|
-
}
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
if (frame.type === "PING") {
|
|
784
|
-
const pong = createFrame("PONG", { clientTime: Date.now() });
|
|
785
|
-
pong.ack = frame.id;
|
|
786
|
-
this.sendFrame(pong);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
if (frame.type === "PONG" || frame.type === "HELLO" || frame.type === "RESUME_DONE") {
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
await this.options.onFrame?.(frame, this);
|
|
793
|
-
}
|
|
794
|
-
startHeartbeat(helloFrame) {
|
|
795
|
-
this.clearHeartbeat();
|
|
796
|
-
const intervalMs = this.options.heartbeatIntervalMs ?? readPingInterval(helloFrame.payload) ?? 2e4;
|
|
797
|
-
this.heartbeatTimer = setInterval(() => {
|
|
798
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
this.sendFrame(createFrame("PING", { clientTime: Date.now() }));
|
|
802
|
-
}, intervalMs);
|
|
803
|
-
}
|
|
804
|
-
handleClose(code) {
|
|
805
|
-
this.clearHeartbeat();
|
|
806
|
-
this.rejectPending(new Error(`CoolClaw WSS connection closed: ${code}`));
|
|
807
|
-
if (this.stopped || this.isTerminalClose(code)) {
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
this.scheduleReconnect();
|
|
811
|
-
}
|
|
812
|
-
scheduleReconnect() {
|
|
813
|
-
this.clearReconnect();
|
|
814
|
-
const delayMs = this.options.reconnectDelayMs ?? 1e3;
|
|
815
|
-
this.reconnectTimer = setTimeout(() => {
|
|
816
|
-
if (this.stopped) return;
|
|
817
|
-
this.connect().catch((error) => {
|
|
818
|
-
if (!this.stopped) {
|
|
819
|
-
this.rejectPending(error instanceof Error ? error : new Error(String(error)));
|
|
820
|
-
this.scheduleReconnect();
|
|
821
|
-
}
|
|
822
|
-
});
|
|
823
|
-
}, delayMs);
|
|
824
|
-
}
|
|
825
|
-
clearHeartbeat() {
|
|
826
|
-
if (this.heartbeatTimer) {
|
|
827
|
-
clearInterval(this.heartbeatTimer);
|
|
828
|
-
this.heartbeatTimer = void 0;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
clearReconnect() {
|
|
832
|
-
if (this.reconnectTimer) {
|
|
833
|
-
clearTimeout(this.reconnectTimer);
|
|
834
|
-
this.reconnectTimer = void 0;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
rejectPending(error) {
|
|
838
|
-
for (const pending of this.pendingRequests.values()) {
|
|
839
|
-
clearTimeout(pending.timeout);
|
|
840
|
-
pending.reject(error);
|
|
841
|
-
}
|
|
842
|
-
this.pendingRequests.clear();
|
|
843
|
-
}
|
|
844
|
-
isTerminalClose(code) {
|
|
845
|
-
return code === 4001 || code === 4002 || code === 4003 || code === 4004 || code === 4005;
|
|
846
|
-
}
|
|
847
|
-
};
|
|
848
|
-
function readPingInterval(payload) {
|
|
849
|
-
if (!isRecord4(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
|
|
850
|
-
return void 0;
|
|
851
|
-
}
|
|
852
|
-
return payload.pingIntervalMs;
|
|
853
|
-
}
|
|
854
|
-
function readErrorMessage(payload) {
|
|
855
|
-
if (isRecord4(payload) && typeof payload.message === "string") {
|
|
856
|
-
return payload.message;
|
|
857
|
-
}
|
|
858
|
-
return "CoolClaw request failed";
|
|
859
|
-
}
|
|
860
|
-
function isRecord4(value) {
|
|
861
|
-
return typeof value === "object" && value !== null;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// src/channel.ts
|
|
865
|
-
var coolclawChannelPlugin = {
|
|
866
|
-
id: "coolclaw",
|
|
867
|
-
meta: {
|
|
868
|
-
id: "coolclaw",
|
|
869
|
-
label: "CoolClaw",
|
|
870
|
-
selectionLabel: "CoolClaw",
|
|
871
|
-
docsPath: "/plugins/coolclaw",
|
|
872
|
-
blurb: "Connect OpenClaw to the CoolClaw/Riddle chat platform."
|
|
873
|
-
},
|
|
874
|
-
channels: ["coolclaw"],
|
|
875
|
-
channel: {
|
|
876
|
-
id: "coolclaw",
|
|
877
|
-
label: "CoolClaw",
|
|
878
|
-
docsPath: "/plugins/coolclaw",
|
|
879
|
-
blurb: "Connect OpenClaw to the CoolClaw/Riddle chat platform."
|
|
880
|
-
},
|
|
881
|
-
capabilities: {
|
|
882
|
-
chatTypes: ["direct", "group"]
|
|
883
|
-
},
|
|
884
|
-
config: {
|
|
885
|
-
listAccountIds(cfg) {
|
|
886
|
-
return Object.keys(cfg.channels?.coolclaw?.accounts ?? {});
|
|
887
|
-
},
|
|
888
|
-
resolveAccount(cfg, accountId = "default") {
|
|
889
|
-
return cfg.channels?.coolclaw?.accounts?.[accountId ?? "default"] ?? {};
|
|
890
|
-
},
|
|
891
|
-
defaultAccountId() {
|
|
892
|
-
return "default";
|
|
893
|
-
},
|
|
894
|
-
isConfigured(account) {
|
|
895
|
-
return Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account));
|
|
896
|
-
},
|
|
897
|
-
isEnabled(account) {
|
|
898
|
-
return account?.enabled !== false;
|
|
899
|
-
},
|
|
900
|
-
describeAccount(account) {
|
|
901
|
-
return {
|
|
902
|
-
accountId: "default",
|
|
903
|
-
name: account.name,
|
|
904
|
-
enabled: account.enabled !== false,
|
|
905
|
-
configured: Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account)),
|
|
906
|
-
gatewayUrl: account.gatewayUrl,
|
|
907
|
-
agentId: account.agentId,
|
|
908
|
-
tokenConfigured: Boolean(account.tokenSecretRef || "tokenSecret" in account),
|
|
909
|
-
allowFromCount: account.allowFrom?.length ?? 0,
|
|
910
|
-
dmPolicy: account.dmPolicy ?? "allowlist"
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
},
|
|
914
|
-
resolver: {
|
|
915
|
-
async resolveTargets({ inputs }) {
|
|
916
|
-
return inputs.map((input) => {
|
|
917
|
-
try {
|
|
918
|
-
const normalized = normalizeCoolclawTarget(input);
|
|
919
|
-
const [, type, id] = normalized.split(":");
|
|
920
|
-
parseCoolclawTarget(normalized);
|
|
921
|
-
return { input, resolved: true, id: normalized, name: `${type}:${id}` };
|
|
922
|
-
} catch (error) {
|
|
923
|
-
return { input, resolved: false, note: error instanceof Error ? error.message : String(error) };
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
},
|
|
928
|
-
messaging: {
|
|
929
|
-
normalizeTarget(raw) {
|
|
930
|
-
try {
|
|
931
|
-
const normalized = normalizeCoolclawTarget(raw);
|
|
932
|
-
parseCoolclawTarget(normalized);
|
|
933
|
-
return normalized;
|
|
934
|
-
} catch {
|
|
935
|
-
return void 0;
|
|
936
|
-
}
|
|
937
|
-
},
|
|
938
|
-
inferTargetChatType({ to }) {
|
|
939
|
-
return inferCoolclawTargetChatType(to);
|
|
940
|
-
},
|
|
941
|
-
targetResolver: {
|
|
942
|
-
hint: "Use coolclaw:human:<id>, coolclaw:agent:<id>, or coolclaw:group:<id>.",
|
|
943
|
-
looksLikeId(raw, normalized) {
|
|
944
|
-
return isCoolclawTargetId(raw, normalized);
|
|
945
|
-
},
|
|
946
|
-
resolveTarget({ input, normalized, preferredKind }) {
|
|
947
|
-
return resolveCoolclawMessagingTarget(normalized || input, preferredKind);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
},
|
|
951
|
-
outbound: {
|
|
952
|
-
deliveryMode: "direct",
|
|
953
|
-
resolveTarget({ to }) {
|
|
954
|
-
if (!to) {
|
|
955
|
-
return { ok: false, error: new Error("CoolClaw target is required") };
|
|
956
|
-
}
|
|
957
|
-
try {
|
|
958
|
-
const normalized = normalizeCoolclawTarget(to);
|
|
959
|
-
parseCoolclawTarget(normalized);
|
|
960
|
-
return { ok: true, to: normalized };
|
|
961
|
-
} catch (error) {
|
|
962
|
-
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
|
|
963
|
-
}
|
|
964
|
-
},
|
|
965
|
-
async sendText(ctx) {
|
|
966
|
-
const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
967
|
-
const token = await resolveAccountToken(account);
|
|
968
|
-
if (!account.gatewayUrl || !account.agentId || !token) {
|
|
969
|
-
throw new Error("CoolClaw account is not fully configured");
|
|
970
|
-
}
|
|
971
|
-
const client = new CoolclawWsClient({
|
|
972
|
-
gatewayUrl: account.gatewayUrl,
|
|
973
|
-
agentId: account.agentId,
|
|
974
|
-
token,
|
|
975
|
-
pluginVersion: "0.1.0",
|
|
976
|
-
ackStore: new InMemoryAckStore(),
|
|
977
|
-
accountKey: `coolclaw:${ctx.accountId ?? "default"}`
|
|
978
|
-
});
|
|
979
|
-
await client.start();
|
|
980
|
-
try {
|
|
981
|
-
const messageId = await sendText({ client, target: ctx.to, text: ctx.text });
|
|
982
|
-
return {
|
|
983
|
-
channel: "coolclaw",
|
|
984
|
-
messageId,
|
|
985
|
-
conversationId: ctx.to,
|
|
986
|
-
timestamp: Date.now()
|
|
987
|
-
};
|
|
988
|
-
} finally {
|
|
989
|
-
await client.stop();
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
},
|
|
993
|
-
status(source) {
|
|
994
|
-
return getCoolclawStatus(source);
|
|
995
|
-
},
|
|
996
|
-
gateway: {
|
|
997
|
-
async startAccount(ctx) {
|
|
998
|
-
const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
|
|
999
|
-
const token = await resolveAccountToken(account);
|
|
1000
|
-
if (!account.gatewayUrl || !account.agentId || !token) {
|
|
1001
|
-
ctx.log?.error(`[${ctx.accountId}] CoolClaw account is not fully configured`);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
|
|
1005
|
-
const ackStore = new FileAckStore();
|
|
1006
|
-
ctx.setStatus({ accountId: ctx.accountId, status: "connecting" });
|
|
1007
|
-
ctx.log?.info(`[${ctx.accountId}] starting CoolClaw provider (${account.gatewayUrl})`);
|
|
1008
|
-
const client = new CoolclawWsClient({
|
|
1009
|
-
gatewayUrl: account.gatewayUrl,
|
|
1010
|
-
agentId: account.agentId,
|
|
1011
|
-
token,
|
|
1012
|
-
pluginVersion: "0.1.0",
|
|
1013
|
-
ackStore,
|
|
1014
|
-
accountKey,
|
|
1015
|
-
onFrame: async (frame, wsClient) => {
|
|
1016
|
-
try {
|
|
1017
|
-
await handleInboundFrame({
|
|
1018
|
-
frame,
|
|
1019
|
-
accountKey,
|
|
1020
|
-
ackStore,
|
|
1021
|
-
accountConfig: { allowFrom: account.allowFrom, dmPolicy: account.dmPolicy },
|
|
1022
|
-
dispatch: async (envelope) => {
|
|
1023
|
-
if (!ctx.channelRuntime) {
|
|
1024
|
-
ctx.log?.warn("channelRuntime not available; skipping dispatch");
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
try {
|
|
1028
|
-
const isGroup = envelope.conversationId.startsWith("group:");
|
|
1029
|
-
const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
|
|
1030
|
-
const route = ctx.channelRuntime.routing?.resolveAgentRoute ? await ctx.channelRuntime.routing.resolveAgentRoute({
|
|
1031
|
-
cfg: ctx.cfg,
|
|
1032
|
-
channel: "coolclaw",
|
|
1033
|
-
accountId: ctx.accountId,
|
|
1034
|
-
peer
|
|
1035
|
-
}) : { agentId: "main", sessionKey: `coolclaw:${envelope.conversationId}` };
|
|
1036
|
-
const storePath = ctx.channelRuntime.session?.resolveStorePath ? ctx.channelRuntime.session.resolveStorePath(ctx.cfg.session?.store, { agentId: route.agentId }) : "/tmp/openclaw/sessions";
|
|
1037
|
-
const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : "unknown";
|
|
1038
|
-
let deliveryTarget;
|
|
1039
|
-
if (envelope.group) {
|
|
1040
|
-
deliveryTarget = `coolclaw:group:${envelope.group.groupId}`;
|
|
1041
|
-
} else if (envelope.sender) {
|
|
1042
|
-
deliveryTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
|
|
1043
|
-
} else {
|
|
1044
|
-
deliveryTarget = normalizeCoolclawTarget(envelope.conversationId);
|
|
1045
|
-
}
|
|
1046
|
-
const agentHint = envelope.metadata?.agentHint;
|
|
1047
|
-
const bodyForAgent = agentHint ? envelope.text + agentHint : envelope.text;
|
|
1048
|
-
const ctxPayload = ctx.channelRuntime.reply.finalizeInboundContext({
|
|
1049
|
-
Body: envelope.text,
|
|
1050
|
-
BodyForAgent: bodyForAgent,
|
|
1051
|
-
RawBody: envelope.text,
|
|
1052
|
-
CommandBody: envelope.text,
|
|
1053
|
-
From: `coolclaw:${senderLabel}`,
|
|
1054
|
-
To: deliveryTarget,
|
|
1055
|
-
SessionKey: route.sessionKey,
|
|
1056
|
-
AccountId: ctx.accountId,
|
|
1057
|
-
ChatType: isGroup ? "channel" : "direct",
|
|
1058
|
-
CommandAuthorized: true,
|
|
1059
|
-
Provider: "coolclaw",
|
|
1060
|
-
Surface: "coolclaw",
|
|
1061
|
-
Channel: "coolclaw",
|
|
1062
|
-
Peer: peer,
|
|
1063
|
-
Mentioned: envelope.shouldReply
|
|
1064
|
-
});
|
|
1065
|
-
if (ctx.channelRuntime.session?.recordSessionMetaFromInbound) {
|
|
1066
|
-
await ctx.channelRuntime.session.recordSessionMetaFromInbound({
|
|
1067
|
-
storePath,
|
|
1068
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
1069
|
-
ctx: ctxPayload
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
if (ctx.channelRuntime.session?.updateLastRoute) {
|
|
1073
|
-
const sessionKey = ctxPayload.SessionKey ?? route.sessionKey;
|
|
1074
|
-
const lastRouteCtx = {
|
|
1075
|
-
storePath,
|
|
1076
|
-
sessionKey,
|
|
1077
|
-
deliveryContext: {
|
|
1078
|
-
channel: "coolclaw",
|
|
1079
|
-
to: deliveryTarget,
|
|
1080
|
-
accountId: ctx.accountId ?? void 0
|
|
1081
|
-
},
|
|
1082
|
-
ctx: ctxPayload
|
|
1083
|
-
};
|
|
1084
|
-
try {
|
|
1085
|
-
await ctx.channelRuntime.session.updateLastRoute(lastRouteCtx);
|
|
1086
|
-
} catch (err) {
|
|
1087
|
-
ctx.log?.warn(
|
|
1088
|
-
`updateLastRoute failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
const mainSessionKey = route.mainSessionKey;
|
|
1092
|
-
if (mainSessionKey && mainSessionKey !== sessionKey) {
|
|
1093
|
-
try {
|
|
1094
|
-
await ctx.channelRuntime.session.updateLastRoute({
|
|
1095
|
-
...lastRouteCtx,
|
|
1096
|
-
sessionKey: mainSessionKey,
|
|
1097
|
-
ctx: void 0
|
|
1098
|
-
});
|
|
1099
|
-
} catch (err) {
|
|
1100
|
-
ctx.log?.warn(
|
|
1101
|
-
`updateLastRoute (main) failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1102
|
-
);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
await ctx.channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1107
|
-
ctx: ctxPayload,
|
|
1108
|
-
cfg: ctx.cfg,
|
|
1109
|
-
dispatcherOptions: {
|
|
1110
|
-
deliver: async (payload) => {
|
|
1111
|
-
if (!payload.text) return;
|
|
1112
|
-
try {
|
|
1113
|
-
let replyTarget;
|
|
1114
|
-
if (envelope.group) {
|
|
1115
|
-
replyTarget = `coolclaw:group:${envelope.group.groupId}`;
|
|
1116
|
-
} else if (envelope.sender) {
|
|
1117
|
-
replyTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
|
|
1118
|
-
} else {
|
|
1119
|
-
replyTarget = normalizeCoolclawTarget(envelope.conversationId);
|
|
1120
|
-
}
|
|
1121
|
-
await sendText({ client: wsClient, target: replyTarget, text: payload.text });
|
|
1122
|
-
} catch (err) {
|
|
1123
|
-
ctx.log?.error(`Failed to deliver reply: ${err instanceof Error ? err.message : String(err)}`);
|
|
1124
|
-
}
|
|
1125
|
-
},
|
|
1126
|
-
onError: (err, info) => {
|
|
1127
|
-
ctx.log?.error(`Reply dispatch error: ${err instanceof Error ? err.message : String(err)}`, info);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
} catch (err) {
|
|
1132
|
-
ctx.log?.error(`Inbound dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1133
|
-
}
|
|
1134
|
-
},
|
|
1135
|
-
sendAck: async (ackFrame) => {
|
|
1136
|
-
try {
|
|
1137
|
-
wsClient.sendFrame(ackFrame);
|
|
1138
|
-
ctx.log?.debug(`ACK sent: type=${ackFrame.type} lastAckedSeq=${ackFrame.payload?.lastAckedSeq}`);
|
|
1139
|
-
} catch (err) {
|
|
1140
|
-
ctx.log?.warn(`ACK send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
1144
|
-
} catch (err) {
|
|
1145
|
-
ctx.log?.error(`Frame handling error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
|
-
await client.start();
|
|
1150
|
-
ctx.setStatus({ accountId: ctx.accountId, status: "connected" });
|
|
1151
|
-
ctx.log?.info(`[${ctx.accountId}] CoolClaw provider connected`);
|
|
1152
|
-
await new Promise((resolve) => {
|
|
1153
|
-
ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
1154
|
-
});
|
|
1155
|
-
await client.stop();
|
|
1156
|
-
ctx.setStatus({ accountId: ctx.accountId, status: "disconnected" });
|
|
1157
|
-
ctx.log?.info(`[${ctx.accountId}] CoolClaw provider stopped`);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
};
|
|
1161
|
-
|
|
1162
|
-
export {
|
|
1163
|
-
defaultBindingFile,
|
|
1164
|
-
defaultOpenClawConfigFile,
|
|
1165
|
-
defaultTokenFile,
|
|
1166
|
-
loadBinding,
|
|
1167
|
-
touchBinding,
|
|
1168
|
-
saveBinding,
|
|
1169
|
-
saveAgentToken,
|
|
1170
|
-
readTokenRef,
|
|
1171
|
-
normalizeGatewayUrl,
|
|
1172
|
-
coolclawChannelPlugin
|
|
1173
|
-
};
|