@coolclaw/coolclaw 0.2.9 → 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 +53 -4
- package/dist/chunk-A54AF634.js +344 -0
- package/dist/chunk-CTTA5JQL.js +123 -0
- package/dist/chunk-GNPSYUMN.js +1113 -0
- package/dist/chunk-Q3NF4NWE.js +105 -0
- package/dist/cli-metadata.d.ts +4 -3
- package/dist/cli-metadata.js +6 -1533
- package/dist/index.d.ts +8 -21
- package/dist/index.js +7 -1536
- package/dist/setup-T2A3RRG7.js +13 -0
- package/dist/setup-entry.d.ts +3 -2
- package/dist/setup-entry.js +6 -1110
- package/dist/types-y7-Cr6xf.d.ts +27 -0
- package/openclaw.plugin.json +35 -1
- package/package.json +16 -9
- package/dist/channel-C5YYO-tp.d.ts +0 -115
package/dist/index.js
CHANGED
|
@@ -1,1538 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
// src/setup.ts
|
|
1163
|
-
import { access } from "fs/promises";
|
|
1164
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1165
|
-
import path4 from "path";
|
|
1166
|
-
import { homedir as homedir3 } from "os";
|
|
1167
|
-
|
|
1168
|
-
// src/identity.ts
|
|
1169
|
-
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
1170
|
-
import path2 from "path";
|
|
1171
|
-
var BEGIN_MARKER = "<!-- BEGIN_RIDDLE_IDENTITY -->";
|
|
1172
|
-
var END_MARKER = "<!-- END_RIDDLE_IDENTITY -->";
|
|
1173
|
-
async function updateRiddleIdentitySummary(summary) {
|
|
1174
|
-
await mkdir2(summary.workspaceDir, { recursive: true });
|
|
1175
|
-
const identityFile = path2.join(summary.workspaceDir, "IDENTITY.md");
|
|
1176
|
-
const existing = await readExisting(identityFile);
|
|
1177
|
-
const block = [
|
|
1178
|
-
BEGIN_MARKER,
|
|
1179
|
-
"## Riddle Platform",
|
|
1180
|
-
"",
|
|
1181
|
-
`Riddle Agent ID: ${summary.agentId}`,
|
|
1182
|
-
`Riddle Gateway: ${normalizeGatewayUrl(summary.gatewayUrl)}`,
|
|
1183
|
-
`Riddle Binding: ${summary.bindingFile}`,
|
|
1184
|
-
"Messaging: handled by the CoolClaw OpenClaw channel.",
|
|
1185
|
-
"Non-message platform actions: handled by the Riddle skill.",
|
|
1186
|
-
END_MARKER
|
|
1187
|
-
].join("\n");
|
|
1188
|
-
const next = replaceBlock(existing, block);
|
|
1189
|
-
await writeFile2(identityFile, next.endsWith("\n") ? next : `${next}
|
|
1190
|
-
`);
|
|
1191
|
-
}
|
|
1192
|
-
async function readExisting(identityFile) {
|
|
1193
|
-
try {
|
|
1194
|
-
return await readFile2(identityFile, "utf8");
|
|
1195
|
-
} catch {
|
|
1196
|
-
return "";
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
function replaceBlock(existing, block) {
|
|
1200
|
-
const start = existing.indexOf(BEGIN_MARKER);
|
|
1201
|
-
const end = existing.indexOf(END_MARKER);
|
|
1202
|
-
if (start >= 0 && end >= start) {
|
|
1203
|
-
return `${existing.slice(0, start).trimEnd()}
|
|
1204
|
-
|
|
1205
|
-
${block}
|
|
1206
|
-
${existing.slice(end + END_MARKER.length).trimStart()}`;
|
|
1207
|
-
}
|
|
1208
|
-
return existing.trim() ? `${existing.trimEnd()}
|
|
1209
|
-
|
|
1210
|
-
${block}
|
|
1211
|
-
` : `${block}
|
|
1212
|
-
`;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// src/openclaw-config.ts
|
|
1216
|
-
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
1217
|
-
import path3 from "path";
|
|
1218
|
-
async function patchCoolclawAccountConfig(patch) {
|
|
1219
|
-
const config = await readOpenClawConfig(patch.configPath);
|
|
1220
|
-
const channels = ensureRecord(config, "channels");
|
|
1221
|
-
const coolclaw = ensureRecord(channels, "coolclaw");
|
|
1222
|
-
const accounts = ensureRecord(coolclaw, "accounts");
|
|
1223
|
-
const account = {
|
|
1224
|
-
name: `Riddle Agent ${patch.agentId}`,
|
|
1225
|
-
enabled: true,
|
|
1226
|
-
gatewayUrl: normalizeGatewayUrl(patch.gatewayUrl),
|
|
1227
|
-
agentId: patch.agentId,
|
|
1228
|
-
tokenSecretRef: patch.tokenSecretRef,
|
|
1229
|
-
dmPolicy: patch.dmPolicy ?? "open"
|
|
1230
|
-
};
|
|
1231
|
-
const effectiveDmPolicy = account.dmPolicy;
|
|
1232
|
-
if (patch.allowFrom && patch.allowFrom.length > 0) {
|
|
1233
|
-
account.allowFrom = patch.allowFrom;
|
|
1234
|
-
} else if (effectiveDmPolicy === "open") {
|
|
1235
|
-
account.allowFrom = ["*"];
|
|
1236
|
-
}
|
|
1237
|
-
accounts.default = account;
|
|
1238
|
-
await writeOpenClawConfig(patch.configPath, config);
|
|
1239
|
-
}
|
|
1240
|
-
async function readOpenClawConfig(configPath) {
|
|
1241
|
-
try {
|
|
1242
|
-
const parsed = JSON.parse(await readFile3(configPath, "utf8"));
|
|
1243
|
-
return isRecord5(parsed) ? parsed : {};
|
|
1244
|
-
} catch {
|
|
1245
|
-
return {};
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
async function writeOpenClawConfig(configPath, config) {
|
|
1249
|
-
await mkdir3(path3.dirname(configPath), { recursive: true });
|
|
1250
|
-
const tmpFile = `${configPath}.${process.pid}.tmp`;
|
|
1251
|
-
await writeFile3(tmpFile, `${JSON.stringify(config, null, 2)}
|
|
1252
|
-
`);
|
|
1253
|
-
await rename2(tmpFile, configPath);
|
|
1254
|
-
}
|
|
1255
|
-
function ensureRecord(parent, key) {
|
|
1256
|
-
const existing = parent[key];
|
|
1257
|
-
if (isRecord5(existing)) return existing;
|
|
1258
|
-
const created = {};
|
|
1259
|
-
parent[key] = created;
|
|
1260
|
-
return created;
|
|
1261
|
-
}
|
|
1262
|
-
function isRecord5(value) {
|
|
1263
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// src/setup.ts
|
|
1267
|
-
async function registerAgent(baseUrl, input) {
|
|
1268
|
-
const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/agent/register`, {
|
|
1269
|
-
method: "POST",
|
|
1270
|
-
headers: { "Content-Type": "application/json" },
|
|
1271
|
-
body: JSON.stringify(input)
|
|
1272
|
-
});
|
|
1273
|
-
const body = await res.json();
|
|
1274
|
-
if (!res.ok || body.code !== 200) {
|
|
1275
|
-
throw new Error(body.message ?? `Agent register failed: ${res.status}`);
|
|
1276
|
-
}
|
|
1277
|
-
if (!body.data?.agentId || !body.data.token) {
|
|
1278
|
-
throw new Error("Agent register response missing agentId or token");
|
|
1279
|
-
}
|
|
1280
|
-
return {
|
|
1281
|
-
agentId: String(body.data.agentId),
|
|
1282
|
-
token: String(body.data.token)
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
async function validateAgentToken(baseUrl, token) {
|
|
1286
|
-
const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/users/me`, {
|
|
1287
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1288
|
-
});
|
|
1289
|
-
if (!res.ok) return false;
|
|
1290
|
-
const body = await res.json();
|
|
1291
|
-
return body.code === 200 && body.data?.identityType === "AGENT";
|
|
1292
|
-
}
|
|
1293
|
-
async function runCoolclawSetup(options = {}) {
|
|
1294
|
-
const gatewayUrl = normalizeGatewayUrl(options.gatewayUrl ?? "https://agits-xa.baidu.com/riddle");
|
|
1295
|
-
const bindingFile = options.bindingFile ?? defaultBindingFile();
|
|
1296
|
-
const openclawConfigPath = options.openclawConfigPath ?? defaultOpenClawConfigFile();
|
|
1297
|
-
const existingBinding = await loadBinding(bindingFile);
|
|
1298
|
-
const existingToken = await readTokenRef(existingBinding.tokenRef);
|
|
1299
|
-
if (options.dryRun) {
|
|
1300
|
-
return {
|
|
1301
|
-
mode: "dry-run",
|
|
1302
|
-
gatewayUrl,
|
|
1303
|
-
agentId: existingBinding.agentId,
|
|
1304
|
-
bindingFile,
|
|
1305
|
-
openclawConfigPath,
|
|
1306
|
-
tokenSavedTo: tokenFileFromBinding(existingBinding, bindingFile)
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
const canReuse = !options.forceRegister && existingBinding.agentId.length > 0 && Boolean(existingToken) && await validateAgentToken(gatewayUrl, existingToken);
|
|
1310
|
-
const binding = canReuse ? existingBinding : await registerAndPersistBinding({
|
|
1311
|
-
gatewayUrl,
|
|
1312
|
-
bindingFile,
|
|
1313
|
-
registration: await resolveRegistrationInput(
|
|
1314
|
-
options.registration,
|
|
1315
|
-
options.workspaceDir,
|
|
1316
|
-
openclawConfigPath
|
|
1317
|
-
)
|
|
1318
|
-
});
|
|
1319
|
-
const tokenFile = tokenFileFromBinding(binding, bindingFile);
|
|
1320
|
-
if (!options.dryRun) {
|
|
1321
|
-
await patchCoolclawAccountConfig({
|
|
1322
|
-
configPath: openclawConfigPath,
|
|
1323
|
-
gatewayUrl,
|
|
1324
|
-
agentId: binding.agentId,
|
|
1325
|
-
tokenSecretRef: binding.tokenRef ?? `file://${tokenFile}`,
|
|
1326
|
-
allowFrom: options.allowFrom,
|
|
1327
|
-
dmPolicy: options.dmPolicy
|
|
1328
|
-
});
|
|
1329
|
-
if (options.workspaceDir) {
|
|
1330
|
-
await updateRiddleIdentitySummary({
|
|
1331
|
-
workspaceDir: options.workspaceDir,
|
|
1332
|
-
agentId: binding.agentId,
|
|
1333
|
-
gatewayUrl,
|
|
1334
|
-
bindingFile,
|
|
1335
|
-
tokenRef: binding.tokenRef ?? void 0
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return {
|
|
1340
|
-
mode: options.dryRun ? "dry-run" : canReuse ? "reuse" : "register",
|
|
1341
|
-
gatewayUrl,
|
|
1342
|
-
agentId: binding.agentId,
|
|
1343
|
-
bindingFile,
|
|
1344
|
-
openclawConfigPath,
|
|
1345
|
-
tokenSavedTo: tokenFile
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
async function registerAndPersistBinding(input) {
|
|
1349
|
-
const registered = await registerAgent(input.gatewayUrl, input.registration);
|
|
1350
|
-
const valid = await validateAgentToken(input.gatewayUrl, registered.token);
|
|
1351
|
-
if (!valid) {
|
|
1352
|
-
throw new Error("Newly registered Riddle token did not validate as an AGENT token");
|
|
1353
|
-
}
|
|
1354
|
-
const tokenFile = defaultTokenFile(input.bindingFile, registered.agentId);
|
|
1355
|
-
await saveAgentToken(tokenFile, registered.token);
|
|
1356
|
-
const binding = touchBinding({
|
|
1357
|
-
agentId: registered.agentId,
|
|
1358
|
-
tokenRef: `file://${tokenFile}`,
|
|
1359
|
-
runtimeType: "openclaw",
|
|
1360
|
-
lastAckedSeq: 0
|
|
1361
|
-
});
|
|
1362
|
-
await saveBinding(input.bindingFile, binding);
|
|
1363
|
-
return binding;
|
|
1364
|
-
}
|
|
1365
|
-
function tokenFileFromBinding(binding, bindingFile) {
|
|
1366
|
-
if (binding.tokenRef?.startsWith("file://")) {
|
|
1367
|
-
return binding.tokenRef.slice("file://".length);
|
|
1368
|
-
}
|
|
1369
|
-
return defaultTokenFile(bindingFile, binding.agentId);
|
|
1370
|
-
}
|
|
1371
|
-
var IDENTITY_PLACEHOLDERS = /* @__PURE__ */ new Set([
|
|
1372
|
-
"pick something you like",
|
|
1373
|
-
"ai? robot? familiar? ghost in the machine? something weirder?",
|
|
1374
|
-
"how do you come across? sharp? warm? chaotic? calm?",
|
|
1375
|
-
"your signature - pick one that feels right",
|
|
1376
|
-
"workspace-relative path, http(s) url, or data uri"
|
|
1377
|
-
]);
|
|
1378
|
-
function parseIdentityMarkdown(content) {
|
|
1379
|
-
const result = {};
|
|
1380
|
-
for (const line of content.split(/\r?\n/)) {
|
|
1381
|
-
const cleaned = line.trim().replace(/^\s*-\s*/, "");
|
|
1382
|
-
const colonIdx = cleaned.indexOf(":");
|
|
1383
|
-
if (colonIdx === -1) continue;
|
|
1384
|
-
const label = cleaned.slice(0, colonIdx).replace(/[*_]/g, "").trim().toLowerCase();
|
|
1385
|
-
const value = cleaned.slice(colonIdx + 1).replace(/^[*_]+|[*_]+$/g, "").trim();
|
|
1386
|
-
if (!value || IDENTITY_PLACEHOLDERS.has(value.toLowerCase().replace(/[()]/g, "").trim())) continue;
|
|
1387
|
-
if (label === "name") result.name = value;
|
|
1388
|
-
if (label === "creature") result.creature = value;
|
|
1389
|
-
if (label === "vibe") result.vibe = value;
|
|
1390
|
-
if (label === "theme") result.theme = value;
|
|
1391
|
-
}
|
|
1392
|
-
return result;
|
|
1393
|
-
}
|
|
1394
|
-
async function readWorkspaceDirFromOpenclawConfig(configPath) {
|
|
1395
|
-
try {
|
|
1396
|
-
const content = await readFile4(configPath, "utf-8");
|
|
1397
|
-
const config = JSON.parse(content);
|
|
1398
|
-
const workspace = config.agents?.defaults?.workspace;
|
|
1399
|
-
if (typeof workspace === "string" && workspace.trim()) {
|
|
1400
|
-
return workspace.trim();
|
|
1401
|
-
}
|
|
1402
|
-
} catch {
|
|
1403
|
-
}
|
|
1404
|
-
const defaultWorkspace = path4.join(homedir3(), ".openclaw", "workspace");
|
|
1405
|
-
try {
|
|
1406
|
-
await access(defaultWorkspace);
|
|
1407
|
-
return defaultWorkspace;
|
|
1408
|
-
} catch {
|
|
1409
|
-
return void 0;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
async function readIdentityFromWorkspace(workspaceDir) {
|
|
1413
|
-
try {
|
|
1414
|
-
const content = await readFile4(path4.join(workspaceDir, "IDENTITY.md"), "utf-8");
|
|
1415
|
-
return parseIdentityMarkdown(content);
|
|
1416
|
-
} catch {
|
|
1417
|
-
return {};
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
async function readIdentityFromOpenclawConfig(configPath) {
|
|
1421
|
-
try {
|
|
1422
|
-
const content = await readFile4(configPath, "utf-8");
|
|
1423
|
-
const config = JSON.parse(content);
|
|
1424
|
-
const defaultsName = config.agents?.defaults?.identity?.name || config.agents?.defaults?.name;
|
|
1425
|
-
if (defaultsName) return { name: defaultsName };
|
|
1426
|
-
const mainAgent = config.agents?.list?.find((a) => a.id === "main") || config.agents?.list?.[0];
|
|
1427
|
-
if (mainAgent?.identity?.name || mainAgent?.name) {
|
|
1428
|
-
return { name: mainAgent.identity?.name || mainAgent.name };
|
|
1429
|
-
}
|
|
1430
|
-
return {};
|
|
1431
|
-
} catch {
|
|
1432
|
-
return {};
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
async function resolveRegistrationInput(explicitRegistration, workspaceDir, openclawConfigPath) {
|
|
1436
|
-
if (explicitRegistration?.name && explicitRegistration?.bio && explicitRegistration.name !== "CoolClaw Agent" && explicitRegistration.bio !== "OpenClaw agent connected through CoolClaw channel.") {
|
|
1437
|
-
return explicitRegistration;
|
|
1438
|
-
}
|
|
1439
|
-
let identityName;
|
|
1440
|
-
let identityBio;
|
|
1441
|
-
const resolvedWorkspaceDir = workspaceDir ?? await readWorkspaceDirFromOpenclawConfig(openclawConfigPath);
|
|
1442
|
-
if (resolvedWorkspaceDir) {
|
|
1443
|
-
const identity = await readIdentityFromWorkspace(resolvedWorkspaceDir);
|
|
1444
|
-
if (identity.name) {
|
|
1445
|
-
identityName = identity.name;
|
|
1446
|
-
}
|
|
1447
|
-
const bioParts = [identity.creature, identity.vibe, identity.theme].filter(Boolean);
|
|
1448
|
-
if (bioParts.length > 0) {
|
|
1449
|
-
identityBio = bioParts.join(". ") + ".";
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
if (!identityName) {
|
|
1453
|
-
const configIdentity = await readIdentityFromOpenclawConfig(openclawConfigPath);
|
|
1454
|
-
identityName = configIdentity.name;
|
|
1455
|
-
}
|
|
1456
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(4, 12);
|
|
1457
|
-
return {
|
|
1458
|
-
name: explicitRegistration?.name && explicitRegistration.name !== "CoolClaw Agent" ? explicitRegistration.name : identityName ?? `RiddleAgent-${stamp}`,
|
|
1459
|
-
bio: explicitRegistration?.bio && explicitRegistration.bio !== "OpenClaw agent connected through CoolClaw channel." ? explicitRegistration.bio : identityBio ?? "OpenClaw agent connected through CoolClaw channel.",
|
|
1460
|
-
tags: explicitRegistration?.tags ?? JSON.stringify(["assistant", "openclaw", "coolclaw"])
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
// src/cli.ts
|
|
1465
|
-
function registerCoolclawCli(options) {
|
|
1466
|
-
const setup = options.setup ?? runCoolclawSetup;
|
|
1467
|
-
const coolclaw = options.program.command("coolclaw").description("Manage the CoolClaw/Riddle channel");
|
|
1468
|
-
coolclaw.command("setup").description("Register or reuse a Riddle agent and configure the CoolClaw channel").option("--gateway-url <url>", "Riddle gateway URL", "https://agits-xa.baidu.com/riddle").option("--binding-file <path>", "Shared Riddle binding file", defaultBindingFile()).option("--openclaw-config <path>", "OpenClaw config file").option("--workspace-dir <path>", "OpenClaw agent workspace directory", options.workspaceDir).option("--name <name>", "Riddle agent display name").option("--bio <bio>", "Riddle agent bio").option("--tags <tags>", "Riddle agent tags").option("--allow-from <items>", "Comma-separated DM allowlist").option("--dm-policy <policy>", "DM policy: allowlist or pairing", "open").option("--force-register", "Register a new Riddle agent even if a valid binding exists", false).option("--dry-run", "Resolve setup inputs without writing local files", false).option("-y, --yes", "Accept defaults for non-interactive setup", false).action(async (...args) => {
|
|
1469
|
-
const rawOptions = readActionOptions(args);
|
|
1470
|
-
const result = await setup(toSetupOptions(rawOptions));
|
|
1471
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1472
|
-
});
|
|
1473
|
-
coolclaw.command("status").description("Show the shared Riddle binding location used by CoolClaw").action(() => {
|
|
1474
|
-
console.log(JSON.stringify({ bindingFile: defaultBindingFile() }, null, 2));
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
function readActionOptions(args) {
|
|
1478
|
-
const candidate = args.at(-1);
|
|
1479
|
-
return typeof candidate === "object" && candidate !== null ? candidate : {};
|
|
1480
|
-
}
|
|
1481
|
-
function toSetupOptions(raw) {
|
|
1482
|
-
const explicitName = stringOption(raw.name);
|
|
1483
|
-
const explicitBio = stringOption(raw.bio);
|
|
1484
|
-
return {
|
|
1485
|
-
gatewayUrl: stringOption(raw.gatewayUrl),
|
|
1486
|
-
bindingFile: stringOption(raw.bindingFile),
|
|
1487
|
-
openclawConfigPath: stringOption(raw.openclawConfig),
|
|
1488
|
-
workspaceDir: stringOption(raw.workspaceDir),
|
|
1489
|
-
registration: explicitName || explicitBio ? {
|
|
1490
|
-
name: explicitName ?? "CoolClaw Agent",
|
|
1491
|
-
bio: explicitBio ?? "OpenClaw agent connected through CoolClaw channel.",
|
|
1492
|
-
tags: stringOption(raw.tags) ?? JSON.stringify(["assistant", "openclaw", "coolclaw"])
|
|
1493
|
-
} : void 0,
|
|
1494
|
-
allowFrom: splitCsv2(stringOption(raw.allowFrom)),
|
|
1495
|
-
dmPolicy: raw.dmPolicy === "pairing" ? "pairing" : "open",
|
|
1496
|
-
forceRegister: Boolean(raw.forceRegister),
|
|
1497
|
-
dryRun: Boolean(raw.dryRun),
|
|
1498
|
-
yes: Boolean(raw.yes)
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
function stringOption(value) {
|
|
1502
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1503
|
-
}
|
|
1504
|
-
function splitCsv2(value) {
|
|
1505
|
-
if (!value) return void 0;
|
|
1506
|
-
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1507
|
-
return items.length > 0 ? items : void 0;
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// index.ts
|
|
1511
|
-
function _register(api) {
|
|
1512
|
-
api.registerChannel({ plugin: coolclawChannelPlugin });
|
|
1513
|
-
api.registerCli?.(
|
|
1514
|
-
({ program, workspaceDir }) => {
|
|
1515
|
-
registerCoolclawCli({ program, workspaceDir });
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
descriptors: [
|
|
1519
|
-
{
|
|
1520
|
-
name: "coolclaw",
|
|
1521
|
-
description: "Manage the CoolClaw/Riddle channel",
|
|
1522
|
-
hasSubcommands: true
|
|
1523
|
-
}
|
|
1524
|
-
]
|
|
1525
|
-
}
|
|
1526
|
-
);
|
|
1527
|
-
}
|
|
1528
|
-
var register = _register;
|
|
1529
|
-
var index_default = {
|
|
1530
|
-
id: "coolclaw",
|
|
1531
|
-
name: "CoolClaw Channel",
|
|
1532
|
-
description: "CoolClaw/Riddle messaging channel for OpenClaw",
|
|
1533
|
-
register: _register
|
|
1534
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
index_default
|
|
3
|
+
} from "./chunk-CTTA5JQL.js";
|
|
4
|
+
import "./chunk-GNPSYUMN.js";
|
|
5
|
+
import "./chunk-A54AF634.js";
|
|
6
|
+
import "./chunk-Q3NF4NWE.js";
|
|
1535
7
|
export {
|
|
1536
|
-
index_default as default
|
|
1537
|
-
register
|
|
8
|
+
index_default as default
|
|
1538
9
|
};
|