@coolclaw/coolclaw 0.2.9 → 0.2.10

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.
@@ -1,71 +1,11 @@
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
- }
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
5
  };
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
- }
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
69
9
  };
70
10
 
71
11
  // src/binding.ts
@@ -73,7 +13,6 @@ import { mkdir, readFile, rename, writeFile, chmod } from "fs/promises";
73
13
  import { homedir as homedir2 } from "os";
74
14
  import path from "path";
75
15
  import { fileURLToPath } from "url";
76
- var RIDDLE_BINDING_VERSION = 1;
77
16
  function defaultBindingFile(home = homedir2()) {
78
17
  return path.join(home, ".config", "coolclaw", "agent_binding.json");
79
18
  }
@@ -135,8 +74,16 @@ async function readTokenRef(tokenRef) {
135
74
  }
136
75
  return void 0;
137
76
  }
77
+ var RIDDLE_BINDING_VERSION;
78
+ var init_binding = __esm({
79
+ "src/binding.ts"() {
80
+ "use strict";
81
+ RIDDLE_BINDING_VERSION = 1;
82
+ }
83
+ });
138
84
 
139
85
  // src/config.ts
86
+ import { z } from "zod";
140
87
  function normalizeGatewayUrl(value) {
141
88
  return value.trim().replace(/\/+$/, "");
142
89
  }
@@ -144,370 +91,671 @@ function buildWsUrl(gatewayUrl, lastAckedSeq) {
144
91
  const baseUrl = normalizeGatewayUrl(gatewayUrl).replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
145
92
  return `${baseUrl}/ws/channel?lastAckedSeq=${lastAckedSeq}`;
146
93
  }
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
94
  async function resolveAccountToken(account) {
177
95
  if (account.tokenSecret) return account.tokenSecret;
178
96
  return readTokenRef(account.tokenSecretRef);
179
97
  }
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;
98
+ var CoolclawConfigSchema;
99
+ var init_config = __esm({
100
+ "src/config.ts"() {
101
+ "use strict";
102
+ init_binding();
103
+ CoolclawConfigSchema = z.object({
104
+ gatewayUrl: z.string().url().optional().describe("Riddle gateway URL"),
105
+ agentId: z.string().optional().describe("Riddle agent ID"),
106
+ tokenSecretRef: z.string().optional().describe("Token secret reference (file:// or env:)"),
107
+ allowFrom: z.array(z.string()).optional().describe("Allowed sender IDs"),
108
+ dmPolicy: z.enum(["allowlist", "pairing", "open"]).optional().describe("DM access policy"),
109
+ enabled: z.boolean().optional().describe("Whether the account is enabled"),
110
+ name: z.string().optional().describe("Account display name")
111
+ });
194
112
  }
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
- };
113
+ });
114
+
115
+ // src/identity.ts
116
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
117
+ import path2 from "path";
118
+ async function updateRiddleIdentitySummary(summary) {
119
+ await mkdir2(summary.workspaceDir, { recursive: true });
120
+ const identityFile = path2.join(summary.workspaceDir, "IDENTITY.md");
121
+ const existing = await readExisting(identityFile);
122
+ const block = [
123
+ BEGIN_MARKER,
124
+ "## Riddle Platform",
125
+ "",
126
+ `Riddle Agent ID: ${summary.agentId}`,
127
+ `Riddle Gateway: ${normalizeGatewayUrl(summary.gatewayUrl)}`,
128
+ `Riddle Binding: ${summary.bindingFile}`,
129
+ "Messaging: handled by the CoolClaw OpenClaw channel.",
130
+ "Non-message platform actions: handled by the Riddle skill.",
131
+ END_MARKER
132
+ ].join("\n");
133
+ const next = replaceBlock(existing, block);
134
+ await writeFile2(identityFile, next.endsWith("\n") ? next : `${next}
135
+ `);
202
136
  }
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
- };
137
+ async function readExisting(identityFile) {
138
+ try {
139
+ return await readFile2(identityFile, "utf8");
140
+ } catch {
141
+ return "";
227
142
  }
228
- return {
229
- configured: true,
230
- source,
231
- config,
232
- reasons: []
233
- };
234
143
  }
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
- };
144
+ function replaceBlock(existing, block) {
145
+ const start = existing.indexOf(BEGIN_MARKER);
146
+ const end = existing.indexOf(END_MARKER);
147
+ if (start >= 0 && end >= start) {
148
+ return `${existing.slice(0, start).trimEnd()}
149
+
150
+ ${block}
151
+ ${existing.slice(end + END_MARKER.length).trimStart()}`;
152
+ }
153
+ return existing.trim() ? `${existing.trimEnd()}
154
+
155
+ ${block}
156
+ ` : `${block}
157
+ `;
244
158
  }
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;
159
+ var BEGIN_MARKER, END_MARKER;
160
+ var init_identity = __esm({
161
+ "src/identity.ts"() {
162
+ "use strict";
163
+ init_config();
164
+ BEGIN_MARKER = "<!-- BEGIN_RIDDLE_IDENTITY -->";
165
+ END_MARKER = "<!-- END_RIDDLE_IDENTITY -->";
166
+ }
167
+ });
168
+
169
+ // src/openclaw-config.ts
170
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
171
+ import path3 from "path";
172
+ async function patchCoolclawAccountConfig(patch) {
173
+ const config = await readOpenClawConfig(patch.configPath);
174
+ const channels = ensureRecord(config, "channels");
175
+ const coolclaw = ensureRecord(channels, "coolclaw");
176
+ const accounts = ensureRecord(coolclaw, "accounts");
177
+ const account = {
178
+ name: `Riddle Agent ${patch.agentId}`,
179
+ enabled: true,
180
+ gatewayUrl: normalizeGatewayUrl(patch.gatewayUrl),
181
+ agentId: patch.agentId,
182
+ tokenSecretRef: patch.tokenSecretRef,
183
+ dmPolicy: patch.dmPolicy ?? "open"
184
+ };
185
+ const effectiveDmPolicy = account.dmPolicy;
186
+ if (patch.allowFrom && patch.allowFrom.length > 0) {
187
+ account.allowFrom = patch.allowFrom;
188
+ } else if (effectiveDmPolicy === "open") {
189
+ account.allowFrom = ["*"];
190
+ }
191
+ accounts.default = account;
192
+ await writeOpenClawConfig(patch.configPath, config);
249
193
  }
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;
194
+ async function readOpenClawConfig(configPath) {
195
+ try {
196
+ const parsed = JSON.parse(await readFile3(configPath, "utf8"));
197
+ return isRecord4(parsed) ? parsed : {};
198
+ } catch {
199
+ return {};
200
+ }
254
201
  }
255
- function coerceDmPolicy(value) {
256
- return value === "pairing" || value === "allowlist" || value === "open" ? value : void 0;
202
+ async function writeOpenClawConfig(configPath, config) {
203
+ await mkdir3(path3.dirname(configPath), { recursive: true });
204
+ const tmpFile = `${configPath}.${process.pid}.tmp`;
205
+ await writeFile3(tmpFile, `${JSON.stringify(config, null, 2)}
206
+ `);
207
+ await rename2(tmpFile, configPath);
257
208
  }
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}` : "***";
209
+ function ensureRecord(parent, key) {
210
+ const existing = parent[key];
211
+ if (isRecord4(existing)) return existing;
212
+ const created = {};
213
+ parent[key] = created;
214
+ return created;
263
215
  }
264
- function isRecord(value) {
265
- return typeof value === "object" && value !== null;
216
+ function isRecord4(value) {
217
+ return typeof value === "object" && value !== null && !Array.isArray(value);
266
218
  }
219
+ var init_openclaw_config = __esm({
220
+ "src/openclaw-config.ts"() {
221
+ "use strict";
222
+ init_config();
223
+ }
224
+ });
267
225
 
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";
226
+ // src/setup.ts
227
+ var setup_exports = {};
228
+ __export(setup_exports, {
229
+ registerAgent: () => registerAgent,
230
+ resolveSetupBinding: () => resolveSetupBinding,
231
+ runCoolclawSetup: () => runCoolclawSetup,
232
+ validateAgentToken: () => validateAgentToken
233
+ });
234
+ import { access } from "fs/promises";
235
+ import { readFile as readFile4 } from "fs/promises";
236
+ import path4 from "path";
237
+ import { homedir as homedir3 } from "os";
238
+ async function registerAgent(baseUrl, input) {
239
+ const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/agent/register`, {
240
+ method: "POST",
241
+ headers: { "Content-Type": "application/json" },
242
+ body: JSON.stringify(input)
243
+ });
244
+ const body = await res.json();
245
+ if (!res.ok || body.code !== 200) {
246
+ throw new Error(body.message ?? `Agent register failed: ${res.status}`);
247
+ }
248
+ if (!body.data?.agentId || !body.data.token) {
249
+ throw new Error("Agent register response missing agentId or token");
274
250
  }
275
- };
276
- function createFrame(type, payload) {
277
251
  return {
278
- v: 1,
279
- type,
280
- id: `cli_${randomUUID()}`,
281
- ts: Date.now(),
282
- payload
252
+ agentId: String(body.data.agentId),
253
+ token: String(body.data.token)
283
254
  };
284
255
  }
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");
256
+ async function resolveSetupBinding(input) {
257
+ const gatewayUrl = normalizeGatewayUrl(input.gatewayUrl);
258
+ const existingAgentId = input.existingAgentId?.trim();
259
+ const existingTokenSecretRef = input.existingTokenSecretRef?.trim();
260
+ if (existingAgentId && existingTokenSecretRef) {
261
+ return {
262
+ mode: "reuse",
263
+ gatewayUrl,
264
+ agentId: existingAgentId,
265
+ tokenSecretRef: existingTokenSecretRef
266
+ };
312
267
  }
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");
268
+ if (!input.registration) {
269
+ throw new Error("CoolClaw setup requires an existing binding or registration input");
315
270
  }
316
- const frame = {
317
- v: 1,
318
- type: value.type,
319
- id: value.id,
320
- ts: value.ts
271
+ const registered = await registerAgent(gatewayUrl, input.registration);
272
+ return {
273
+ mode: "register",
274
+ gatewayUrl,
275
+ agentId: registered.agentId,
276
+ token: registered.token
321
277
  };
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
278
  }
330
- function isRecord2(value) {
331
- return typeof value === "object" && value !== null;
279
+ async function validateAgentToken(baseUrl, token) {
280
+ const res = await fetch(`${normalizeGatewayUrl(baseUrl)}/api/users/me`, {
281
+ headers: { Authorization: `Bearer ${token}` }
282
+ });
283
+ if (!res.ok) return false;
284
+ const body = await res.json();
285
+ return body.code === 200 && body.data?.identityType === "AGENT";
332
286
  }
333
-
334
- // src/security.ts
335
- function applyInboundSecurityPolicy(envelope, config) {
336
- if (!isPrivateConversation(envelope)) {
287
+ async function runCoolclawSetup(options = {}) {
288
+ const gatewayUrl = normalizeGatewayUrl(options.gatewayUrl ?? "https://agits-xa.baidu.com/riddle");
289
+ const bindingFile = options.bindingFile ?? defaultBindingFile();
290
+ const openclawConfigPath = options.openclawConfigPath ?? defaultOpenClawConfigFile();
291
+ const existingBinding = await loadBinding(bindingFile);
292
+ const existingToken = await readTokenRef(existingBinding.tokenRef);
293
+ if (options.dryRun) {
337
294
  return {
338
- accepted: true,
339
- envelope: {
340
- ...envelope,
341
- shouldReply: envelope.group ? envelope.shouldReply === true : false
342
- }
295
+ mode: "dry-run",
296
+ gatewayUrl,
297
+ agentId: existingBinding.agentId,
298
+ bindingFile,
299
+ openclawConfigPath,
300
+ tokenSavedTo: tokenFileFromBinding(existingBinding, bindingFile)
343
301
  };
344
302
  }
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
- }
303
+ const canReuse = !options.forceRegister && existingBinding.agentId.length > 0 && Boolean(existingToken) && await validateAgentToken(gatewayUrl, existingToken);
304
+ const binding = canReuse ? existingBinding : await registerAndPersistBinding({
305
+ gatewayUrl,
306
+ bindingFile,
307
+ registration: await resolveRegistrationInput(
308
+ options.registration,
309
+ options.workspaceDir,
310
+ openclawConfigPath
311
+ )
312
+ });
313
+ const tokenFile = tokenFileFromBinding(binding, bindingFile);
314
+ if (!options.dryRun) {
315
+ await patchCoolclawAccountConfig({
316
+ configPath: openclawConfigPath,
317
+ gatewayUrl,
318
+ agentId: binding.agentId,
319
+ tokenSecretRef: binding.tokenRef ?? `file://${tokenFile}`,
320
+ allowFrom: options.allowFrom,
321
+ dmPolicy: options.dmPolicy
322
+ });
323
+ if (options.workspaceDir) {
324
+ await updateRiddleIdentitySummary({
325
+ workspaceDir: options.workspaceDir,
326
+ agentId: binding.agentId,
327
+ gatewayUrl,
328
+ bindingFile,
329
+ tokenRef: binding.tokenRef ?? void 0
330
+ });
331
+ }
332
+ if (options.autoRestart !== false) {
333
+ try {
334
+ const { execSync } = await import("child_process");
335
+ console.log("[coolclaw] Restarting gateway...");
336
+ execSync("openclaw gateway restart", { stdio: "inherit" });
337
+ console.log("[coolclaw] Gateway restarted.");
338
+ } catch (err) {
339
+ console.warn(
340
+ `[coolclaw] Could not auto-restart gateway. Please run: openclaw gateway restart`
341
+ );
365
342
  }
366
- };
343
+ }
367
344
  }
368
345
  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
- }
346
+ mode: options.dryRun ? "dry-run" : canReuse ? "reuse" : "register",
347
+ gatewayUrl,
348
+ agentId: binding.agentId,
349
+ bindingFile,
350
+ openclawConfigPath,
351
+ tokenSavedTo: tokenFile
379
352
  };
380
353
  }
381
- function isPrivateConversation(envelope) {
382
- return envelope.conversationId.startsWith("private:");
383
- }
384
- function toIdentityKey(userType, userId) {
385
- return `${userType.toLowerCase()}:${userId}`;
354
+ async function registerAndPersistBinding(input) {
355
+ const registered = await registerAgent(input.gatewayUrl, input.registration);
356
+ const valid = await validateAgentToken(input.gatewayUrl, registered.token);
357
+ if (!valid) {
358
+ throw new Error("Newly registered Riddle token did not validate as an AGENT token");
359
+ }
360
+ const tokenFile = defaultTokenFile(input.bindingFile, registered.agentId);
361
+ await saveAgentToken(tokenFile, registered.token);
362
+ const binding = touchBinding({
363
+ agentId: registered.agentId,
364
+ tokenRef: `file://${tokenFile}`,
365
+ runtimeType: "openclaw",
366
+ lastAckedSeq: 0
367
+ });
368
+ await saveBinding(input.bindingFile, binding);
369
+ return binding;
386
370
  }
387
- function normalizeIdentityKey(value) {
388
- return value.trim().toLowerCase();
371
+ function tokenFileFromBinding(binding, bindingFile) {
372
+ if (binding.tokenRef?.startsWith("file://")) {
373
+ return binding.tokenRef.slice("file://".length);
374
+ }
375
+ return defaultTokenFile(bindingFile, binding.agentId);
389
376
  }
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
- };
377
+ function parseIdentityMarkdown(content) {
378
+ const result = {};
379
+ for (const line of content.split(/\r?\n/)) {
380
+ const cleaned = line.trim().replace(/^\s*-\s*/, "");
381
+ const colonIdx = cleaned.indexOf(":");
382
+ if (colonIdx === -1) continue;
383
+ const label = cleaned.slice(0, colonIdx).replace(/[*_]/g, "").trim().toLowerCase();
384
+ const value = cleaned.slice(colonIdx + 1).replace(/^[*_]+|[*_]+$/g, "").trim();
385
+ if (!value || IDENTITY_PLACEHOLDERS.has(value.toLowerCase().replace(/[()]/g, "").trim())) continue;
386
+ if (label === "name") result.name = value;
387
+ if (label === "creature") result.creature = value;
388
+ if (label === "vibe") result.vibe = value;
389
+ if (label === "theme") result.theme = value;
411
390
  }
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
- };
391
+ return result;
392
+ }
393
+ async function readWorkspaceDirFromOpenclawConfig(configPath) {
394
+ try {
395
+ const content = await readFile4(configPath, "utf-8");
396
+ const config = JSON.parse(content);
397
+ const workspace = config.agents?.defaults?.workspace;
398
+ if (typeof workspace === "string" && workspace.trim()) {
399
+ return workspace.trim();
400
+ }
401
+ } catch {
434
402
  }
435
- if (frame.type === "SYSTEM_NOTIFICATION" || frame.type === "GAME_EVENT" || frame.type === "CONTENT_TASK") {
436
- return mapNotificationFrame(frame);
403
+ const defaultWorkspace = path4.join(homedir3(), ".openclaw", "workspace");
404
+ try {
405
+ await access(defaultWorkspace);
406
+ return defaultWorkspace;
407
+ } catch {
408
+ return void 0;
437
409
  }
438
- throw new Error(`Unsupported inbound CoolClaw frame type: ${frame.type}`);
439
410
  }
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;
411
+ async function readIdentityFromWorkspace(workspaceDir) {
412
+ try {
413
+ const content = await readFile4(path4.join(workspaceDir, "IDENTITY.md"), "utf-8");
414
+ return parseIdentityMarkdown(content);
415
+ } catch {
416
+ return {};
447
417
  }
448
- await input.dispatch(envelope);
449
418
  }
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
419
+ async function readIdentityFromOpenclawConfig(configPath) {
420
+ try {
421
+ const content = await readFile4(configPath, "utf-8");
422
+ const config = JSON.parse(content);
423
+ const defaultsName = config.agents?.defaults?.identity?.name || config.agents?.defaults?.name;
424
+ if (defaultsName) return { name: defaultsName };
425
+ const mainAgent = config.agents?.list?.find((a) => a.id === "main") || config.agents?.list?.[0];
426
+ if (mainAgent?.identity?.name || mainAgent?.name) {
427
+ return { name: mainAgent.identity?.name || mainAgent.name };
464
428
  }
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 }));
429
+ return {};
430
+ } catch {
431
+ return {};
471
432
  }
472
433
  }
473
- function assertPrivatePayload(value) {
474
- if (!isRecord3(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
475
- throw new Error("Invalid PRIVATE_MESSAGE payload");
434
+ async function resolveRegistrationInput(explicitRegistration, workspaceDir, openclawConfigPath) {
435
+ if (explicitRegistration?.name && explicitRegistration?.bio && explicitRegistration.name !== "CoolClaw Agent" && explicitRegistration.bio !== "OpenClaw agent connected through CoolClaw channel.") {
436
+ return explicitRegistration;
476
437
  }
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");
438
+ let identityName;
439
+ let identityBio;
440
+ const resolvedWorkspaceDir = workspaceDir ?? await readWorkspaceDirFromOpenclawConfig(openclawConfigPath);
441
+ if (resolvedWorkspaceDir) {
442
+ const identity = await readIdentityFromWorkspace(resolvedWorkspaceDir);
443
+ if (identity.name) {
444
+ identityName = identity.name;
445
+ }
446
+ const bioParts = [identity.creature, identity.vibe, identity.theme].filter(Boolean);
447
+ if (bioParts.length > 0) {
448
+ identityBio = bioParts.join(". ") + ".";
449
+ }
450
+ }
451
+ if (!identityName) {
452
+ const configIdentity = await readIdentityFromOpenclawConfig(openclawConfigPath);
453
+ identityName = configIdentity.name;
492
454
  }
455
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(4, 12);
493
456
  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")
457
+ name: explicitRegistration?.name && explicitRegistration.name !== "CoolClaw Agent" ? explicitRegistration.name : identityName ?? `RiddleAgent-${stamp}`,
458
+ bio: explicitRegistration?.bio && explicitRegistration.bio !== "OpenClaw agent connected through CoolClaw channel." ? explicitRegistration.bio : identityBio ?? "OpenClaw agent connected through CoolClaw channel.",
459
+ tags: explicitRegistration?.tags ?? JSON.stringify(["assistant", "openclaw", "coolclaw"])
505
460
  };
506
461
  }
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}`);
462
+ var IDENTITY_PLACEHOLDERS;
463
+ var init_setup = __esm({
464
+ "src/setup.ts"() {
465
+ "use strict";
466
+ init_config();
467
+ init_binding();
468
+ init_identity();
469
+ init_openclaw_config();
470
+ IDENTITY_PLACEHOLDERS = /* @__PURE__ */ new Set([
471
+ "pick something you like",
472
+ "ai? robot? familiar? ghost in the machine? something weirder?",
473
+ "how do you come across? sharp? warm? chaotic? calm?",
474
+ "your signature - pick one that feels right",
475
+ "workspace-relative path, http(s) url, or data uri"
476
+ ]);
477
+ }
478
+ });
479
+
480
+ // index.ts
481
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
482
+ import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
483
+
484
+ // src/ack-store.ts
485
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
486
+ import { join } from "path";
487
+ import { homedir } from "os";
488
+ var InMemoryAckStore = class {
489
+ cursors = /* @__PURE__ */ new Map();
490
+ async getLastAckedSeq(accountKey) {
491
+ return this.cursors.get(accountKey) ?? 0;
492
+ }
493
+ async record(accountKey, seq) {
494
+ if (!Number.isInteger(seq) || seq < 1) {
495
+ throw new Error(`Invalid ACK seq: ${seq}`);
496
+ }
497
+ const current = this.cursors.get(accountKey) ?? 0;
498
+ if (seq <= current) {
499
+ return current;
500
+ }
501
+ this.cursors.set(accountKey, seq);
502
+ return seq;
503
+ }
504
+ };
505
+ var FileAckStore = class {
506
+ cursors = /* @__PURE__ */ new Map();
507
+ dir;
508
+ constructor(dir) {
509
+ this.dir = dir ?? join(homedir(), ".openclaw", "extensions", "coolclaw", ".ack-store");
510
+ if (!existsSync(this.dir)) {
511
+ mkdirSync(this.dir, { recursive: true });
512
+ }
513
+ }
514
+ async getLastAckedSeq(accountKey) {
515
+ return this.cursors.get(accountKey) ?? this.load(accountKey);
516
+ }
517
+ async record(accountKey, seq) {
518
+ if (!Number.isInteger(seq) || seq < 1) {
519
+ throw new Error(`Invalid ACK seq: ${seq}`);
520
+ }
521
+ const current = this.cursors.get(accountKey) ?? this.load(accountKey);
522
+ if (seq <= current) {
523
+ return current;
524
+ }
525
+ this.cursors.set(accountKey, seq);
526
+ this.persist(accountKey, seq);
527
+ return seq;
528
+ }
529
+ load(accountKey) {
530
+ const filePath = this.filePath(accountKey);
531
+ if (!existsSync(filePath)) {
532
+ return 0;
533
+ }
534
+ try {
535
+ const text = readFileSync(filePath, "utf-8").trim();
536
+ const value = parseInt(text, 10);
537
+ return Number.isFinite(value) && value >= 0 ? value : 0;
538
+ } catch {
539
+ return 0;
540
+ }
541
+ }
542
+ persist(accountKey, lastAckedSeq) {
543
+ try {
544
+ writeFileSync(this.filePath(accountKey), String(lastAckedSeq), "utf-8");
545
+ } catch {
546
+ }
547
+ }
548
+ filePath(accountKey) {
549
+ const safeName = accountKey.replace(/[^a-zA-Z0-9_-]/g, "_");
550
+ return join(this.dir, `${safeName}.ack`);
551
+ }
552
+ };
553
+
554
+ // src/channel.ts
555
+ init_config();
556
+
557
+ // src/frame-codec.ts
558
+ import { randomUUID } from "crypto";
559
+ var CoolclawFrameDecodeError = class extends Error {
560
+ constructor(message) {
561
+ super(message);
562
+ this.name = "CoolclawFrameDecodeError";
563
+ }
564
+ };
565
+ function createFrame(type, payload) {
566
+ return {
567
+ v: 1,
568
+ type,
569
+ id: `cli_${randomUUID()}`,
570
+ ts: Date.now(),
571
+ payload
572
+ };
573
+ }
574
+ function encodeFrame(frame) {
575
+ return JSON.stringify(frame);
576
+ }
577
+ function decodeFrame(raw) {
578
+ let value;
579
+ try {
580
+ value = JSON.parse(raw);
581
+ } catch (error) {
582
+ throw new CoolclawFrameDecodeError(`Invalid CoolClaw frame JSON: ${error.message}`);
583
+ }
584
+ if (!isRecord(value)) {
585
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: expected object");
586
+ }
587
+ if (!("v" in value)) {
588
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing v");
589
+ }
590
+ if (value.v !== 1) {
591
+ throw new CoolclawFrameDecodeError("Unsupported CoolClaw frame version");
592
+ }
593
+ if (typeof value.type !== "string" || value.type.length === 0) {
594
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing type");
595
+ }
596
+ if (typeof value.id !== "string" || value.id.length === 0) {
597
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing id");
598
+ }
599
+ if (typeof value.ts !== "number" || !Number.isFinite(value.ts)) {
600
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: missing ts");
601
+ }
602
+ if ("ack" in value && value.ack !== void 0 && typeof value.ack !== "string") {
603
+ throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: ack must be a string");
604
+ }
605
+ const frame = {
606
+ v: 1,
607
+ type: value.type,
608
+ id: value.id,
609
+ ts: value.ts
610
+ };
611
+ if (typeof value.ack === "string") {
612
+ frame.ack = value.ack;
613
+ }
614
+ if ("payload" in value) {
615
+ frame.payload = value.payload;
616
+ }
617
+ return frame;
618
+ }
619
+ function isRecord(value) {
620
+ return typeof value === "object" && value !== null;
621
+ }
622
+
623
+ // src/inbound.ts
624
+ function mapInboundFrame(frame) {
625
+ if (frame.type === "PRIVATE_MESSAGE") {
626
+ const payload = assertPrivatePayload(frame.payload);
627
+ return {
628
+ id: payload.messageId,
629
+ channel: "coolclaw",
630
+ conversationId: `private:${payload.sender.userType}:${payload.sender.userId}`,
631
+ text: payload.content,
632
+ messageType: payload.messageType,
633
+ seq: payload.seq,
634
+ shouldReply: true,
635
+ // 私聊本身就是与 Bot 对话的意图
636
+ sender: payload.sender,
637
+ recipient: payload.recipient,
638
+ metadata: {
639
+ riddleConversationId: payload.conversationId,
640
+ sentAt: payload.sentAt,
641
+ sourceFrameId: frame.id
642
+ }
643
+ };
644
+ }
645
+ if (frame.type === "GROUP_MESSAGE") {
646
+ const payload = assertGroupPayload(frame.payload);
647
+ return {
648
+ id: payload.messageId,
649
+ channel: "coolclaw",
650
+ conversationId: `group:${payload.groupId}`,
651
+ text: payload.content,
652
+ messageType: payload.messageType,
653
+ seq: payload.seq,
654
+ shouldReply: payload.mentioned,
655
+ // 群聊仅在 @ 时回复
656
+ sender: payload.sender,
657
+ group: {
658
+ groupId: payload.groupId,
659
+ groupName: payload.groupName
660
+ },
661
+ metadata: {
662
+ riddleConversationId: payload.conversationId,
663
+ sentAt: payload.sentAt,
664
+ sourceFrameId: frame.id,
665
+ agentHint: payload.agentHint
666
+ }
667
+ };
668
+ }
669
+ if (frame.type === "SYSTEM_NOTIFICATION" || frame.type === "GAME_EVENT" || frame.type === "CONTENT_TASK") {
670
+ return mapNotificationFrame(frame);
671
+ }
672
+ throw new Error(`Unsupported inbound CoolClaw frame type: ${frame.type}`);
673
+ }
674
+ async function handleInboundFrame(input) {
675
+ const envelope = mapInboundFrame(input.frame);
676
+ if (!envelope) return;
677
+ await ackProcessedSeq(input, envelope);
678
+ await input.dispatch(envelope);
679
+ }
680
+ function mapNotificationFrame(frame) {
681
+ const payload = isRecord2(frame.payload) ? frame.payload : {};
682
+ const seq = typeof payload.seq === "number" ? payload.seq : void 0;
683
+ if (frame.type === "SYSTEM_NOTIFICATION") {
684
+ const title = typeof payload.title === "string" ? payload.title : "System";
685
+ const content = typeof payload.content === "string" ? payload.content : JSON.stringify(payload);
686
+ return {
687
+ id: frame.id,
688
+ channel: "coolclaw",
689
+ conversationId: "notification:system",
690
+ text: `[\u7CFB\u7EDF\u901A\u77E5] ${title}: ${content}`,
691
+ messageType: frame.type,
692
+ seq,
693
+ shouldReply: false,
694
+ metadata: { sourceFrameId: frame.id, payload: frame.payload }
695
+ };
696
+ }
697
+ if (frame.type === "GAME_EVENT") {
698
+ return null;
699
+ }
700
+ if (frame.type === "CONTENT_TASK") {
701
+ const taskType = typeof payload.taskType === "string" ? payload.taskType : "unknown";
702
+ return {
703
+ id: frame.id,
704
+ channel: "coolclaw",
705
+ conversationId: "notification:content_task",
706
+ text: `[\u5185\u5BB9\u4EFB\u52A1] ${taskType}: ${JSON.stringify(payload)}`,
707
+ messageType: frame.type,
708
+ seq,
709
+ shouldReply: false,
710
+ metadata: { sourceFrameId: frame.id, payload: frame.payload }
711
+ };
712
+ }
713
+ return null;
714
+ }
715
+ async function ackProcessedSeq(input, envelope) {
716
+ if (typeof envelope.seq === "number") {
717
+ const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
718
+ await input.sendAck(createFrame("ACK", { lastAckedSeq }));
719
+ }
720
+ }
721
+ function assertPrivatePayload(value) {
722
+ if (!isRecord2(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
723
+ throw new Error("Invalid PRIVATE_MESSAGE payload");
724
+ }
725
+ return {
726
+ seq: readNumber(value, "seq"),
727
+ messageId: readString(value, "messageId"),
728
+ conversationId: readString(value, "conversationId"),
729
+ sender: value.sender,
730
+ recipient: value.recipient,
731
+ messageType: readString(value, "messageType"),
732
+ content: readString(value, "content"),
733
+ mentioned: readBoolean(value, "mentioned"),
734
+ sentAt: readString(value, "sentAt")
735
+ };
736
+ }
737
+ function assertGroupPayload(value) {
738
+ if (!isRecord2(value) || !isUserRef(value.sender)) {
739
+ throw new Error("Invalid GROUP_MESSAGE payload");
740
+ }
741
+ return {
742
+ seq: readNumber(value, "seq"),
743
+ messageId: readString(value, "messageId"),
744
+ groupId: readString(value, "groupId"),
745
+ groupName: readString(value, "groupName"),
746
+ conversationId: readString(value, "conversationId"),
747
+ sender: value.sender,
748
+ messageType: readString(value, "messageType"),
749
+ content: readString(value, "content"),
750
+ mentioned: readBoolean(value, "mentioned"),
751
+ sentAt: readString(value, "sentAt"),
752
+ agentHint: readOptionalString(value, "agentHint")
753
+ };
754
+ }
755
+ function readString(source, key) {
756
+ const value = source[key];
757
+ if (typeof value !== "string" || value.length === 0) {
758
+ throw new Error(`Invalid inbound payload: missing ${key}`);
511
759
  }
512
760
  return value;
513
761
  }
@@ -536,9 +784,9 @@ function readOptionalString(source, key) {
536
784
  return value;
537
785
  }
538
786
  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");
787
+ return isRecord2(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
540
788
  }
541
- function isRecord3(value) {
789
+ function isRecord2(value) {
542
790
  return typeof value === "object" && value !== null;
543
791
  }
544
792
 
@@ -635,21 +883,34 @@ async function sendText(input) {
635
883
  }
636
884
  return response.messageId;
637
885
  }
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
- }
886
+ async function sendMedia(input) {
887
+ const target = parseCoolclawTarget(input.target);
888
+ const fs = await import("fs/promises");
889
+ const fileBuffer = await fs.readFile(input.filePath);
890
+ const base64Content = fileBuffer.toString("base64");
891
+ const frame = target.kind === "private" ? createFrame("SEND_PRIVATE_MEDIA", {
892
+ target: { userId: target.userId, userType: target.userType },
893
+ messageType: "IMAGE",
894
+ content: base64Content,
895
+ mimeType: input.mimeType ?? "image/png"
896
+ }) : createFrame("SEND_GROUP_MEDIA", {
897
+ groupId: target.groupId,
898
+ messageType: "IMAGE",
899
+ content: base64Content,
900
+ mimeType: input.mimeType ?? "image/png"
901
+ });
902
+ const response = await input.client.request(frame);
903
+ if (response.ok === false) {
904
+ throw new Error(response.error?.message ?? "CoolClaw media send failed");
905
+ }
906
+ if (!response.messageId) {
907
+ throw new Error("CoolClaw media send response missing messageId");
908
+ }
909
+ return response.messageId;
910
+ }
651
911
 
652
912
  // src/ws-client.ts
913
+ init_config();
653
914
  import WebSocket from "ws";
654
915
  var CoolclawWsClient = class {
655
916
  constructor(options) {
@@ -659,6 +920,7 @@ var CoolclawWsClient = class {
659
920
  heartbeatTimer;
660
921
  reconnectTimer;
661
922
  stopped = true;
923
+ reconnectAttempt = 0;
662
924
  pendingRequests = /* @__PURE__ */ new Map();
663
925
  async start() {
664
926
  this.stopped = false;
@@ -703,7 +965,11 @@ var CoolclawWsClient = class {
703
965
  }
704
966
  this.socket.send(encodeFrame(frame));
705
967
  }
968
+ isConnected() {
969
+ return this.socket?.readyState === WebSocket.OPEN;
970
+ }
706
971
  async connect() {
972
+ this.notifyState("connecting");
707
973
  const lastAckedSeq = await this.options.ackStore.getLastAckedSeq(this.options.accountKey);
708
974
  const socket = new WebSocket(buildWsUrl(this.options.gatewayUrl, lastAckedSeq), {
709
975
  headers: {
@@ -739,6 +1005,7 @@ var CoolclawWsClient = class {
739
1005
  });
740
1006
  if (frame.type === "HELLO") {
741
1007
  helloReceived = true;
1008
+ this.reconnectAttempt = 0;
742
1009
  cleanupBeforeHello();
743
1010
  socket.on("message", (nextData) => {
744
1011
  this.handleRawMessage(nextData).catch((error) => {
@@ -747,6 +1014,7 @@ var CoolclawWsClient = class {
747
1014
  });
748
1015
  socket.on("close", (code) => this.handleClose(code));
749
1016
  this.startHeartbeat(frame);
1017
+ this.notifyState("connected");
750
1018
  resolve();
751
1019
  }
752
1020
  };
@@ -804,6 +1072,7 @@ var CoolclawWsClient = class {
804
1072
  handleClose(code) {
805
1073
  this.clearHeartbeat();
806
1074
  this.rejectPending(new Error(`CoolClaw WSS connection closed: ${code}`));
1075
+ this.notifyState("disconnected");
807
1076
  if (this.stopped || this.isTerminalClose(code)) {
808
1077
  return;
809
1078
  }
@@ -811,661 +1080,522 @@ var CoolclawWsClient = class {
811
1080
  }
812
1081
  scheduleReconnect() {
813
1082
  this.clearReconnect();
814
- const delayMs = this.options.reconnectDelayMs ?? 1e3;
1083
+ this.notifyState("reconnecting");
1084
+ const baseDelay = this.options.reconnectDelayMs ?? 1e3;
1085
+ const maxDelay = 6e4;
1086
+ const attempt = this.reconnectAttempt++;
1087
+ const delayMs = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
815
1088
  this.reconnectTimer = setTimeout(() => {
816
1089
  if (this.stopped) return;
817
1090
  this.connect().catch((error) => {
818
1091
  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
1092
+ this.scheduleReconnect();
1093
+ }
1336
1094
  });
1337
- }
1095
+ }, delayMs);
1338
1096
  }
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");
1097
+ notifyState(state) {
1098
+ this.options.onStateChange?.(state);
1353
1099
  }
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);
1100
+ clearHeartbeat() {
1101
+ if (this.heartbeatTimer) {
1102
+ clearInterval(this.heartbeatTimer);
1103
+ this.heartbeatTimer = void 0;
1104
+ }
1368
1105
  }
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;
1106
+ clearReconnect() {
1107
+ if (this.reconnectTimer) {
1108
+ clearTimeout(this.reconnectTimer);
1109
+ this.reconnectTimer = void 0;
1110
+ }
1391
1111
  }
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();
1112
+ rejectPending(error) {
1113
+ for (const pending of this.pendingRequests.values()) {
1114
+ clearTimeout(pending.timeout);
1115
+ pending.reject(error);
1401
1116
  }
1402
- } catch {
1117
+ this.pendingRequests.clear();
1403
1118
  }
1404
- const defaultWorkspace = path4.join(homedir3(), ".openclaw", "workspace");
1405
- try {
1406
- await access(defaultWorkspace);
1407
- return defaultWorkspace;
1408
- } catch {
1119
+ isTerminalClose(code) {
1120
+ return code === 4001 || code === 4002 || code === 4003 || code === 4004 || code === 4005;
1121
+ }
1122
+ };
1123
+ function readPingInterval(payload) {
1124
+ if (!isRecord3(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
1409
1125
  return void 0;
1410
1126
  }
1127
+ return payload.pingIntervalMs;
1411
1128
  }
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 {};
1129
+ function readErrorMessage(payload) {
1130
+ if (isRecord3(payload) && typeof payload.message === "string") {
1131
+ return payload.message;
1418
1132
  }
1133
+ return "CoolClaw request failed";
1419
1134
  }
1420
- async function readIdentityFromOpenclawConfig(configPath) {
1135
+ function isRecord3(value) {
1136
+ return typeof value === "object" && value !== null;
1137
+ }
1138
+
1139
+ // src/runtime.ts
1140
+ var _runtime;
1141
+ function setCoolclawRuntime(runtime) {
1142
+ _runtime = runtime;
1143
+ }
1144
+ function getCoolclawRuntime() {
1145
+ return _runtime;
1146
+ }
1147
+
1148
+ // src/version.ts
1149
+ import { readFileSync as readFileSync2 } from "fs";
1150
+ import { join as join2, dirname } from "path";
1151
+ import { fileURLToPath as fileURLToPath2 } from "url";
1152
+ var _version;
1153
+ function getPluginVersion() {
1154
+ if (_version) return _version;
1421
1155
  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 {};
1156
+ const pkgPath = join2(dirname(fileURLToPath2(import.meta.url)), "..", "package.json");
1157
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1158
+ _version = pkg.version ?? "0.0.0";
1431
1159
  } catch {
1432
- return {};
1160
+ _version = "0.0.0";
1433
1161
  }
1162
+ return _version;
1434
1163
  }
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;
1164
+
1165
+ // src/channel.ts
1166
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
1167
+ import { createAccountStatusSink, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-lifecycle";
1168
+ import { logInboundDrop, logAckFailure } from "openclaw/plugin-sdk/channel-logging";
1169
+ var runtimeClients = /* @__PURE__ */ new Map();
1170
+ function setRuntimeClient(accountKey, client) {
1171
+ runtimeClients.set(accountKey, client);
1172
+ }
1173
+ function getRuntimeClient(accountKey) {
1174
+ return runtimeClients.get(accountKey);
1175
+ }
1176
+ function clearRuntimeClient(accountKey) {
1177
+ runtimeClients.delete(accountKey);
1178
+ }
1179
+ function extractAccountFromConfig(cfg, accountId) {
1180
+ const coolclawSection = cfg.channels?.coolclaw;
1181
+ const accounts = coolclawSection?.accounts;
1182
+ return accounts?.[accountId ?? "default"] ?? {};
1183
+ }
1184
+ var coolclawChannelPlugin = createChatChannelPlugin({
1185
+ base: {
1186
+ id: "coolclaw",
1187
+ meta: {
1188
+ id: "coolclaw",
1189
+ label: "CoolClaw",
1190
+ selectionLabel: "CoolClaw",
1191
+ docsPath: "/plugins/coolclaw",
1192
+ blurb: "Connect OpenClaw to the CoolClaw/Riddle chat platform."
1193
+ },
1194
+ capabilities: {
1195
+ chatTypes: ["direct", "group"],
1196
+ media: true,
1197
+ blockStreaming: true
1198
+ },
1199
+ streaming: {
1200
+ blockStreamingCoalesceDefaults: {
1201
+ minChars: 200,
1202
+ idleMs: 3e3
1203
+ }
1204
+ },
1205
+ agentPrompt: {
1206
+ messageToolHints: () => [
1207
+ "To send a message on CoolClaw/Riddle, use the message tool with action='send' and set 'to' to a CoolClaw target like 'coolclaw:human:<userId>', 'coolclaw:agent:<agentId>', or 'coolclaw:group:<groupId>'.",
1208
+ "To send an image or file, use the message tool with action='send' and set 'media' to a local file path or a remote URL.",
1209
+ "When sending a message to a CoolClaw group, the agent will only reply if it was mentioned in the group message.",
1210
+ "When creating a cron job for CoolClaw, set delivery.to to the target CoolClaw ID and delivery.accountId to the current accountId."
1211
+ ]
1212
+ },
1213
+ config: {
1214
+ listAccountIds(cfg) {
1215
+ return Object.keys(cfg.channels?.coolclaw?.accounts ?? {});
1216
+ },
1217
+ resolveAccount(cfg, accountId) {
1218
+ return extractAccountFromConfig(cfg, accountId);
1219
+ },
1220
+ defaultAccountId() {
1221
+ return "default";
1222
+ },
1223
+ isConfigured(account) {
1224
+ return Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account));
1225
+ },
1226
+ isEnabled(account) {
1227
+ return account?.enabled !== false;
1228
+ },
1229
+ describeAccount(account) {
1230
+ return {
1231
+ accountId: "default",
1232
+ name: account.name,
1233
+ enabled: account.enabled !== false,
1234
+ configured: Boolean(account.gatewayUrl && account.agentId && (account.tokenSecretRef || "tokenSecret" in account)),
1235
+ gatewayUrl: account.gatewayUrl,
1236
+ agentId: account.agentId,
1237
+ tokenConfigured: Boolean(account.tokenSecretRef || "tokenSecret" in account),
1238
+ allowFromCount: account.allowFrom?.length ?? 0,
1239
+ dmPolicy: account.dmPolicy ?? "allowlist"
1240
+ };
1241
+ }
1242
+ },
1243
+ resolver: {
1244
+ async resolveTargets({ inputs }) {
1245
+ return inputs.map((input) => {
1246
+ try {
1247
+ const normalized = normalizeCoolclawTarget(input);
1248
+ const [, type, id] = normalized.split(":");
1249
+ parseCoolclawTarget(normalized);
1250
+ return { input, resolved: true, id: normalized, name: `${type}:${id}` };
1251
+ } catch (error) {
1252
+ return { input, resolved: false, note: error instanceof Error ? error.message : String(error) };
1253
+ }
1254
+ });
1255
+ }
1256
+ },
1257
+ messaging: {
1258
+ normalizeTarget(raw) {
1259
+ try {
1260
+ const normalized = normalizeCoolclawTarget(raw);
1261
+ parseCoolclawTarget(normalized);
1262
+ return normalized;
1263
+ } catch {
1264
+ return void 0;
1265
+ }
1266
+ },
1267
+ inferTargetChatType({ to }) {
1268
+ return inferCoolclawTargetChatType(to);
1269
+ },
1270
+ targetResolver: {
1271
+ hint: "Use coolclaw:human:<id>, coolclaw:agent:<id>, or coolclaw:group:<id>.",
1272
+ looksLikeId(raw, normalized) {
1273
+ return isCoolclawTargetId(raw, normalized);
1274
+ },
1275
+ resolveTarget({ input, normalized, preferredKind }) {
1276
+ return resolveCoolclawMessagingTarget(normalized || input, preferredKind);
1277
+ }
1278
+ }
1279
+ },
1280
+ gateway: {
1281
+ async startAccount(ctx) {
1282
+ const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
1283
+ const token = await resolveAccountToken(account);
1284
+ if (!account.gatewayUrl || !account.agentId || !token) {
1285
+ ctx.log?.error(`[${ctx.accountId}] CoolClaw account is not fully configured`);
1286
+ return;
1287
+ }
1288
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
1289
+ const ackStore = new FileAckStore();
1290
+ const statusSink = createAccountStatusSink({
1291
+ accountId: ctx.accountId,
1292
+ setStatus: ctx.setStatus
1293
+ });
1294
+ statusSink({ statusState: "connecting" });
1295
+ ctx.log?.info(`[${ctx.accountId}] starting CoolClaw provider (${account.gatewayUrl})`);
1296
+ await runPassiveAccountLifecycle({
1297
+ abortSignal: ctx.abortSignal,
1298
+ start: async () => {
1299
+ const client = new CoolclawWsClient({
1300
+ gatewayUrl: account.gatewayUrl,
1301
+ agentId: account.agentId,
1302
+ token,
1303
+ pluginVersion: getPluginVersion(),
1304
+ ackStore,
1305
+ accountKey,
1306
+ onStateChange: (state) => {
1307
+ statusSink({ statusState: state });
1308
+ },
1309
+ onFrame: async (frame, wsClient) => {
1310
+ try {
1311
+ await handleInboundFrame({
1312
+ frame,
1313
+ accountKey,
1314
+ ackStore,
1315
+ dispatch: async (envelope) => {
1316
+ const runtime = getCoolclawRuntime();
1317
+ if (!runtime?.channel) {
1318
+ logInboundDrop({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
1319
+ }), channel: "coolclaw", reason: "runtime not available; skipping dispatch" });
1320
+ return;
1321
+ }
1322
+ try {
1323
+ const isGroup = envelope.conversationId.startsWith("group:");
1324
+ const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
1325
+ if (!runtime.channel.routing?.resolveAgentRoute) {
1326
+ throw new Error(
1327
+ "CoolClaw requires runtime.channel.routing.resolveAgentRoute. Please upgrade OpenClaw to >=2026.3.22."
1328
+ );
1329
+ }
1330
+ const route = await runtime.channel.routing.resolveAgentRoute({
1331
+ cfg: ctx.cfg,
1332
+ channel: "coolclaw",
1333
+ accountId: ctx.accountId,
1334
+ peer
1335
+ });
1336
+ if (!runtime.channel.session?.resolveStorePath) {
1337
+ throw new Error(
1338
+ "CoolClaw requires runtime.channel.session.resolveStorePath. Please upgrade OpenClaw to >=2026.3.22."
1339
+ );
1340
+ }
1341
+ const storePath = runtime.channel.session.resolveStorePath(
1342
+ ctx.cfg.session?.store,
1343
+ { agentId: route.agentId }
1344
+ );
1345
+ const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : "unknown";
1346
+ let deliveryTarget;
1347
+ if (envelope.group) {
1348
+ deliveryTarget = `coolclaw:group:${envelope.group.groupId}`;
1349
+ } else if (envelope.sender) {
1350
+ deliveryTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
1351
+ } else {
1352
+ deliveryTarget = normalizeCoolclawTarget(envelope.conversationId);
1353
+ }
1354
+ const agentHint = envelope.metadata?.agentHint;
1355
+ const bodyForAgent = agentHint ? envelope.text + agentHint : envelope.text;
1356
+ const ctxPayload = runtime.channel.reply.finalizeInboundContext({
1357
+ Body: envelope.text,
1358
+ BodyForAgent: bodyForAgent,
1359
+ RawBody: envelope.text,
1360
+ CommandBody: envelope.text,
1361
+ From: `coolclaw:${senderLabel}`,
1362
+ To: deliveryTarget,
1363
+ SessionKey: route.sessionKey,
1364
+ AccountId: ctx.accountId,
1365
+ ChatType: isGroup ? "channel" : "direct",
1366
+ CommandAuthorized: true,
1367
+ Provider: "coolclaw",
1368
+ Surface: "coolclaw",
1369
+ Channel: "coolclaw",
1370
+ Peer: peer,
1371
+ Mentioned: envelope.shouldReply
1372
+ });
1373
+ const sessionKey = ctxPayload.SessionKey ?? route.sessionKey;
1374
+ const mainSessionKey = route.mainSessionKey;
1375
+ await runtime.channel.session.recordInboundSession({
1376
+ storePath,
1377
+ sessionKey,
1378
+ ctx: ctxPayload,
1379
+ updateLastRoute: mainSessionKey && mainSessionKey !== sessionKey ? { sessionKey: mainSessionKey, channel: "coolclaw", to: deliveryTarget, accountId: ctx.accountId ?? void 0 } : void 0,
1380
+ onRecordError: (err) => {
1381
+ ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
1382
+ }
1383
+ });
1384
+ await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1385
+ ctx: ctxPayload,
1386
+ cfg: ctx.cfg,
1387
+ dispatcherOptions: {
1388
+ deliver: async (payload) => {
1389
+ if (!payload.text) return;
1390
+ try {
1391
+ let replyTarget;
1392
+ if (envelope.group) {
1393
+ replyTarget = `coolclaw:group:${envelope.group.groupId}`;
1394
+ } else if (envelope.sender) {
1395
+ replyTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
1396
+ } else {
1397
+ replyTarget = normalizeCoolclawTarget(envelope.conversationId);
1398
+ }
1399
+ await sendText({ client: wsClient, target: replyTarget, text: payload.text });
1400
+ } catch (err) {
1401
+ ctx.log?.error(`Failed to deliver reply: ${err instanceof Error ? err.message : String(err)}`);
1402
+ }
1403
+ },
1404
+ onError: (err, info) => {
1405
+ ctx.log?.error(`Reply dispatch error: ${err instanceof Error ? err.message : String(err)}${info ? ` ${JSON.stringify(info)}` : ""}`);
1406
+ }
1407
+ }
1408
+ });
1409
+ } catch (err) {
1410
+ ctx.log?.error(`Inbound dispatch error: ${err instanceof Error ? err.message : String(err)}`);
1411
+ }
1412
+ },
1413
+ sendAck: async (ackFrame) => {
1414
+ try {
1415
+ wsClient.sendFrame(ackFrame);
1416
+ ctx.log?.debug?.(`ACK sent: type=${ackFrame.type} lastAckedSeq=${ackFrame.payload?.lastAckedSeq}`);
1417
+ } catch (err) {
1418
+ logAckFailure({ log: ctx.log?.warn?.bind(ctx.log) ?? (() => {
1419
+ }), channel: "coolclaw", error: err });
1420
+ }
1421
+ }
1422
+ });
1423
+ } catch (err) {
1424
+ ctx.log?.error(`Frame handling error: ${err instanceof Error ? err.message : String(err)}`);
1425
+ }
1426
+ }
1427
+ });
1428
+ await client.start();
1429
+ setRuntimeClient(accountKey, client);
1430
+ statusSink({ statusState: "connected" });
1431
+ ctx.log?.info(`[${ctx.accountId}] CoolClaw provider connected`);
1432
+ return client;
1433
+ },
1434
+ stop: async (client) => {
1435
+ await client.stop();
1436
+ clearRuntimeClient(accountKey);
1437
+ },
1438
+ onStop: () => {
1439
+ statusSink({ statusState: "disconnected" });
1440
+ ctx.log?.info(`[${ctx.accountId}] CoolClaw provider stopped`);
1441
+ }
1442
+ });
1443
+ },
1444
+ /** 显式停止账户连接,清理 WebSocket 客户端资源 */
1445
+ async stopAccount(ctx) {
1446
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
1447
+ const client = getRuntimeClient(accountKey);
1448
+ if (client) {
1449
+ await client.stop();
1450
+ clearRuntimeClient(accountKey);
1451
+ ctx.log?.info(`[${ctx.accountId}] CoolClaw client stopped via stopAccount`);
1452
+ }
1453
+ }
1454
+ },
1455
+ /** auth 适配器 — 支持 openclaw channels login --channel coolclaw 原生命令 */
1456
+ auth: {
1457
+ async login({ cfg, accountId, verbose }) {
1458
+ const resolvedAccountId = accountId?.trim() || "default";
1459
+ const account = coolclawChannelPlugin.config.resolveAccount(cfg, resolvedAccountId);
1460
+ const { validateAgentToken: validateAgentToken2, runCoolclawSetup: runCoolclawSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
1461
+ const existingToken = account.tokenSecretRef ? await resolveAccountToken(account) : void 0;
1462
+ if (existingToken) {
1463
+ const gatewayUrl = account.gatewayUrl ?? "https://agits-xa.baidu.com/riddle";
1464
+ const valid = await validateAgentToken2(gatewayUrl, existingToken);
1465
+ if (valid) {
1466
+ if (verbose) console.log("[coolclaw] Already authenticated.");
1467
+ return;
1468
+ }
1469
+ }
1470
+ if (verbose) console.log("[coolclaw] Running setup...");
1471
+ const result = await runCoolclawSetup2({
1472
+ gatewayUrl: account.gatewayUrl,
1473
+ accountId: resolvedAccountId,
1474
+ autoRestart: false
1475
+ // login 场景不需要重启网关
1476
+ });
1477
+ if (result.mode === "register") {
1478
+ if (verbose) console.log(`[coolclaw] Agent registered: ${result.agentId}`);
1479
+ } else {
1480
+ if (verbose) console.log(`[coolclaw] Reusing existing agent: ${result.agentId}`);
1481
+ }
1482
+ if (verbose) console.log("[coolclaw] Authentication complete.");
1483
+ }
1446
1484
  }
1447
- const bioParts = [identity.creature, identity.vibe, identity.theme].filter(Boolean);
1448
- if (bioParts.length > 0) {
1449
- identityBio = bioParts.join(". ") + ".";
1485
+ },
1486
+ security: {
1487
+ dm: {
1488
+ channelKey: "coolclaw",
1489
+ resolvePolicy: (account) => account.dmPolicy ?? "allowlist",
1490
+ resolveAllowFrom: (account) => account.allowFrom ?? [],
1491
+ defaultPolicy: "allowlist",
1492
+ normalizeEntry: (raw) => raw.trim().toLowerCase()
1493
+ }
1494
+ },
1495
+ pairing: {
1496
+ text: {
1497
+ idLabel: "CoolClaw user ID",
1498
+ message: "You are not authorized to message this agent. Send this pairing code to verify:",
1499
+ notify: async ({ id, message }) => {
1500
+ const client = getRuntimeClient("coolclaw:default");
1501
+ if (client) {
1502
+ await sendText({
1503
+ client,
1504
+ target: id,
1505
+ text: message
1506
+ });
1507
+ }
1508
+ }
1509
+ }
1510
+ },
1511
+ threading: {
1512
+ topLevelReplyToMode: "reply"
1513
+ },
1514
+ outbound: {
1515
+ base: {
1516
+ deliveryMode: "direct",
1517
+ resolveTarget({ to }) {
1518
+ if (!to) {
1519
+ return { ok: false, error: new Error("CoolClaw target is required") };
1520
+ }
1521
+ try {
1522
+ const normalized = normalizeCoolclawTarget(to);
1523
+ parseCoolclawTarget(normalized);
1524
+ return { ok: true, to: normalized };
1525
+ } catch (error) {
1526
+ return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
1527
+ }
1528
+ }
1529
+ },
1530
+ attachedResults: {
1531
+ channel: "coolclaw",
1532
+ async sendText(ctx) {
1533
+ const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
1534
+ const token = await resolveAccountToken(account);
1535
+ if (!account.gatewayUrl || !account.agentId || !token) {
1536
+ throw new Error("CoolClaw account is not fully configured");
1537
+ }
1538
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
1539
+ let client = getRuntimeClient(accountKey);
1540
+ if (!client || !client.isConnected()) {
1541
+ client = new CoolclawWsClient({
1542
+ gatewayUrl: account.gatewayUrl,
1543
+ agentId: account.agentId,
1544
+ token,
1545
+ pluginVersion: getPluginVersion(),
1546
+ ackStore: new InMemoryAckStore(),
1547
+ accountKey
1548
+ });
1549
+ await client.start();
1550
+ try {
1551
+ const messageId2 = await sendText({ client, target: ctx.to, text: ctx.text });
1552
+ return { messageId: messageId2, conversationId: ctx.to, timestamp: Date.now() };
1553
+ } finally {
1554
+ await client.stop();
1555
+ }
1556
+ }
1557
+ const messageId = await sendText({ client, target: ctx.to, text: ctx.text });
1558
+ return { messageId, conversationId: ctx.to, timestamp: Date.now() };
1559
+ },
1560
+ async sendMedia(ctx) {
1561
+ const account = coolclawChannelPlugin.config.resolveAccount(ctx.cfg, ctx.accountId);
1562
+ const token = await resolveAccountToken(account);
1563
+ if (!account.gatewayUrl || !account.agentId || !token) {
1564
+ throw new Error("CoolClaw account is not fully configured");
1565
+ }
1566
+ const accountKey = `coolclaw:${ctx.accountId ?? "default"}`;
1567
+ let client = getRuntimeClient(accountKey);
1568
+ if (!client || !client.isConnected()) {
1569
+ client = new CoolclawWsClient({
1570
+ gatewayUrl: account.gatewayUrl,
1571
+ agentId: account.agentId,
1572
+ token,
1573
+ pluginVersion: getPluginVersion(),
1574
+ ackStore: new InMemoryAckStore(),
1575
+ accountKey
1576
+ });
1577
+ await client.start();
1578
+ try {
1579
+ const messageId2 = await sendMedia({ client, target: ctx.to, filePath: ctx.mediaUrl ?? "" });
1580
+ return { messageId: messageId2, conversationId: ctx.to, timestamp: Date.now() };
1581
+ } finally {
1582
+ await client.stop();
1583
+ }
1584
+ }
1585
+ const messageId = await sendMedia({ client, target: ctx.to, filePath: ctx.mediaUrl ?? "" });
1586
+ return { messageId, conversationId: ctx.to, timestamp: Date.now() };
1587
+ }
1450
1588
  }
1451
1589
  }
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
- }
1590
+ });
1463
1591
 
1464
1592
  // src/cli.ts
1593
+ init_setup();
1594
+ init_binding();
1465
1595
  function registerCoolclawCli(options) {
1466
1596
  const setup = options.setup ?? runCoolclawSetup;
1467
1597
  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) => {
1598
+ 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("--no-restart", "Skip automatic gateway restart after setup").option("-y, --yes", "Accept defaults for non-interactive setup", false).action(async (...args) => {
1469
1599
  const rawOptions = readActionOptions(args);
1470
1600
  const result = await setup(toSetupOptions(rawOptions));
1471
1601
  console.log(JSON.stringify(result, null, 2));
@@ -1491,46 +1621,80 @@ function toSetupOptions(raw) {
1491
1621
  bio: explicitBio ?? "OpenClaw agent connected through CoolClaw channel.",
1492
1622
  tags: stringOption(raw.tags) ?? JSON.stringify(["assistant", "openclaw", "coolclaw"])
1493
1623
  } : void 0,
1494
- allowFrom: splitCsv2(stringOption(raw.allowFrom)),
1624
+ allowFrom: splitCsv(stringOption(raw.allowFrom)),
1495
1625
  dmPolicy: raw.dmPolicy === "pairing" ? "pairing" : "open",
1496
1626
  forceRegister: Boolean(raw.forceRegister),
1497
1627
  dryRun: Boolean(raw.dryRun),
1628
+ autoRestart: raw.restart !== false,
1629
+ // --no-restart 时为 false,默认 true
1498
1630
  yes: Boolean(raw.yes)
1499
1631
  };
1500
1632
  }
1501
1633
  function stringOption(value) {
1502
1634
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1503
1635
  }
1504
- function splitCsv2(value) {
1636
+ function splitCsv(value) {
1505
1637
  if (!value) return void 0;
1506
1638
  const items = value.split(",").map((item) => item.trim()).filter(Boolean);
1507
1639
  return items.length > 0 ? items : void 0;
1508
1640
  }
1509
1641
 
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
- }
1642
+ // src/compat.ts
1643
+ var SUPPORTED_HOST_MIN = "2026.3.22";
1644
+ function assertHostCompatibility(hostVersion) {
1645
+ if (!hostVersion || hostVersion === "unknown") return;
1646
+ if (isHostVersionSupported(hostVersion)) return;
1647
+ throw new Error(
1648
+ `This version of @coolclaw/coolclaw requires OpenClaw >=${SUPPORTED_HOST_MIN}, but found ${hostVersion}. Please upgrade OpenClaw:
1649
+ npm install -g openclaw@latest
1650
+ Then reinstall the plugin:
1651
+ openclaw plugins install @coolclaw/coolclaw
1652
+
1653
+ Or use the one-command installer (requires @coolclaw/coolclaw-cli published to npm):
1654
+ npx @coolclaw/coolclaw-cli install`
1526
1655
  );
1527
1656
  }
1528
- var index_default = {
1657
+ function isHostVersionSupported(hostVersion) {
1658
+ const clean = hostVersion.split("-")[0];
1659
+ const host = clean.split(".").map(Number);
1660
+ const min = SUPPORTED_HOST_MIN.split(".").map(Number);
1661
+ for (let i = 0; i < 3; i++) {
1662
+ if (host[i] > min[i]) return true;
1663
+ if (host[i] < min[i]) return false;
1664
+ }
1665
+ return true;
1666
+ }
1667
+
1668
+ // index.ts
1669
+ init_config();
1670
+ var entry = defineChannelPluginEntry({
1529
1671
  id: "coolclaw",
1530
- name: "CoolClaw Channel",
1672
+ name: "CoolClaw",
1531
1673
  description: "CoolClaw/Riddle messaging channel for OpenClaw",
1532
- register: _register
1533
- };
1674
+ configSchema: buildChannelConfigSchema(CoolclawConfigSchema),
1675
+ plugin: coolclawChannelPlugin,
1676
+ registerCliMetadata(api) {
1677
+ api.registerCli(
1678
+ ({ program }) => {
1679
+ registerCoolclawCli({ program });
1680
+ },
1681
+ {
1682
+ descriptors: [
1683
+ {
1684
+ name: "coolclaw",
1685
+ description: "Manage the CoolClaw/Riddle channel",
1686
+ hasSubcommands: true
1687
+ }
1688
+ ]
1689
+ }
1690
+ );
1691
+ },
1692
+ registerFull(api) {
1693
+ assertHostCompatibility(api.runtime?.version);
1694
+ setCoolclawRuntime(api.runtime);
1695
+ }
1696
+ });
1697
+ var index_default = entry;
1534
1698
 
1535
1699
  // cli-metadata.ts
1536
1700
  var cli_metadata_default = index_default;