@openclaw/clickclack 2026.7.2-beta.1 → 2026.7.2-beta.3

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.
@@ -0,0 +1,911 @@
1
+ import { createAccountListHelpers, hasConfiguredAccountValue } from "openclaw/plugin-sdk/account-helpers";
2
+ import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
+ import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
4
+ import { resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution-runtime";
5
+ import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
6
+ import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
7
+ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
8
+ import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
9
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
10
+ import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
11
+ import { buildChannelConfigSchema, buildMultiAccountChannelSchema } from "openclaw/plugin-sdk/channel-config-schema";
12
+ import { z } from "zod";
13
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
14
+ import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
15
+ import { WebSocket } from "ws";
16
+ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, applyAccountNameToChannelSection, applySetupAccountConfigPatch, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, hasConfiguredSecretInput, migrateBaseNameToDefaultAccount, moveSingleAccountChannelSectionToDefaultAccount, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
17
+ import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup-runtime";
18
+ //#region \0rolldown/runtime.js
19
+ var __defProp = Object.defineProperty;
20
+ var __exportAll = (all, no_symbols) => {
21
+ let target = {};
22
+ for (var name in all) __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true
25
+ });
26
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
27
+ return target;
28
+ };
29
+ //#endregion
30
+ //#region extensions/clickclack/src/accounts.ts
31
+ /**
32
+ * Resolves ClickClack account configuration from root channel config, named
33
+ * account overrides, and secret-provider references.
34
+ */
35
+ const DEFAULT_RECONNECT_MS = 1500;
36
+ const MIN_RECONNECT_MS = 100;
37
+ const MAX_RECONNECT_MS = 6e4;
38
+ const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
39
+ normalizeAccountId,
40
+ hasImplicitDefaultAccount: (cfg) => {
41
+ const channel = cfg.channels?.clickclack;
42
+ return Boolean(channel?.baseUrl?.trim() && (hasConfiguredAccountValue(channel.token) || Boolean(channel.tokenFile?.trim()) || Boolean(process.env.CLICKCLACK_BOT_TOKEN?.trim())) && channel.workspace?.trim());
43
+ }
44
+ });
45
+ function resolveClickClackAccountConfig(cfg, accountId) {
46
+ const channel = cfg.channels?.clickclack;
47
+ const merged = resolveMergedAccountConfig({
48
+ channelConfig: cfg.channels?.clickclack,
49
+ accounts: channel?.accounts,
50
+ accountId,
51
+ omitKeys: ["defaultAccount"],
52
+ normalizeAccountId
53
+ });
54
+ const account = resolveNormalizedAccountEntry(channel?.accounts, accountId, normalizeAccountId);
55
+ const accountTokenFile = account?.tokenFile?.trim();
56
+ if (accountTokenFile) return {
57
+ ...merged,
58
+ token: account?.token,
59
+ tokenFile: accountTokenFile
60
+ };
61
+ if (hasConfiguredAccountValue(account?.token)) return {
62
+ ...merged,
63
+ token: account?.token,
64
+ tokenFile: void 0
65
+ };
66
+ return merged;
67
+ }
68
+ function resolveClickClackToken(params) {
69
+ const tokenFile = params.tokenFile?.trim();
70
+ if (tokenFile) return tryReadSecretFileSync(tokenFile, params.accountId === DEFAULT_ACCOUNT_ID$2 ? "channels.clickclack.tokenFile" : `channels.clickclack.accounts.${params.accountId}.tokenFile`, { rejectSymlink: true }) ?? "";
71
+ const resolved = resolveSecretInputString({
72
+ value: params.value,
73
+ path: params.accountId === DEFAULT_ACCOUNT_ID$2 ? "channels.clickclack.token" : `channels.clickclack.accounts.${params.accountId}.token`,
74
+ defaults: params.cfg.secrets?.defaults,
75
+ mode: "inspect"
76
+ });
77
+ if (resolved.status !== "available") {
78
+ if (resolved.status === "missing" && params.accountId === DEFAULT_ACCOUNT_ID$2) return normalizeSecretInputString((params.env ?? process.env).CLICKCLACK_BOT_TOKEN) ?? "";
79
+ if (resolved.status === "configured_unavailable" && resolved.ref.source === "env") {
80
+ const providerConfig = params.cfg.secrets?.providers?.[resolved.ref.provider];
81
+ if (providerConfig) {
82
+ if (providerConfig.source !== "env") throw new Error(`Secret provider "${resolved.ref.provider}" has source "${providerConfig.source}" but ref requests "env".`);
83
+ if (providerConfig.allowlist && !providerConfig.allowlist.includes(resolved.ref.id)) throw new Error(`Environment variable "${resolved.ref.id}" is not allowlisted in secrets.providers.${resolved.ref.provider}.allowlist.`);
84
+ } else if (resolved.ref.provider !== resolveDefaultSecretProviderAlias({ secrets: params.cfg.secrets }, "env")) throw new Error(`Secret provider "${resolved.ref.provider}" is not configured (ref: env:${resolved.ref.provider}:${resolved.ref.id}).`);
85
+ return normalizeSecretInputString((params.env ?? process.env)[resolved.ref.id]) ?? "";
86
+ }
87
+ return "";
88
+ }
89
+ return normalizeResolvedSecretInputString({
90
+ value: resolved.value,
91
+ path: "channels.clickclack.token"
92
+ }) ?? "";
93
+ }
94
+ /**
95
+ * Builds the normalized account snapshot used by gateway, outbound delivery,
96
+ * status reporting, and channel routing.
97
+ */
98
+ function resolveClickClackAccount(params) {
99
+ const accountId = normalizeAccountId(params.accountId);
100
+ const merged = resolveClickClackAccountConfig(params.cfg, accountId);
101
+ const enabled = params.cfg.channels?.clickclack?.enabled !== false && merged.enabled !== false;
102
+ const baseUrl = merged.baseUrl?.trim().replace(/\/$/, "") ?? "";
103
+ const token = resolveClickClackToken({
104
+ cfg: params.cfg,
105
+ value: merged.token,
106
+ tokenFile: merged.tokenFile,
107
+ accountId,
108
+ env: params.env
109
+ });
110
+ const workspace = merged.workspace?.trim() ?? "";
111
+ return {
112
+ accountId,
113
+ enabled,
114
+ configured: Boolean(baseUrl && token && workspace),
115
+ name: normalizeOptionalString(merged.name),
116
+ baseUrl,
117
+ token,
118
+ workspace,
119
+ botUserId: normalizeOptionalString(merged.botUserId),
120
+ agentId: normalizeOptionalString(merged.agentId),
121
+ replyMode: merged.replyMode === "model" ? "model" : "agent",
122
+ model: normalizeOptionalString(merged.model),
123
+ systemPrompt: normalizeOptionalString(merged.systemPrompt),
124
+ timeoutSeconds: merged.timeoutSeconds,
125
+ toolsAllow: merged.toolsAllow,
126
+ defaultTo: merged.defaultTo?.trim() || "channel:general",
127
+ allowFrom: merged.allowFrom ?? ["*"],
128
+ reconnectMs: resolveIntegerOption(merged.reconnectMs, DEFAULT_RECONNECT_MS, {
129
+ min: MIN_RECONNECT_MS,
130
+ max: MAX_RECONNECT_MS
131
+ }),
132
+ agentActivity: merged.agentActivity === true,
133
+ commandMenu: merged.commandMenu !== false,
134
+ config: {
135
+ ...merged,
136
+ allowFrom: merged.allowFrom ?? ["*"]
137
+ }
138
+ };
139
+ }
140
+ /**
141
+ * Returns all enabled accounts, including the implicit default account when
142
+ * legacy top-level ClickClack config is present.
143
+ */
144
+ function listEnabledClickClackAccounts(cfg) {
145
+ return listClickClackAccountIds(cfg).map((accountId) => resolveClickClackAccount({
146
+ cfg,
147
+ accountId
148
+ })).filter((account) => account.enabled);
149
+ }
150
+ //#endregion
151
+ //#region extensions/clickclack/src/channel-config.ts
152
+ const CLICKCLACK_CHANNEL_ID = "clickclack";
153
+ const clickClackMeta = { ...getChatChannelMeta(CLICKCLACK_CHANNEL_ID) };
154
+ const clickClackConfigAdapter = {
155
+ listAccountIds: (cfg) => listClickClackAccountIds(cfg),
156
+ resolveAccount: (cfg, accountId) => resolveClickClackAccount({
157
+ cfg,
158
+ accountId
159
+ }),
160
+ defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg),
161
+ isConfigured: (account) => account.configured,
162
+ resolveAllowFrom: ({ cfg, accountId }) => resolveClickClackAccount({
163
+ cfg,
164
+ accountId
165
+ }).allowFrom,
166
+ resolveDefaultTo: ({ cfg, accountId }) => resolveClickClackAccount({
167
+ cfg,
168
+ accountId
169
+ }).defaultTo
170
+ };
171
+ //#endregion
172
+ //#region extensions/clickclack/src/config-schema.ts
173
+ /**
174
+ * Zod-backed config schema for ClickClack channel accounts.
175
+ */
176
+ const ClickClackAccountConfigSchema = z.object({
177
+ name: z.string().optional(),
178
+ enabled: z.boolean().optional(),
179
+ baseUrl: z.string().url().optional(),
180
+ token: buildSecretInputSchema().optional(),
181
+ tokenFile: z.string().optional(),
182
+ workspace: z.string().optional(),
183
+ botUserId: z.string().optional(),
184
+ agentId: z.string().optional(),
185
+ replyMode: z.enum(["agent", "model"]).optional(),
186
+ model: z.string().optional(),
187
+ systemPrompt: z.string().optional(),
188
+ timeoutSeconds: z.number().int().min(1).max(3600).optional(),
189
+ toolsAllow: z.array(z.string()).optional(),
190
+ defaultTo: z.string().optional(),
191
+ allowFrom: z.array(z.string()).optional(),
192
+ reconnectMs: z.number().int().min(100).max(6e4).optional(),
193
+ agentActivity: z.boolean().optional(),
194
+ commandMenu: z.boolean().optional()
195
+ }).strict();
196
+ /**
197
+ * Config schema exported to core so `openclaw doctor` and config validation
198
+ * understand both default and named ClickClack accounts.
199
+ */
200
+ const clickClackConfigSchema = buildChannelConfigSchema(buildMultiAccountChannelSchema(ClickClackAccountConfigSchema, { accountSchema: ClickClackAccountConfigSchema.partial() }));
201
+ //#endregion
202
+ //#region extensions/clickclack/src/http-client.ts
203
+ /**
204
+ * Thin ClickClack REST/websocket client used by gateway, resolver, and outbound
205
+ * delivery code.
206
+ */
207
+ /**
208
+ * Serializes optional provenance into the wire fields. Unknown JSON fields
209
+ * are ignored by servers without the provenance columns, so these are safe
210
+ * to send unconditionally when present.
211
+ */
212
+ function provenanceFields(provenance) {
213
+ const fields = {};
214
+ if (provenance?.model?.trim()) fields.author_model = provenance.model.trim();
215
+ if (provenance?.thinking?.trim()) fields.author_thinking = provenance.thinking.trim();
216
+ if (provenance?.runtime?.trim()) fields.author_runtime = provenance.runtime.trim();
217
+ return fields;
218
+ }
219
+ const CLICKCLACK_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
220
+ const CLICKCLACK_CORRELATION_ID_MAX_LENGTH = 128;
221
+ const CLICKCLACK_CORRELATION_ID_PATTERN = /^[A-Za-z0-9._:-]+$/u;
222
+ const CLICKCLACK_CORRELATION_ID_HEADER = "X-Correlation-ID";
223
+ const CLICKCLACK_INBOUND_JSON_LIMIT_BYTES = 16 * 1024 * 1024;
224
+ const CLICKCLACK_WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 3e4;
225
+ var ClickClackHttpError = class extends Error {
226
+ constructor(status, detail, headers) {
227
+ super(`ClickClack ${status}: ${detail}`);
228
+ this.status = status;
229
+ this.headers = headers;
230
+ }
231
+ };
232
+ /** Accepts the same bounded request-correlation shape as the ClickClack API. */
233
+ function normalizeClickClackCorrelationId(value) {
234
+ if (typeof value !== "string") return;
235
+ const normalized = value.trim();
236
+ if (!normalized || normalized.length > CLICKCLACK_CORRELATION_ID_MAX_LENGTH || !CLICKCLACK_CORRELATION_ID_PATTERN.test(normalized)) return;
237
+ return normalized;
238
+ }
239
+ /**
240
+ * Creates a typed client for the ClickClack API using bearer-token auth.
241
+ */
242
+ function createClickClackClient(options) {
243
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
244
+ const fetcher = options.fetch ?? fetch;
245
+ const correlationId = normalizeClickClackCorrelationId(options.correlationId);
246
+ const headers = {
247
+ Authorization: `Bearer ${options.token}`,
248
+ Accept: "application/json"
249
+ };
250
+ async function request(path, init = {}) {
251
+ const requestHeaders = new Headers(init.headers);
252
+ for (const [key, value] of Object.entries(headers)) requestHeaders.set(key, value);
253
+ if (correlationId) requestHeaders.set(CLICKCLACK_CORRELATION_ID_HEADER, correlationId);
254
+ if (init.body && !(init.body instanceof FormData)) requestHeaders.set("Content-Type", "application/json");
255
+ const response = await fetcher(`${baseUrl}${path}`, {
256
+ ...init,
257
+ headers: requestHeaders
258
+ });
259
+ if (!response.ok) {
260
+ const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
261
+ throw new ClickClackHttpError(response.status, detail, new Headers(response.headers));
262
+ }
263
+ return await readProviderJsonResponse(response, "ClickClack response", { maxBytes: CLICKCLACK_INBOUND_JSON_LIMIT_BYTES });
264
+ }
265
+ async function fetchEventPage(workspaceId, pageOptions = {}) {
266
+ const query = new URLSearchParams({ workspace_id: workspaceId });
267
+ if (pageOptions.afterCursor) query.set("after_cursor", pageOptions.afterCursor);
268
+ if (pageOptions.limit !== void 0) query.set("limit", String(pageOptions.limit));
269
+ if (pageOptions.includeTail) query.set("include_tail", "true");
270
+ const data = await request(`/api/realtime/events?${query.toString()}`);
271
+ return {
272
+ events: data.events,
273
+ ...typeof data.tail_cursor === "string" ? { tailCursor: data.tail_cursor } : {}
274
+ };
275
+ }
276
+ return {
277
+ me: async () => {
278
+ return (await request("/api/me")).user;
279
+ },
280
+ setBotCommands: async (commands) => {
281
+ return (await request("/api/bots/self/commands", {
282
+ method: "PUT",
283
+ body: JSON.stringify({ commands })
284
+ })).bot_commands;
285
+ },
286
+ workspaces: async () => {
287
+ return (await request("/api/workspaces")).workspaces;
288
+ },
289
+ channels: async (workspaceId) => {
290
+ return (await request(`/api/workspaces/${encodeURIComponent(workspaceId)}/channels`)).channels;
291
+ },
292
+ channelMessages: async (channelId, afterSeq, limit = 20) => {
293
+ return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
294
+ },
295
+ directMessages: async (conversationId, afterSeq, limit = 20) => {
296
+ return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
297
+ },
298
+ thread: async (messageId) => await request(`/api/messages/${encodeURIComponent(messageId)}/thread`),
299
+ message: async (messageId) => {
300
+ return (await request(`/api/messages/${encodeURIComponent(messageId)}`)).message;
301
+ },
302
+ findMessageByNonce: async (params) => {
303
+ const query = new URLSearchParams({
304
+ workspace_id: params.workspaceId,
305
+ nonce: params.nonce
306
+ });
307
+ try {
308
+ return (await request(`/api/messages/by-nonce?${query.toString()}`)).message;
309
+ } catch (error) {
310
+ if (error instanceof ClickClackHttpError && error.status === 404) {
311
+ if (error.headers.get("X-ClickClack-Message-Nonce") === "supported") return;
312
+ throw new Error("ClickClack server does not support durable message nonce lookup", { cause: error });
313
+ }
314
+ throw error;
315
+ }
316
+ },
317
+ createChannelMessage: async (channelId, body, opts) => {
318
+ return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages`, {
319
+ method: "POST",
320
+ body: JSON.stringify({
321
+ body,
322
+ ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
323
+ ...opts?.nonce ? { nonce: opts.nonce } : {},
324
+ ...provenanceFields(opts?.provenance)
325
+ })
326
+ })).message;
327
+ },
328
+ createThreadReply: async (messageId, body, opts) => {
329
+ return (await request(`/api/messages/${encodeURIComponent(messageId)}/thread/replies`, {
330
+ method: "POST",
331
+ body: JSON.stringify({
332
+ body,
333
+ ...opts?.nonce ? { nonce: opts.nonce } : {},
334
+ ...provenanceFields(opts?.provenance)
335
+ })
336
+ })).message;
337
+ },
338
+ createDirectConversation: async (workspaceId, memberIds) => {
339
+ return (await request("/api/dms", {
340
+ method: "POST",
341
+ body: JSON.stringify({
342
+ workspace_id: workspaceId,
343
+ member_ids: memberIds
344
+ })
345
+ })).conversation;
346
+ },
347
+ createUpload: async (params) => {
348
+ const form = new FormData();
349
+ const bytes = new Uint8Array(params.buffer);
350
+ form.append("file", new Blob([bytes], { type: params.contentType }), params.filename);
351
+ const query = new URLSearchParams({ workspace_id: params.workspaceId });
352
+ if (params.nonce) query.set("nonce", params.nonce);
353
+ return (await request(`/api/uploads?${query.toString()}`, {
354
+ method: "POST",
355
+ body: form
356
+ })).upload;
357
+ },
358
+ findUploadByNonce: async (params) => {
359
+ const query = new URLSearchParams({
360
+ workspace_id: params.workspaceId,
361
+ nonce: params.nonce
362
+ });
363
+ try {
364
+ return (await request(`/api/uploads/by-nonce?${query.toString()}`)).upload;
365
+ } catch (error) {
366
+ if (error instanceof ClickClackHttpError && error.status === 404) {
367
+ if (error.headers.get("X-ClickClack-Upload-Nonce") === "supported") return;
368
+ throw new Error("ClickClack server does not support durable upload nonce lookup", { cause: error });
369
+ }
370
+ throw error;
371
+ }
372
+ },
373
+ attachUpload: async (messageId, uploadId) => {
374
+ await request(`/api/messages/${encodeURIComponent(messageId)}/attachments`, {
375
+ method: "POST",
376
+ body: JSON.stringify({ upload_id: uploadId })
377
+ });
378
+ },
379
+ /**
380
+ * POSTs a durable agent activity row (agent_commentary / agent_tool)
381
+ * through the normal message create path. Requires a bot token carrying
382
+ * the agent_activity:write scope on the ClickClack side.
383
+ */
384
+ createActivityMessage: async (params) => {
385
+ if (!params.channelId && !params.conversationId) throw new Error("createActivityMessage requires a channelId or conversationId");
386
+ return (await request(params.channelId ? `/api/channels/${encodeURIComponent(params.channelId)}/messages` : `/api/dms/${encodeURIComponent(params.conversationId ?? "")}/messages`, {
387
+ method: "POST",
388
+ body: JSON.stringify({
389
+ body: params.body,
390
+ kind: params.kind,
391
+ turn_id: params.turnId,
392
+ ...provenanceFields(params.provenance)
393
+ })
394
+ })).message;
395
+ },
396
+ /** PATCHes the body of an existing message (activity row coalescing). */
397
+ updateMessageBody: async (messageId, body) => {
398
+ return (await request(`/api/messages/${encodeURIComponent(messageId)}`, {
399
+ method: "PATCH",
400
+ body: JSON.stringify({ body })
401
+ })).message;
402
+ },
403
+ createDirectMessage: async (conversationId, body, opts) => {
404
+ return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages`, {
405
+ method: "POST",
406
+ body: JSON.stringify({
407
+ body,
408
+ ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
409
+ ...opts?.nonce ? { nonce: opts.nonce } : {}
410
+ })
411
+ })).message;
412
+ },
413
+ events: async (workspaceId, afterCursor) => (await fetchEventPage(workspaceId, { afterCursor })).events,
414
+ eventPage: fetchEventPage,
415
+ websocket: (workspaceId, afterCursor) => {
416
+ const url = new URL(`${baseUrl}/api/realtime/ws`);
417
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
418
+ url.searchParams.set("workspace_id", workspaceId);
419
+ if (afterCursor) url.searchParams.set("after_cursor", afterCursor);
420
+ return new WebSocket(url, {
421
+ headers: { Authorization: `Bearer ${options.token}` },
422
+ handshakeTimeout: CLICKCLACK_WEBSOCKET_HANDSHAKE_TIMEOUT_MS,
423
+ maxPayload: CLICKCLACK_INBOUND_JSON_LIMIT_BYTES
424
+ });
425
+ }
426
+ };
427
+ }
428
+ //#endregion
429
+ //#region extensions/clickclack/src/resolve.ts
430
+ /**
431
+ * Resolves a workspace slug/name/id from config to a ClickClack workspace id.
432
+ */
433
+ async function resolveWorkspaceId(client, workspace) {
434
+ if (workspace.startsWith("wsp_")) return workspace;
435
+ const found = (await client.workspaces()).find((candidate) => candidate.id === workspace || candidate.slug === workspace || candidate.name === workspace);
436
+ if (!found) throw new Error(`ClickClack workspace not found: ${workspace}`);
437
+ return found.id;
438
+ }
439
+ /**
440
+ * Resolves a channel name/id from config or target input to a ClickClack
441
+ * channel id.
442
+ */
443
+ async function resolveChannelId(client, workspaceId, channel) {
444
+ if (channel.startsWith("chn_")) return channel;
445
+ const found = (await client.channels(workspaceId)).find((candidate) => candidate.id === channel || candidate.name === channel);
446
+ if (!found) throw new Error(`ClickClack channel not found: ${channel}`);
447
+ return found.id;
448
+ }
449
+ //#endregion
450
+ //#region extensions/clickclack/src/setup-core.ts
451
+ const channel$1 = "clickclack";
452
+ const SETUP_CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
453
+ const SETUP_CODE_LENGTH = 12;
454
+ const REQUIRED_INPUT_ERROR = "ClickClack requires --token, --base-url, and --workspace (or --use-env).";
455
+ const INVALID_BASE_URL_ERROR = "ClickClack base URL must be a valid http(s) URL.";
456
+ const SETUP_CODE_CONFLICT_ERROR = "ClickClack --code cannot be combined with --token, --token-file, or --use-env.";
457
+ function normalizeClickClackBaseUrl(value) {
458
+ const trimmed = value?.trim();
459
+ if (!trimmed) return;
460
+ try {
461
+ const parsed = new URL(trimmed);
462
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
463
+ return parsed.toString().replace(/\/+$/, "");
464
+ } catch {
465
+ return;
466
+ }
467
+ }
468
+ function normalizeClickClackSetupCode(value) {
469
+ const normalized = value.trim().toUpperCase().replaceAll("-", "").replaceAll(" ", "");
470
+ if (normalized.length !== SETUP_CODE_LENGTH || Array.from(normalized).some((character) => !SETUP_CODE_ALPHABET.includes(character))) return;
471
+ return normalized;
472
+ }
473
+ function requireClickClackSetupCodeBaseUrl(value) {
474
+ const baseUrl = normalizeClickClackBaseUrl(value);
475
+ if (!baseUrl) throw new Error("ClickClack setup codes require a valid HTTP(S) base URL.");
476
+ return baseUrl;
477
+ }
478
+ function parseClickClackSetupCodeInput(params) {
479
+ const rawCode = params.code.trim();
480
+ if (!rawCode) throw new Error("ClickClack --code must not be empty.");
481
+ let code = rawCode;
482
+ let baseUrl;
483
+ if (/^[a-z][a-z\d+.-]*:\/\//iu.test(rawCode)) {
484
+ let setupUrl;
485
+ try {
486
+ setupUrl = new URL(rawCode);
487
+ } catch {
488
+ throw new Error("ClickClack --code must be a valid HTTP(S) setup URL or a bare setup code.");
489
+ }
490
+ if (setupUrl.protocol !== "http:" && setupUrl.protocol !== "https:") throw new Error("ClickClack setup codes require an HTTP(S) URL.");
491
+ if (setupUrl.username || setupUrl.password) throw new Error("ClickClack setup URLs must not include credentials.");
492
+ code = setupUrl.hash.slice(1);
493
+ if (!code) throw new Error("ClickClack setup URL is missing its #CODE fragment.");
494
+ setupUrl.hash = "";
495
+ setupUrl.search = "";
496
+ baseUrl = requireClickClackSetupCodeBaseUrl(setupUrl.toString());
497
+ if (params.baseUrl) {
498
+ if (requireClickClackSetupCodeBaseUrl(params.baseUrl) !== baseUrl) throw new Error("ClickClack --base-url does not match the server in the setup-code URL.");
499
+ }
500
+ } else {
501
+ code = code.startsWith("#") ? code.slice(1) : code;
502
+ if (!params.baseUrl) throw new Error("A bare ClickClack setup code requires --base-url.");
503
+ baseUrl = requireClickClackSetupCodeBaseUrl(params.baseUrl);
504
+ }
505
+ const normalizedCode = normalizeClickClackSetupCode(code);
506
+ if (!normalizedCode) throw new Error("ClickClack setup code must contain 12 valid base32 characters.");
507
+ return {
508
+ code: normalizedCode,
509
+ baseUrl
510
+ };
511
+ }
512
+ function formatClickClackSetupCodeClaimError(error) {
513
+ if (typeof error === "object" && error !== null && "status" in error) {
514
+ const status = error.status;
515
+ if (status === 404) return /* @__PURE__ */ new Error("ClickClack setup code is invalid, expired, or already used. Generate a new code and try again.");
516
+ if (status === 429) return /* @__PURE__ */ new Error("Too many ClickClack setup code attempts. Wait and try again.");
517
+ }
518
+ return /* @__PURE__ */ new Error(`Could not claim ClickClack setup code: ${formatErrorMessage(error)}`);
519
+ }
520
+ function applyClickClackSetupConfigPatch(params) {
521
+ const accountId = normalizeAccountId(params.accountId);
522
+ const namedConfig = applyAccountNameToChannelSection({
523
+ cfg: accountId === DEFAULT_ACCOUNT_ID ? params.cfg : moveSingleAccountChannelSectionToDefaultAccount({
524
+ cfg: params.cfg,
525
+ channelKey: channel$1
526
+ }),
527
+ channelKey: channel$1,
528
+ accountId,
529
+ name: params.name
530
+ });
531
+ return applySetupAccountConfigPatch({
532
+ cfg: accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
533
+ cfg: namedConfig,
534
+ channelKey: channel$1
535
+ }) : namedConfig,
536
+ channelKey: channel$1,
537
+ accountId,
538
+ patch: params.patch
539
+ });
540
+ }
541
+ function clearClickClackSetupConfigFields(params) {
542
+ const clickclack = params.cfg.channels?.clickclack;
543
+ if (!clickclack) return params.cfg;
544
+ const accountId = normalizeAccountId(params.accountId);
545
+ if (accountId === DEFAULT_ACCOUNT_ID) {
546
+ const nextClickClack = { ...clickclack };
547
+ for (const field of params.fields) delete nextClickClack[field];
548
+ return {
549
+ ...params.cfg,
550
+ channels: {
551
+ ...params.cfg.channels,
552
+ clickclack: nextClickClack
553
+ }
554
+ };
555
+ }
556
+ const currentAccount = clickclack.accounts?.[accountId];
557
+ if (!currentAccount) return params.cfg;
558
+ const nextAccount = { ...currentAccount };
559
+ for (const field of params.fields) delete nextAccount[field];
560
+ return {
561
+ ...params.cfg,
562
+ channels: {
563
+ ...params.cfg.channels,
564
+ clickclack: {
565
+ ...clickclack,
566
+ accounts: {
567
+ ...clickclack.accounts,
568
+ [accountId]: nextAccount
569
+ }
570
+ }
571
+ }
572
+ };
573
+ }
574
+ function applyClickClackCredentialConfig(params) {
575
+ const fieldsToClear = params.useEnv ? ["token", "tokenFile"] : params.tokenFile ? ["token"] : params.token !== void 0 ? ["tokenFile"] : [];
576
+ return clearClickClackSetupConfigFields({
577
+ cfg: applyClickClackSetupConfigPatch({
578
+ cfg: params.cfg,
579
+ accountId: params.accountId,
580
+ patch: params.useEnv ? {} : params.tokenFile ? { tokenFile: params.tokenFile } : params.token !== void 0 ? { token: params.token } : {}
581
+ }),
582
+ accountId: params.accountId,
583
+ fields: fieldsToClear
584
+ });
585
+ }
586
+ const clickClackSetupAdapter = {
587
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
588
+ prepareAccountConfigInput: async ({ input }) => {
589
+ if (!input.code?.trim()) return input;
590
+ if (input.token?.trim() || input.tokenFile?.trim() || input.useEnv) throw new Error(SETUP_CODE_CONFLICT_ERROR);
591
+ const setup = parseClickClackSetupCodeInput({
592
+ code: input.code,
593
+ baseUrl: input.baseUrl
594
+ });
595
+ let claim;
596
+ try {
597
+ const { claimClickClackSetupCode } = await import("./setup-claim-BUWvZjtu.js");
598
+ claim = await claimClickClackSetupCode(setup);
599
+ } catch (error) {
600
+ throw formatClickClackSetupCodeClaimError(error);
601
+ }
602
+ const { code: _code, tokenFile: _tokenFile, useEnv: _useEnv, ...remainingInput } = input;
603
+ return {
604
+ ...remainingInput,
605
+ baseUrl: setup.baseUrl,
606
+ token: claim.token,
607
+ workspace: claim.workspace.id,
608
+ ...claim.defaults.defaultTo !== void 0 ? { defaultTo: claim.defaults.defaultTo } : {},
609
+ ...claim.defaults.allowFrom !== void 0 ? { allowFrom: [...claim.defaults.allowFrom] } : {},
610
+ ...claim.defaults.agentActivity !== void 0 ? { agentActivity: claim.defaults.agentActivity } : {}
611
+ };
612
+ },
613
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
614
+ cfg,
615
+ channelKey: channel$1,
616
+ accountId,
617
+ name
618
+ }),
619
+ validateInput: createSetupInputPresenceValidator({
620
+ defaultAccountOnlyEnvError: "CLICKCLACK_BOT_TOKEN can only be used for the default account.",
621
+ whenNotUseEnv: [
622
+ {
623
+ someOf: ["token", "tokenFile"],
624
+ message: REQUIRED_INPUT_ERROR
625
+ },
626
+ {
627
+ someOf: ["baseUrl"],
628
+ message: REQUIRED_INPUT_ERROR
629
+ },
630
+ {
631
+ someOf: ["workspace"],
632
+ message: REQUIRED_INPUT_ERROR
633
+ }
634
+ ],
635
+ validate: ({ cfg, accountId, input }) => {
636
+ const baseUrl = normalizeClickClackBaseUrl(input.baseUrl);
637
+ if (input.baseUrl && !baseUrl) return INVALID_BASE_URL_ERROR;
638
+ if (!input.useEnv) return null;
639
+ const existing = resolveClickClackAccountConfig(cfg, accountId);
640
+ const existingBaseUrl = normalizeClickClackBaseUrl(existing.baseUrl);
641
+ if (!baseUrl && existing.baseUrl?.trim() && !existingBaseUrl) return INVALID_BASE_URL_ERROR;
642
+ if (!baseUrl && !existingBaseUrl) return REQUIRED_INPUT_ERROR;
643
+ if (!input.workspace?.trim() && !existing.workspace?.trim()) return REQUIRED_INPUT_ERROR;
644
+ return null;
645
+ }
646
+ }),
647
+ applyAccountConfig: ({ cfg, accountId, input }) => {
648
+ const existing = input.useEnv ? resolveClickClackAccountConfig(cfg, accountId) : void 0;
649
+ const baseUrl = normalizeClickClackBaseUrl(input.baseUrl ?? existing?.baseUrl);
650
+ const workspace = input.workspace?.trim() || existing?.workspace?.trim();
651
+ const tokenFile = input.tokenFile?.trim();
652
+ const token = input.token?.trim();
653
+ return applyClickClackCredentialConfig({
654
+ cfg: applyClickClackSetupConfigPatch({
655
+ cfg,
656
+ accountId,
657
+ name: input.name,
658
+ patch: {
659
+ ...baseUrl ? { baseUrl } : {},
660
+ ...workspace ? { workspace } : {},
661
+ ...input.defaultTo?.trim() ? { defaultTo: input.defaultTo.trim() } : {},
662
+ ...input.allowFrom ? { allowFrom: [...input.allowFrom] } : {},
663
+ ...input.agentActivity !== void 0 ? { agentActivity: input.agentActivity } : {}
664
+ }
665
+ }),
666
+ accountId,
667
+ token,
668
+ tokenFile,
669
+ useEnv: input.useEnv
670
+ });
671
+ },
672
+ afterAccountConfigWritten: async ({ cfg, accountId, runtime }) => {
673
+ const { verifyClickClackAccountAfterSetup } = await Promise.resolve().then(() => setup_verify_exports);
674
+ await verifyClickClackAccountAfterSetup({
675
+ cfg,
676
+ accountId,
677
+ runtime
678
+ });
679
+ }
680
+ };
681
+ //#endregion
682
+ //#region extensions/clickclack/src/setup-verify.ts
683
+ var setup_verify_exports = /* @__PURE__ */ __exportAll({
684
+ checkClickClackSetupConnection: () => checkClickClackSetupConnection,
685
+ verifyClickClackAccountAfterSetup: () => verifyClickClackAccountAfterSetup
686
+ });
687
+ const GATEWAY_RUNNING_MESSAGE = "OpenClaw is running — ClickClack will connect automatically.";
688
+ const GATEWAY_NOT_RUNNING_MESSAGE = "Start OpenClaw to connect: openclaw gateway";
689
+ const GATEWAY_UNKNOWN_MESSAGE = "If OpenClaw is running it connects automatically; otherwise start it with: openclaw gateway";
690
+ function isHttpStatus(error, status) {
691
+ return typeof error === "object" && error !== null && "status" in error && error.status === status;
692
+ }
693
+ function isWorkspaceNotFound(error) {
694
+ return error instanceof Error && error.message.startsWith("ClickClack workspace not found:");
695
+ }
696
+ function usesUnavailableImplicitEnvToken(account, tokenOverride) {
697
+ return account.accountId === DEFAULT_ACCOUNT_ID && Boolean(account.baseUrl && account.workspace) && !tokenOverride && !account.token && !hasConfiguredSecretInput(account.config.token) && !account.config.tokenFile?.trim();
698
+ }
699
+ async function checkClickClackSetupConnection(params) {
700
+ let workspaceInput = "";
701
+ try {
702
+ const account = resolveClickClackAccount({
703
+ cfg: params.cfg,
704
+ accountId: params.accountId
705
+ });
706
+ workspaceInput = account.workspace;
707
+ const token = params.token?.trim() || account.token;
708
+ if (usesUnavailableImplicitEnvToken(account, token)) return { status: "skipped-env-token" };
709
+ if (!account.baseUrl || !account.workspace || !token) return { status: "skipped-unconfigured" };
710
+ const client = createClickClackClient({
711
+ baseUrl: account.baseUrl,
712
+ token
713
+ });
714
+ const me = await client.me();
715
+ const workspaceId = await resolveWorkspaceId(client, account.workspace);
716
+ const workspace = (await client.workspaces()).find((candidate) => candidate.id === workspaceId);
717
+ if (!workspace) throw new Error(`ClickClack workspace not found: ${account.workspace}`);
718
+ return {
719
+ status: "connected",
720
+ handle: me.handle,
721
+ workspaceName: workspace.name
722
+ };
723
+ } catch (error) {
724
+ if (isHttpStatus(error, 401)) return { status: "invalid-token" };
725
+ if (isWorkspaceNotFound(error)) return {
726
+ status: "workspace-not-found",
727
+ workspace: workspaceInput
728
+ };
729
+ return {
730
+ status: "failed",
731
+ error: formatErrorMessage(error)
732
+ };
733
+ }
734
+ }
735
+ function isGatewayNotRunningError(error) {
736
+ if (typeof error === "object" && error !== null && "name" in error && "kind" in error && "code" in error && error.name === "GatewayTransportError" && error.kind === "closed" && error.code === 1006) return true;
737
+ const message = formatErrorMessage(error).toLowerCase();
738
+ return message.includes("econnrefused") || message.includes("connection refused");
739
+ }
740
+ async function probeClickClackGatewayStatus() {
741
+ try {
742
+ const { callGatewayFromCli } = await import("openclaw/plugin-sdk/gateway-runtime");
743
+ await callGatewayFromCli("health", {
744
+ timeout: "1000",
745
+ json: true
746
+ }, void 0, {
747
+ expectFinal: false,
748
+ progress: false
749
+ });
750
+ return "running";
751
+ } catch (error) {
752
+ return isGatewayNotRunningError(error) ? "not-running" : "unavailable";
753
+ }
754
+ }
755
+ function formatClickClackConnectionLog(result) {
756
+ switch (result.status) {
757
+ case "connected": return `Connected as @${result.handle} — workspace ${result.workspaceName} resolved.`;
758
+ case "invalid-token": return "ClickClack rejected the bot token (401). Copy a current token and rerun setup.";
759
+ case "workspace-not-found": return `Workspace "${result.workspace}" was not found. Check the id, slug, or name, list available workspaces, and rerun setup.`;
760
+ case "failed": return `Connection check failed: ${result.error}. Setup was saved; fix the connection and rerun setup.`;
761
+ case "skipped-env-token": return "Token comes from CLICKCLACK_BOT_TOKEN; verification skipped.";
762
+ case "skipped-unconfigured": return;
763
+ }
764
+ }
765
+ function formatClickClackGatewayLog(status) {
766
+ switch (status) {
767
+ case "running": return GATEWAY_RUNNING_MESSAGE;
768
+ case "not-running": return GATEWAY_NOT_RUNNING_MESSAGE;
769
+ case "unavailable": return GATEWAY_UNKNOWN_MESSAGE;
770
+ }
771
+ return GATEWAY_UNKNOWN_MESSAGE;
772
+ }
773
+ async function verifyClickClackAccountAfterSetup(params) {
774
+ try {
775
+ const message = formatClickClackConnectionLog(await checkClickClackSetupConnection({
776
+ cfg: params.cfg,
777
+ accountId: params.accountId
778
+ }));
779
+ if (message) params.runtime.log(message);
780
+ } catch (error) {
781
+ params.runtime.log(`Connection check failed: ${formatErrorMessage(error)}. Setup was saved; fix the connection and rerun setup.`);
782
+ }
783
+ try {
784
+ const status = await probeClickClackGatewayStatus();
785
+ params.runtime.log(formatClickClackGatewayLog(status));
786
+ } catch {
787
+ params.runtime.log(GATEWAY_UNKNOWN_MESSAGE);
788
+ }
789
+ }
790
+ //#endregion
791
+ //#region extensions/clickclack/src/setup-surface.ts
792
+ const t = createSetupTranslator();
793
+ const channel = "clickclack";
794
+ function hasConfiguredClickClackCredential(account) {
795
+ return hasConfiguredSecretInput(account.config.token) || Boolean(account.config.tokenFile?.trim());
796
+ }
797
+ function isClickClackSetupConfigured(account) {
798
+ return Boolean(account.baseUrl && account.workspace && (account.token || hasConfiguredClickClackCredential(account)));
799
+ }
800
+ const clickClackSetupWizard = {
801
+ channel,
802
+ status: createStandardChannelSetupStatus({
803
+ channelLabel: "ClickClack",
804
+ configuredLabel: t("wizard.channels.statusConfigured"),
805
+ unconfiguredLabel: t("wizard.channels.statusNeedsSetup"),
806
+ configuredHint: t("wizard.channels.statusSelfHostedChat"),
807
+ unconfiguredHint: t("wizard.channels.statusNeedsSetup"),
808
+ configuredScore: 2,
809
+ unconfiguredScore: 1,
810
+ resolveConfigured: ({ cfg, accountId }) => (accountId ? [accountId] : listClickClackAccountIds(cfg)).some((resolvedAccountId) => isClickClackSetupConfigured(resolveClickClackAccount({
811
+ cfg,
812
+ accountId: resolvedAccountId
813
+ })))
814
+ }),
815
+ introNote: {
816
+ title: t("wizard.clickclack.botTokenTitle"),
817
+ lines: [t("wizard.clickclack.helpCreateToken"), t("wizard.channels.docs", { link: formatDocsLink("/channels/clickclack", "clickclack") })],
818
+ shouldShow: ({ cfg, accountId }) => !isClickClackSetupConfigured(resolveClickClackAccount({
819
+ cfg,
820
+ accountId
821
+ }))
822
+ },
823
+ credentials: [defineTokenCredential({
824
+ inputKey: "token",
825
+ configKey: "token",
826
+ configuredFields: ["token", "tokenFile"],
827
+ providerHint: channel,
828
+ credentialLabel: t("wizard.clickclack.botToken"),
829
+ preferredEnvVar: "CLICKCLACK_BOT_TOKEN",
830
+ envPrompt: t("wizard.clickclack.envPrompt"),
831
+ keepPrompt: t("wizard.clickclack.botTokenKeep"),
832
+ inputPrompt: t("wizard.clickclack.botTokenInput"),
833
+ allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1,
834
+ resolveAccount: ({ cfg, accountId }) => resolveClickClackAccount({
835
+ cfg,
836
+ accountId
837
+ }),
838
+ hasConfiguredValue: hasConfiguredClickClackCredential,
839
+ resolvedValue: (account) => account.token || void 0,
840
+ envValue: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1 ? process.env.CLICKCLACK_BOT_TOKEN?.trim() || void 0 : void 0,
841
+ patchAccount: ({ cfg, accountId, mode, patch }) => mode === "env" ? applyClickClackCredentialConfig({
842
+ cfg,
843
+ accountId,
844
+ useEnv: true
845
+ }) : applyClickClackCredentialConfig({
846
+ cfg,
847
+ accountId,
848
+ ...patch
849
+ }),
850
+ useEnv: { clearFields: ["token", "tokenFile"] },
851
+ set: { clearFields: ["tokenFile"] }
852
+ })],
853
+ textInputs: [baseUrlTextInput({
854
+ inputKey: "baseUrl",
855
+ configKey: "baseUrl",
856
+ message: t("wizard.clickclack.baseUrlPrompt"),
857
+ resolveAccount: ({ cfg, accountId }) => resolveClickClackAccount({
858
+ cfg,
859
+ accountId
860
+ }),
861
+ currentValue: (account) => account.baseUrl || void 0,
862
+ includeInitialValue: true,
863
+ validate: (value) => normalizeClickClackBaseUrl(value) ? void 0 : "ClickClack server URL must be a valid http(s) URL.",
864
+ normalize: (value) => normalizeClickClackBaseUrl(value) ?? value.trim(),
865
+ patchAccount: ({ cfg, accountId, patch }) => applyClickClackSetupConfigPatch({
866
+ cfg,
867
+ accountId,
868
+ patch
869
+ })
870
+ }), {
871
+ inputKey: "workspace",
872
+ message: t("wizard.clickclack.workspacePrompt"),
873
+ helpTitle: t("wizard.clickclack.workspacePrompt"),
874
+ helpLines: [t("wizard.clickclack.workspaceHelp")],
875
+ currentValue: ({ cfg, accountId }) => resolveClickClackAccount({
876
+ cfg,
877
+ accountId
878
+ }).workspace || void 0,
879
+ initialValue: ({ cfg, accountId }) => resolveClickClackAccount({
880
+ cfg,
881
+ accountId
882
+ }).workspace || void 0,
883
+ validate: ({ value }) => value.trim() ? void 0 : "Required",
884
+ normalizeValue: ({ value }) => value.trim(),
885
+ applySet: async ({ cfg, accountId, value }) => applyClickClackSetupConfigPatch({
886
+ cfg,
887
+ accountId,
888
+ patch: { workspace: value }
889
+ })
890
+ }],
891
+ finalize: async ({ cfg, accountId, credentialValues, prompter }) => {
892
+ const result = await checkClickClackSetupConnection({
893
+ cfg,
894
+ accountId,
895
+ token: credentialValues.token
896
+ });
897
+ if (result.status === "connected") {
898
+ await prompter.note(t("wizard.clickclack.connected", {
899
+ handle: result.handle,
900
+ workspace: result.workspaceName
901
+ }), t("wizard.clickclack.connectionTitle"));
902
+ return;
903
+ }
904
+ if (result.status === "skipped-env-token" || result.status === "skipped-unconfigured") return;
905
+ const message = result.status === "invalid-token" ? t("wizard.clickclack.invalidToken") : result.status === "workspace-not-found" ? t("wizard.clickclack.workspaceNotFound", { workspace: result.workspace }) : t("wizard.clickclack.connectionFailed", { error: result.error });
906
+ await prompter.note(message, t("wizard.clickclack.validationWarningTitle"));
907
+ },
908
+ disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
909
+ };
910
+ //#endregion
911
+ export { createClickClackClient as a, CLICKCLACK_CHANNEL_ID as c, DEFAULT_ACCOUNT_ID$2 as d, listClickClackAccountIds as f, resolveDefaultClickClackAccountId as h, resolveWorkspaceId as i, clickClackConfigAdapter as l, resolveClickClackAccount as m, clickClackSetupAdapter as n, normalizeClickClackCorrelationId as o, listEnabledClickClackAccounts as p, resolveChannelId as r, clickClackConfigSchema as s, clickClackSetupWizard as t, clickClackMeta as u };