@openclaw/clickclack 2026.7.1 → 2026.7.2-beta.2

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,828 @@
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 REQUIRED_INPUT_ERROR = "ClickClack requires --token, --base-url, and --workspace (or --use-env).";
453
+ const INVALID_BASE_URL_ERROR = "ClickClack base URL must be a valid http(s) URL.";
454
+ function normalizeClickClackBaseUrl(value) {
455
+ const trimmed = value?.trim();
456
+ if (!trimmed) return;
457
+ try {
458
+ const parsed = new URL(trimmed);
459
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
460
+ return parsed.toString().replace(/\/+$/, "");
461
+ } catch {
462
+ return;
463
+ }
464
+ }
465
+ function applyClickClackSetupConfigPatch(params) {
466
+ const accountId = normalizeAccountId(params.accountId);
467
+ const namedConfig = applyAccountNameToChannelSection({
468
+ cfg: accountId === DEFAULT_ACCOUNT_ID ? params.cfg : moveSingleAccountChannelSectionToDefaultAccount({
469
+ cfg: params.cfg,
470
+ channelKey: channel$1
471
+ }),
472
+ channelKey: channel$1,
473
+ accountId,
474
+ name: params.name
475
+ });
476
+ return applySetupAccountConfigPatch({
477
+ cfg: accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount({
478
+ cfg: namedConfig,
479
+ channelKey: channel$1
480
+ }) : namedConfig,
481
+ channelKey: channel$1,
482
+ accountId,
483
+ patch: params.patch
484
+ });
485
+ }
486
+ function clearClickClackSetupConfigFields(params) {
487
+ const clickclack = params.cfg.channels?.clickclack;
488
+ if (!clickclack) return params.cfg;
489
+ const accountId = normalizeAccountId(params.accountId);
490
+ if (accountId === DEFAULT_ACCOUNT_ID) {
491
+ const nextClickClack = { ...clickclack };
492
+ for (const field of params.fields) delete nextClickClack[field];
493
+ return {
494
+ ...params.cfg,
495
+ channels: {
496
+ ...params.cfg.channels,
497
+ clickclack: nextClickClack
498
+ }
499
+ };
500
+ }
501
+ const currentAccount = clickclack.accounts?.[accountId];
502
+ if (!currentAccount) return params.cfg;
503
+ const nextAccount = { ...currentAccount };
504
+ for (const field of params.fields) delete nextAccount[field];
505
+ return {
506
+ ...params.cfg,
507
+ channels: {
508
+ ...params.cfg.channels,
509
+ clickclack: {
510
+ ...clickclack,
511
+ accounts: {
512
+ ...clickclack.accounts,
513
+ [accountId]: nextAccount
514
+ }
515
+ }
516
+ }
517
+ };
518
+ }
519
+ function applyClickClackCredentialConfig(params) {
520
+ const fieldsToClear = params.useEnv ? ["token", "tokenFile"] : params.tokenFile ? ["token"] : params.token !== void 0 ? ["tokenFile"] : [];
521
+ return clearClickClackSetupConfigFields({
522
+ cfg: applyClickClackSetupConfigPatch({
523
+ cfg: params.cfg,
524
+ accountId: params.accountId,
525
+ patch: params.useEnv ? {} : params.tokenFile ? { tokenFile: params.tokenFile } : params.token !== void 0 ? { token: params.token } : {}
526
+ }),
527
+ accountId: params.accountId,
528
+ fields: fieldsToClear
529
+ });
530
+ }
531
+ const clickClackSetupAdapter = {
532
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
533
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
534
+ cfg,
535
+ channelKey: channel$1,
536
+ accountId,
537
+ name
538
+ }),
539
+ validateInput: createSetupInputPresenceValidator({
540
+ defaultAccountOnlyEnvError: "CLICKCLACK_BOT_TOKEN can only be used for the default account.",
541
+ whenNotUseEnv: [
542
+ {
543
+ someOf: ["token", "tokenFile"],
544
+ message: REQUIRED_INPUT_ERROR
545
+ },
546
+ {
547
+ someOf: ["baseUrl"],
548
+ message: REQUIRED_INPUT_ERROR
549
+ },
550
+ {
551
+ someOf: ["workspace"],
552
+ message: REQUIRED_INPUT_ERROR
553
+ }
554
+ ],
555
+ validate: ({ cfg, accountId, input }) => {
556
+ const baseUrl = normalizeClickClackBaseUrl(input.baseUrl);
557
+ if (input.baseUrl && !baseUrl) return INVALID_BASE_URL_ERROR;
558
+ if (!input.useEnv) return null;
559
+ const existing = resolveClickClackAccountConfig(cfg, accountId);
560
+ const existingBaseUrl = normalizeClickClackBaseUrl(existing.baseUrl);
561
+ if (!baseUrl && existing.baseUrl?.trim() && !existingBaseUrl) return INVALID_BASE_URL_ERROR;
562
+ if (!baseUrl && !existingBaseUrl) return REQUIRED_INPUT_ERROR;
563
+ if (!input.workspace?.trim() && !existing.workspace?.trim()) return REQUIRED_INPUT_ERROR;
564
+ return null;
565
+ }
566
+ }),
567
+ applyAccountConfig: ({ cfg, accountId, input }) => {
568
+ const existing = input.useEnv ? resolveClickClackAccountConfig(cfg, accountId) : void 0;
569
+ const baseUrl = normalizeClickClackBaseUrl(input.baseUrl ?? existing?.baseUrl);
570
+ const workspace = input.workspace?.trim() || existing?.workspace?.trim();
571
+ const tokenFile = input.tokenFile?.trim();
572
+ const token = input.token?.trim();
573
+ return applyClickClackCredentialConfig({
574
+ cfg: applyClickClackSetupConfigPatch({
575
+ cfg,
576
+ accountId,
577
+ name: input.name,
578
+ patch: {
579
+ ...baseUrl ? { baseUrl } : {},
580
+ ...workspace ? { workspace } : {}
581
+ }
582
+ }),
583
+ accountId,
584
+ token,
585
+ tokenFile,
586
+ useEnv: input.useEnv
587
+ });
588
+ },
589
+ afterAccountConfigWritten: async ({ cfg, accountId, runtime }) => {
590
+ const { verifyClickClackAccountAfterSetup } = await Promise.resolve().then(() => setup_verify_exports);
591
+ await verifyClickClackAccountAfterSetup({
592
+ cfg,
593
+ accountId,
594
+ runtime
595
+ });
596
+ }
597
+ };
598
+ //#endregion
599
+ //#region extensions/clickclack/src/setup-verify.ts
600
+ var setup_verify_exports = /* @__PURE__ */ __exportAll({
601
+ checkClickClackSetupConnection: () => checkClickClackSetupConnection,
602
+ verifyClickClackAccountAfterSetup: () => verifyClickClackAccountAfterSetup
603
+ });
604
+ const GATEWAY_RUNNING_MESSAGE = "OpenClaw is running — ClickClack will connect automatically.";
605
+ const GATEWAY_NOT_RUNNING_MESSAGE = "Start OpenClaw to connect: openclaw gateway";
606
+ const GATEWAY_UNKNOWN_MESSAGE = "If OpenClaw is running it connects automatically; otherwise start it with: openclaw gateway";
607
+ function isHttpStatus(error, status) {
608
+ return typeof error === "object" && error !== null && "status" in error && error.status === status;
609
+ }
610
+ function isWorkspaceNotFound(error) {
611
+ return error instanceof Error && error.message.startsWith("ClickClack workspace not found:");
612
+ }
613
+ function usesUnavailableImplicitEnvToken(account, tokenOverride) {
614
+ return account.accountId === DEFAULT_ACCOUNT_ID && Boolean(account.baseUrl && account.workspace) && !tokenOverride && !account.token && !hasConfiguredSecretInput(account.config.token) && !account.config.tokenFile?.trim();
615
+ }
616
+ async function checkClickClackSetupConnection(params) {
617
+ let workspaceInput = "";
618
+ try {
619
+ const account = resolveClickClackAccount({
620
+ cfg: params.cfg,
621
+ accountId: params.accountId
622
+ });
623
+ workspaceInput = account.workspace;
624
+ const token = params.token?.trim() || account.token;
625
+ if (usesUnavailableImplicitEnvToken(account, token)) return { status: "skipped-env-token" };
626
+ if (!account.baseUrl || !account.workspace || !token) return { status: "skipped-unconfigured" };
627
+ const client = createClickClackClient({
628
+ baseUrl: account.baseUrl,
629
+ token
630
+ });
631
+ const me = await client.me();
632
+ const workspaceId = await resolveWorkspaceId(client, account.workspace);
633
+ const workspace = (await client.workspaces()).find((candidate) => candidate.id === workspaceId);
634
+ if (!workspace) throw new Error(`ClickClack workspace not found: ${account.workspace}`);
635
+ return {
636
+ status: "connected",
637
+ handle: me.handle,
638
+ workspaceName: workspace.name
639
+ };
640
+ } catch (error) {
641
+ if (isHttpStatus(error, 401)) return { status: "invalid-token" };
642
+ if (isWorkspaceNotFound(error)) return {
643
+ status: "workspace-not-found",
644
+ workspace: workspaceInput
645
+ };
646
+ return {
647
+ status: "failed",
648
+ error: formatErrorMessage(error)
649
+ };
650
+ }
651
+ }
652
+ function isGatewayNotRunningError(error) {
653
+ 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;
654
+ const message = formatErrorMessage(error).toLowerCase();
655
+ return message.includes("econnrefused") || message.includes("connection refused");
656
+ }
657
+ async function probeClickClackGatewayStatus() {
658
+ try {
659
+ const { callGatewayFromCli } = await import("openclaw/plugin-sdk/gateway-runtime");
660
+ await callGatewayFromCli("health", {
661
+ timeout: "1000",
662
+ json: true
663
+ }, void 0, {
664
+ expectFinal: false,
665
+ progress: false
666
+ });
667
+ return "running";
668
+ } catch (error) {
669
+ return isGatewayNotRunningError(error) ? "not-running" : "unavailable";
670
+ }
671
+ }
672
+ function formatClickClackConnectionLog(result) {
673
+ switch (result.status) {
674
+ case "connected": return `Connected as @${result.handle} — workspace ${result.workspaceName} resolved.`;
675
+ case "invalid-token": return "ClickClack rejected the bot token (401). Copy a current token and rerun setup.";
676
+ case "workspace-not-found": return `Workspace "${result.workspace}" was not found. Check the id, slug, or name, list available workspaces, and rerun setup.`;
677
+ case "failed": return `Connection check failed: ${result.error}. Setup was saved; fix the connection and rerun setup.`;
678
+ case "skipped-env-token": return "Token comes from CLICKCLACK_BOT_TOKEN; verification skipped.";
679
+ case "skipped-unconfigured": return;
680
+ }
681
+ }
682
+ function formatClickClackGatewayLog(status) {
683
+ switch (status) {
684
+ case "running": return GATEWAY_RUNNING_MESSAGE;
685
+ case "not-running": return GATEWAY_NOT_RUNNING_MESSAGE;
686
+ case "unavailable": return GATEWAY_UNKNOWN_MESSAGE;
687
+ }
688
+ return GATEWAY_UNKNOWN_MESSAGE;
689
+ }
690
+ async function verifyClickClackAccountAfterSetup(params) {
691
+ try {
692
+ const message = formatClickClackConnectionLog(await checkClickClackSetupConnection({
693
+ cfg: params.cfg,
694
+ accountId: params.accountId
695
+ }));
696
+ if (message) params.runtime.log(message);
697
+ } catch (error) {
698
+ params.runtime.log(`Connection check failed: ${formatErrorMessage(error)}. Setup was saved; fix the connection and rerun setup.`);
699
+ }
700
+ try {
701
+ const status = await probeClickClackGatewayStatus();
702
+ params.runtime.log(formatClickClackGatewayLog(status));
703
+ } catch {
704
+ params.runtime.log(GATEWAY_UNKNOWN_MESSAGE);
705
+ }
706
+ }
707
+ //#endregion
708
+ //#region extensions/clickclack/src/setup-surface.ts
709
+ const t = createSetupTranslator();
710
+ const channel = "clickclack";
711
+ function hasConfiguredClickClackCredential(account) {
712
+ return hasConfiguredSecretInput(account.config.token) || Boolean(account.config.tokenFile?.trim());
713
+ }
714
+ function isClickClackSetupConfigured(account) {
715
+ return Boolean(account.baseUrl && account.workspace && (account.token || hasConfiguredClickClackCredential(account)));
716
+ }
717
+ const clickClackSetupWizard = {
718
+ channel,
719
+ status: createStandardChannelSetupStatus({
720
+ channelLabel: "ClickClack",
721
+ configuredLabel: t("wizard.channels.statusConfigured"),
722
+ unconfiguredLabel: t("wizard.channels.statusNeedsSetup"),
723
+ configuredHint: t("wizard.channels.statusSelfHostedChat"),
724
+ unconfiguredHint: t("wizard.channels.statusNeedsSetup"),
725
+ configuredScore: 2,
726
+ unconfiguredScore: 1,
727
+ resolveConfigured: ({ cfg, accountId }) => (accountId ? [accountId] : listClickClackAccountIds(cfg)).some((resolvedAccountId) => isClickClackSetupConfigured(resolveClickClackAccount({
728
+ cfg,
729
+ accountId: resolvedAccountId
730
+ })))
731
+ }),
732
+ introNote: {
733
+ title: t("wizard.clickclack.botTokenTitle"),
734
+ lines: [t("wizard.clickclack.helpCreateToken"), t("wizard.channels.docs", { link: formatDocsLink("/channels/clickclack", "clickclack") })],
735
+ shouldShow: ({ cfg, accountId }) => !isClickClackSetupConfigured(resolveClickClackAccount({
736
+ cfg,
737
+ accountId
738
+ }))
739
+ },
740
+ credentials: [defineTokenCredential({
741
+ inputKey: "token",
742
+ configKey: "token",
743
+ configuredFields: ["token", "tokenFile"],
744
+ providerHint: channel,
745
+ credentialLabel: t("wizard.clickclack.botToken"),
746
+ preferredEnvVar: "CLICKCLACK_BOT_TOKEN",
747
+ envPrompt: t("wizard.clickclack.envPrompt"),
748
+ keepPrompt: t("wizard.clickclack.botTokenKeep"),
749
+ inputPrompt: t("wizard.clickclack.botTokenInput"),
750
+ allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1,
751
+ resolveAccount: ({ cfg, accountId }) => resolveClickClackAccount({
752
+ cfg,
753
+ accountId
754
+ }),
755
+ hasConfiguredValue: hasConfiguredClickClackCredential,
756
+ resolvedValue: (account) => account.token || void 0,
757
+ envValue: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID$1 ? process.env.CLICKCLACK_BOT_TOKEN?.trim() || void 0 : void 0,
758
+ patchAccount: ({ cfg, accountId, mode, patch }) => mode === "env" ? applyClickClackCredentialConfig({
759
+ cfg,
760
+ accountId,
761
+ useEnv: true
762
+ }) : applyClickClackCredentialConfig({
763
+ cfg,
764
+ accountId,
765
+ ...patch
766
+ }),
767
+ useEnv: { clearFields: ["token", "tokenFile"] },
768
+ set: { clearFields: ["tokenFile"] }
769
+ })],
770
+ textInputs: [baseUrlTextInput({
771
+ inputKey: "baseUrl",
772
+ configKey: "baseUrl",
773
+ message: t("wizard.clickclack.baseUrlPrompt"),
774
+ resolveAccount: ({ cfg, accountId }) => resolveClickClackAccount({
775
+ cfg,
776
+ accountId
777
+ }),
778
+ currentValue: (account) => account.baseUrl || void 0,
779
+ includeInitialValue: true,
780
+ validate: (value) => normalizeClickClackBaseUrl(value) ? void 0 : "ClickClack server URL must be a valid http(s) URL.",
781
+ normalize: (value) => normalizeClickClackBaseUrl(value) ?? value.trim(),
782
+ patchAccount: ({ cfg, accountId, patch }) => applyClickClackSetupConfigPatch({
783
+ cfg,
784
+ accountId,
785
+ patch
786
+ })
787
+ }), {
788
+ inputKey: "workspace",
789
+ message: t("wizard.clickclack.workspacePrompt"),
790
+ helpTitle: t("wizard.clickclack.workspacePrompt"),
791
+ helpLines: [t("wizard.clickclack.workspaceHelp")],
792
+ currentValue: ({ cfg, accountId }) => resolveClickClackAccount({
793
+ cfg,
794
+ accountId
795
+ }).workspace || void 0,
796
+ initialValue: ({ cfg, accountId }) => resolveClickClackAccount({
797
+ cfg,
798
+ accountId
799
+ }).workspace || void 0,
800
+ validate: ({ value }) => value.trim() ? void 0 : "Required",
801
+ normalizeValue: ({ value }) => value.trim(),
802
+ applySet: async ({ cfg, accountId, value }) => applyClickClackSetupConfigPatch({
803
+ cfg,
804
+ accountId,
805
+ patch: { workspace: value }
806
+ })
807
+ }],
808
+ finalize: async ({ cfg, accountId, credentialValues, prompter }) => {
809
+ const result = await checkClickClackSetupConnection({
810
+ cfg,
811
+ accountId,
812
+ token: credentialValues.token
813
+ });
814
+ if (result.status === "connected") {
815
+ await prompter.note(t("wizard.clickclack.connected", {
816
+ handle: result.handle,
817
+ workspace: result.workspaceName
818
+ }), t("wizard.clickclack.connectionTitle"));
819
+ return;
820
+ }
821
+ if (result.status === "skipped-env-token" || result.status === "skipped-unconfigured") return;
822
+ 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 });
823
+ await prompter.note(message, t("wizard.clickclack.validationWarningTitle"));
824
+ },
825
+ disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
826
+ };
827
+ //#endregion
828
+ 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 };