@openclaw/clickclack 2026.7.2-beta.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.
@@ -1,158 +1,17 @@
1
- import { createAccountListHelpers, hasConfiguredAccountValue } from "openclaw/plugin-sdk/account-helpers";
2
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
- import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
4
- import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
5
- import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
6
- import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
7
- import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { a as createClickClackClient, c as CLICKCLACK_CHANNEL_ID, d as DEFAULT_ACCOUNT_ID, i as resolveWorkspaceId, l as clickClackConfigAdapter, m as resolveClickClackAccount, n as clickClackSetupAdapter, o as normalizeClickClackCorrelationId, r as resolveChannelId, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-B346XaAu.js";
2
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
8
3
  import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
9
4
  import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
10
- import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
11
5
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
12
- import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
13
- import { z } from "zod";
14
6
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
+ import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
15
8
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
16
9
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
17
- import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
18
- import { WebSocket } from "ws";
10
+ import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
19
11
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
20
12
  import { createHash } from "node:crypto";
21
13
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
22
14
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
23
- //#region extensions/clickclack/src/accounts.ts
24
- /**
25
- * Resolves ClickClack account configuration from root channel config, named
26
- * account overrides, and secret-provider references.
27
- */
28
- const DEFAULT_RECONNECT_MS = 1500;
29
- const MIN_RECONNECT_MS = 100;
30
- const MAX_RECONNECT_MS = 6e4;
31
- const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
32
- normalizeAccountId,
33
- hasImplicitDefaultAccount: (cfg) => {
34
- const channel = cfg.channels?.clickclack;
35
- return Boolean(channel?.baseUrl?.trim() && hasConfiguredAccountValue(channel.token) && channel.workspace?.trim());
36
- }
37
- });
38
- function resolveMergedClickClackAccountConfig(cfg, accountId) {
39
- return resolveMergedAccountConfig({
40
- channelConfig: cfg.channels?.clickclack,
41
- accounts: cfg.channels?.clickclack?.accounts,
42
- accountId,
43
- omitKeys: ["defaultAccount"],
44
- normalizeAccountId
45
- });
46
- }
47
- function resolveClickClackToken(params) {
48
- const resolved = resolveSecretInputString({
49
- value: params.value,
50
- path: params.accountId === DEFAULT_ACCOUNT_ID ? "channels.clickclack.token" : `channels.clickclack.accounts.${params.accountId}.token`,
51
- defaults: params.cfg.secrets?.defaults,
52
- mode: "inspect"
53
- });
54
- if (resolved.status !== "available") {
55
- if (resolved.status === "configured_unavailable" && resolved.ref.source === "env") {
56
- const providerConfig = params.cfg.secrets?.providers?.[resolved.ref.provider];
57
- if (providerConfig) {
58
- if (providerConfig.source !== "env") throw new Error(`Secret provider "${resolved.ref.provider}" has source "${providerConfig.source}" but ref requests "env".`);
59
- 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.`);
60
- } 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}).`);
61
- return normalizeSecretInputString((params.env ?? process.env)[resolved.ref.id]) ?? "";
62
- }
63
- return "";
64
- }
65
- return normalizeResolvedSecretInputString({
66
- value: resolved.value,
67
- path: "channels.clickclack.token"
68
- }) ?? "";
69
- }
70
- /**
71
- * Builds the normalized account snapshot used by gateway, outbound delivery,
72
- * status reporting, and channel routing.
73
- */
74
- function resolveClickClackAccount(params) {
75
- const accountId = normalizeAccountId(params.accountId);
76
- const merged = resolveMergedClickClackAccountConfig(params.cfg, accountId);
77
- const enabled = params.cfg.channels?.clickclack?.enabled !== false && merged.enabled !== false;
78
- const baseUrl = merged.baseUrl?.trim().replace(/\/$/, "") ?? "";
79
- const token = resolveClickClackToken({
80
- cfg: params.cfg,
81
- value: merged.token,
82
- accountId,
83
- env: params.env
84
- });
85
- const workspace = merged.workspace?.trim() ?? "";
86
- return {
87
- accountId,
88
- enabled,
89
- configured: Boolean(baseUrl && token && workspace),
90
- name: normalizeOptionalString(merged.name),
91
- baseUrl,
92
- token,
93
- workspace,
94
- botUserId: normalizeOptionalString(merged.botUserId),
95
- agentId: normalizeOptionalString(merged.agentId),
96
- replyMode: merged.replyMode === "model" ? "model" : "agent",
97
- model: normalizeOptionalString(merged.model),
98
- systemPrompt: normalizeOptionalString(merged.systemPrompt),
99
- timeoutSeconds: merged.timeoutSeconds,
100
- toolsAllow: merged.toolsAllow,
101
- defaultTo: merged.defaultTo?.trim() || "channel:general",
102
- allowFrom: merged.allowFrom ?? ["*"],
103
- reconnectMs: resolveIntegerOption(merged.reconnectMs, DEFAULT_RECONNECT_MS, {
104
- min: MIN_RECONNECT_MS,
105
- max: MAX_RECONNECT_MS
106
- }),
107
- agentActivity: merged.agentActivity === true,
108
- config: {
109
- ...merged,
110
- allowFrom: merged.allowFrom ?? ["*"]
111
- }
112
- };
113
- }
114
- /**
115
- * Returns all enabled accounts, including the implicit default account when
116
- * legacy top-level ClickClack config is present.
117
- */
118
- function listEnabledClickClackAccounts(cfg) {
119
- return listClickClackAccountIds(cfg).map((accountId) => resolveClickClackAccount({
120
- cfg,
121
- accountId
122
- })).filter((account) => account.enabled);
123
- }
124
- //#endregion
125
- //#region extensions/clickclack/src/config-schema.ts
126
- /**
127
- * Zod-backed config schema for ClickClack channel accounts.
128
- */
129
- const ClickClackAccountConfigSchema = z.object({
130
- name: z.string().optional(),
131
- enabled: z.boolean().optional(),
132
- baseUrl: z.string().url().optional(),
133
- token: buildSecretInputSchema().optional(),
134
- workspace: z.string().optional(),
135
- botUserId: z.string().optional(),
136
- agentId: z.string().optional(),
137
- replyMode: z.enum(["agent", "model"]).optional(),
138
- model: z.string().optional(),
139
- systemPrompt: z.string().optional(),
140
- timeoutSeconds: z.number().int().min(1).max(3600).optional(),
141
- toolsAllow: z.array(z.string()).optional(),
142
- defaultTo: z.string().optional(),
143
- allowFrom: z.array(z.string()).optional(),
144
- reconnectMs: z.number().int().min(100).max(6e4).optional(),
145
- agentActivity: z.boolean().optional()
146
- }).strict();
147
- /**
148
- * Config schema exported to core so `openclaw doctor` and config validation
149
- * understand both default and named ClickClack accounts.
150
- */
151
- const clickClackConfigSchema = buildChannelConfigSchema(ClickClackAccountConfigSchema.extend({
152
- accounts: z.record(z.string(), ClickClackAccountConfigSchema.partial()).optional(),
153
- defaultAccount: z.string().optional()
154
- }).strict());
155
- //#endregion
156
15
  //#region extensions/clickclack/src/runtime.ts
157
16
  /**
158
17
  * Runtime store for host-provided OpenClaw services used by the ClickClack
@@ -215,223 +74,62 @@ async function resolveClickClackInboundAccess(params) {
215
74
  };
216
75
  }
217
76
  //#endregion
218
- //#region extensions/clickclack/src/http-client.ts
219
- /**
220
- * Thin ClickClack REST/websocket client used by gateway, resolver, and outbound
221
- * delivery code.
222
- */
223
- /**
224
- * Serializes optional provenance into the wire fields. Unknown JSON fields
225
- * are ignored by servers without the provenance columns, so these are safe
226
- * to send unconditionally when present.
227
- */
228
- function provenanceFields(provenance) {
229
- const fields = {};
230
- if (provenance?.model?.trim()) fields.author_model = provenance.model.trim();
231
- if (provenance?.thinking?.trim()) fields.author_thinking = provenance.thinking.trim();
232
- if (provenance?.runtime?.trim()) fields.author_runtime = provenance.runtime.trim();
233
- return fields;
77
+ //#region extensions/clickclack/src/command-menu.ts
78
+ const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
79
+ const CLICKCLACK_MAX_COMMANDS = 100;
80
+ const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
81
+ const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
82
+ function truncateCodePoints(value, maxLength) {
83
+ return Array.from(value).slice(0, maxLength).join("");
234
84
  }
235
- const CLICKCLACK_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
236
- const CLICKCLACK_CORRELATION_ID_MAX_LENGTH = 128;
237
- const CLICKCLACK_CORRELATION_ID_PATTERN = /^[A-Za-z0-9._:-]+$/u;
238
- const CLICKCLACK_CORRELATION_ID_HEADER = "X-Correlation-ID";
239
- const CLICKCLACK_INBOUND_JSON_LIMIT_BYTES = 16 * 1024 * 1024;
240
- var ClickClackHttpError = class extends Error {
241
- constructor(status, detail, headers) {
242
- super(`ClickClack ${status}: ${detail}`);
243
- this.status = status;
244
- this.headers = headers;
245
- }
246
- };
247
- /** Accepts the same bounded request-correlation shape as the ClickClack API. */
248
- function normalizeClickClackCorrelationId(value) {
249
- if (typeof value !== "string") return;
250
- const normalized = value.trim();
251
- if (!normalized || normalized.length > CLICKCLACK_CORRELATION_ID_MAX_LENGTH || !CLICKCLACK_CORRELATION_ID_PATTERN.test(normalized)) return;
252
- return normalized;
85
+ function commandArgsHint(spec) {
86
+ if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
87
+ return spec.acceptsArgs ? "[args]" : "";
253
88
  }
254
- /**
255
- * Creates a typed client for the ClickClack API using bearer-token auth.
256
- */
257
- function createClickClackClient(options) {
258
- const baseUrl = options.baseUrl.replace(/\/$/, "");
259
- const fetcher = options.fetch ?? fetch;
260
- const correlationId = normalizeClickClackCorrelationId(options.correlationId);
261
- const headers = {
262
- Authorization: `Bearer ${options.token}`,
263
- Accept: "application/json"
264
- };
265
- async function request(path, init = {}) {
266
- const requestHeaders = new Headers(init.headers);
267
- for (const [key, value] of Object.entries(headers)) requestHeaders.set(key, value);
268
- if (correlationId) requestHeaders.set(CLICKCLACK_CORRELATION_ID_HEADER, correlationId);
269
- if (init.body && !(init.body instanceof FormData)) requestHeaders.set("Content-Type", "application/json");
270
- const response = await fetcher(`${baseUrl}${path}`, {
271
- ...init,
272
- headers: requestHeaders
273
- });
274
- if (!response.ok) {
275
- const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
276
- throw new ClickClackHttpError(response.status, detail, new Headers(response.headers));
89
+ function errorStatus(error) {
90
+ if (!error || typeof error !== "object" || !("status" in error)) return;
91
+ const status = error.status;
92
+ return typeof status === "number" ? status : void 0;
93
+ }
94
+ function mapNativeCommandSpecsToClickClackMenu(specs, log) {
95
+ const commands = [];
96
+ const seen = /* @__PURE__ */ new Set();
97
+ const invalidNames = /* @__PURE__ */ new Set();
98
+ for (const spec of specs) {
99
+ if (spec.isAlias) continue;
100
+ const command = spec.name.trim().toLowerCase();
101
+ if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
102
+ invalidNames.add(spec.name.trim() || "<empty>");
103
+ continue;
277
104
  }
278
- return await readProviderJsonResponse(response, "ClickClack response", { maxBytes: CLICKCLACK_INBOUND_JSON_LIMIT_BYTES });
279
- }
280
- async function fetchEventPage(workspaceId, pageOptions = {}) {
281
- const query = new URLSearchParams({ workspace_id: workspaceId });
282
- if (pageOptions.afterCursor) query.set("after_cursor", pageOptions.afterCursor);
283
- if (pageOptions.limit !== void 0) query.set("limit", String(pageOptions.limit));
284
- if (pageOptions.includeTail) query.set("include_tail", "true");
285
- const data = await request(`/api/realtime/events?${query.toString()}`);
286
- return {
287
- events: data.events,
288
- ...typeof data.tail_cursor === "string" ? { tailCursor: data.tail_cursor } : {}
289
- };
105
+ if (seen.has(command)) continue;
106
+ seen.add(command);
107
+ const description = spec.description.trim() || command;
108
+ commands.push({
109
+ command,
110
+ description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
111
+ args_hint: commandArgsHint(spec)
112
+ });
290
113
  }
291
- return {
292
- me: async () => {
293
- return (await request("/api/me")).user;
294
- },
295
- workspaces: async () => {
296
- return (await request("/api/workspaces")).workspaces;
297
- },
298
- channels: async (workspaceId) => {
299
- return (await request(`/api/workspaces/${encodeURIComponent(workspaceId)}/channels`)).channels;
300
- },
301
- channelMessages: async (channelId, afterSeq, limit = 20) => {
302
- return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
303
- },
304
- directMessages: async (conversationId, afterSeq, limit = 20) => {
305
- return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
306
- },
307
- thread: async (messageId) => await request(`/api/messages/${encodeURIComponent(messageId)}/thread`),
308
- message: async (messageId) => {
309
- return (await request(`/api/messages/${encodeURIComponent(messageId)}`)).message;
310
- },
311
- findMessageByNonce: async (params) => {
312
- const query = new URLSearchParams({
313
- workspace_id: params.workspaceId,
314
- nonce: params.nonce
315
- });
316
- try {
317
- return (await request(`/api/messages/by-nonce?${query.toString()}`)).message;
318
- } catch (error) {
319
- if (error instanceof ClickClackHttpError && error.status === 404) {
320
- if (error.headers.get("X-ClickClack-Message-Nonce") === "supported") return;
321
- throw new Error("ClickClack server does not support durable message nonce lookup", { cause: error });
322
- }
323
- throw error;
324
- }
325
- },
326
- createChannelMessage: async (channelId, body, opts) => {
327
- return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages`, {
328
- method: "POST",
329
- body: JSON.stringify({
330
- body,
331
- ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
332
- ...opts?.nonce ? { nonce: opts.nonce } : {},
333
- ...provenanceFields(opts?.provenance)
334
- })
335
- })).message;
336
- },
337
- createThreadReply: async (messageId, body, opts) => {
338
- return (await request(`/api/messages/${encodeURIComponent(messageId)}/thread/replies`, {
339
- method: "POST",
340
- body: JSON.stringify({
341
- body,
342
- ...opts?.nonce ? { nonce: opts.nonce } : {},
343
- ...provenanceFields(opts?.provenance)
344
- })
345
- })).message;
346
- },
347
- createDirectConversation: async (workspaceId, memberIds) => {
348
- return (await request("/api/dms", {
349
- method: "POST",
350
- body: JSON.stringify({
351
- workspace_id: workspaceId,
352
- member_ids: memberIds
353
- })
354
- })).conversation;
355
- },
356
- createUpload: async (params) => {
357
- const form = new FormData();
358
- const bytes = new Uint8Array(params.buffer);
359
- form.append("file", new Blob([bytes], { type: params.contentType }), params.filename);
360
- const query = new URLSearchParams({ workspace_id: params.workspaceId });
361
- if (params.nonce) query.set("nonce", params.nonce);
362
- return (await request(`/api/uploads?${query.toString()}`, {
363
- method: "POST",
364
- body: form
365
- })).upload;
366
- },
367
- findUploadByNonce: async (params) => {
368
- const query = new URLSearchParams({
369
- workspace_id: params.workspaceId,
370
- nonce: params.nonce
371
- });
372
- try {
373
- return (await request(`/api/uploads/by-nonce?${query.toString()}`)).upload;
374
- } catch (error) {
375
- if (error instanceof ClickClackHttpError && error.status === 404) {
376
- if (error.headers.get("X-ClickClack-Upload-Nonce") === "supported") return;
377
- throw new Error("ClickClack server does not support durable upload nonce lookup", { cause: error });
378
- }
379
- throw error;
380
- }
381
- },
382
- attachUpload: async (messageId, uploadId) => {
383
- await request(`/api/messages/${encodeURIComponent(messageId)}/attachments`, {
384
- method: "POST",
385
- body: JSON.stringify({ upload_id: uploadId })
386
- });
387
- },
388
- /**
389
- * POSTs a durable agent activity row (agent_commentary / agent_tool)
390
- * through the normal message create path. Requires a bot token carrying
391
- * the agent_activity:write scope on the ClickClack side.
392
- */
393
- createActivityMessage: async (params) => {
394
- if (!params.channelId && !params.conversationId) throw new Error("createActivityMessage requires a channelId or conversationId");
395
- return (await request(params.channelId ? `/api/channels/${encodeURIComponent(params.channelId)}/messages` : `/api/dms/${encodeURIComponent(params.conversationId ?? "")}/messages`, {
396
- method: "POST",
397
- body: JSON.stringify({
398
- body: params.body,
399
- kind: params.kind,
400
- turn_id: params.turnId,
401
- ...provenanceFields(params.provenance)
402
- })
403
- })).message;
404
- },
405
- /** PATCHes the body of an existing message (activity row coalescing). */
406
- updateMessageBody: async (messageId, body) => {
407
- return (await request(`/api/messages/${encodeURIComponent(messageId)}`, {
408
- method: "PATCH",
409
- body: JSON.stringify({ body })
410
- })).message;
411
- },
412
- createDirectMessage: async (conversationId, body, opts) => {
413
- return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages`, {
414
- method: "POST",
415
- body: JSON.stringify({
416
- body,
417
- ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
418
- ...opts?.nonce ? { nonce: opts.nonce } : {}
419
- })
420
- })).message;
421
- },
422
- events: async (workspaceId, afterCursor) => (await fetchEventPage(workspaceId, { afterCursor })).events,
423
- eventPage: fetchEventPage,
424
- websocket: (workspaceId, afterCursor) => {
425
- const url = new URL(`${baseUrl}/api/realtime/ws`);
426
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
427
- url.searchParams.set("workspace_id", workspaceId);
428
- if (afterCursor) url.searchParams.set("after_cursor", afterCursor);
429
- return new WebSocket(url, {
430
- headers: { Authorization: `Bearer ${options.token}` },
431
- maxPayload: CLICKCLACK_INBOUND_JSON_LIMIT_BYTES
432
- });
114
+ if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
115
+ return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
116
+ }
117
+ async function syncClickClackCommandMenu(params) {
118
+ try {
119
+ const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
120
+ await params.client.setBotCommands(commands);
121
+ } catch (error) {
122
+ const status = errorStatus(error);
123
+ if (status === 403) {
124
+ params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
125
+ return;
433
126
  }
434
- };
127
+ if (status === 404) {
128
+ params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
129
+ return;
130
+ }
131
+ params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
132
+ }
435
133
  }
436
134
  //#endregion
437
135
  //#region extensions/clickclack/src/activity.ts
@@ -629,27 +327,6 @@ function createClickClackActivityPublisher(params) {
629
327
  };
630
328
  }
631
329
  //#endregion
632
- //#region extensions/clickclack/src/resolve.ts
633
- /**
634
- * Resolves a workspace slug/name/id from config to a ClickClack workspace id.
635
- */
636
- async function resolveWorkspaceId(client, workspace) {
637
- if (workspace.startsWith("wsp_")) return workspace;
638
- const found = (await client.workspaces()).find((candidate) => candidate.id === workspace || candidate.slug === workspace || candidate.name === workspace);
639
- if (!found) throw new Error(`ClickClack workspace not found: ${workspace}`);
640
- return found.id;
641
- }
642
- /**
643
- * Resolves a channel name/id from config or target input to a ClickClack
644
- * channel id.
645
- */
646
- async function resolveChannelId(client, workspaceId, channel) {
647
- if (channel.startsWith("chn_")) return channel;
648
- const found = (await client.channels(workspaceId)).find((candidate) => candidate.id === channel || candidate.name === channel);
649
- if (!found) throw new Error(`ClickClack channel not found: ${channel}`);
650
- return found.id;
651
- }
652
- //#endregion
653
330
  //#region extensions/clickclack/src/target.ts
654
331
  /**
655
332
  * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
@@ -1322,6 +999,18 @@ async function startClickClackGatewayAccount(ctx) {
1322
999
  workspace: workspaceId,
1323
1000
  botUserId: configuredAccount.botUserId ?? me.id
1324
1001
  };
1002
+ const processIncomingEvent = (event) => processEvent({
1003
+ account,
1004
+ config: ctx.cfg,
1005
+ client,
1006
+ event,
1007
+ botUserId: account.botUserId
1008
+ });
1009
+ if (account.commandMenu) await syncClickClackCommandMenu({
1010
+ cfg: ctx.cfg,
1011
+ client,
1012
+ log: ctx.log
1013
+ });
1325
1014
  ctx.setStatus({
1326
1015
  accountId: account.accountId,
1327
1016
  running: true,
@@ -1343,20 +1032,15 @@ async function startClickClackGatewayAccount(ctx) {
1343
1032
  workspaceId,
1344
1033
  afterCursor,
1345
1034
  abortSignal: ctx.abortSignal,
1346
- onEvent: async (event) => {
1347
- await processEvent({
1348
- account,
1349
- config: ctx.cfg,
1350
- client,
1351
- event,
1352
- botUserId: account.botUserId
1353
- });
1354
- }
1035
+ onEvent: processIncomingEvent
1355
1036
  });
1356
1037
  if (ctx.abortSignal.aborted) break;
1357
1038
  const socket = client.websocket(workspaceId, afterCursor);
1358
- await new Promise((resolve, reject) => {
1039
+ await new Promise((resolve) => {
1359
1040
  let settled = false;
1041
+ let closing = false;
1042
+ let loggedMessageFailure = false;
1043
+ let messageQueue = Promise.resolve();
1360
1044
  let removeAbortListener;
1361
1045
  const finishSocketCycle = () => {
1362
1046
  if (settled) return;
@@ -1365,6 +1049,20 @@ async function startClickClackGatewayAccount(ctx) {
1365
1049
  removeAbortListener = void 0;
1366
1050
  resolve();
1367
1051
  };
1052
+ const finishAfterQueuedMessages = () => {
1053
+ messageQueue.then(() => finishSocketCycle(), () => finishSocketCycle());
1054
+ };
1055
+ const reconnectAfterMessageFailure = (error) => {
1056
+ if (settled || ctx.abortSignal.aborted) return;
1057
+ if (!loggedMessageFailure) {
1058
+ loggedMessageFailure = true;
1059
+ ctx.log?.warn?.(`[${account.accountId}] ClickClack event processing failed; reconnecting: ${error instanceof Error ? error.message : formatErrorMessage(error)}`);
1060
+ }
1061
+ if (!closing) {
1062
+ closing = true;
1063
+ socket.close();
1064
+ }
1065
+ };
1368
1066
  const abort = () => {
1369
1067
  socket.close();
1370
1068
  finishSocketCycle();
@@ -1372,36 +1070,38 @@ async function startClickClackGatewayAccount(ctx) {
1372
1070
  ctx.abortSignal.addEventListener("abort", abort, { once: true });
1373
1071
  removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
1374
1072
  socket.on("message", (data) => {
1375
- (async () => {
1073
+ if (closing || settled) return;
1074
+ messageQueue = messageQueue.then(async () => {
1376
1075
  const event = parseSocketEvent(data);
1377
1076
  if (!event) {
1378
1077
  ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
1379
1078
  return;
1380
1079
  }
1080
+ await processIncomingEvent(event);
1381
1081
  afterCursor = event.cursor || afterCursor;
1382
- await processEvent({
1383
- account,
1384
- config: ctx.cfg,
1385
- client,
1386
- event,
1387
- botUserId: account.botUserId ?? ""
1388
- });
1389
- })().catch((e) => reject(e instanceof Error ? e : new Error(`ClickClack ws message failed: ${formatErrorMessage(e)}`, { cause: e })));
1082
+ });
1083
+ messageQueue.catch(reconnectAfterMessageFailure);
1084
+ });
1085
+ socket.on("close", () => {
1086
+ closing = true;
1087
+ finishAfterQueuedMessages();
1390
1088
  });
1391
- socket.on("close", finishSocketCycle);
1392
1089
  socket.on("error", (error) => {
1393
1090
  if (settled || ctx.abortSignal.aborted) {
1394
1091
  finishSocketCycle();
1395
1092
  return;
1396
1093
  }
1094
+ if (closing) return;
1397
1095
  ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
1398
- finishSocketCycle();
1096
+ closing = true;
1399
1097
  socket.close();
1400
1098
  });
1401
1099
  });
1402
- if (!ctx.abortSignal.aborted) await new Promise((resolve) => {
1403
- setTimeout(resolve, account.reconnectMs);
1404
- });
1100
+ if (!ctx.abortSignal.aborted) try {
1101
+ await sleepWithAbort(account.reconnectMs, ctx.abortSignal);
1102
+ } catch (error) {
1103
+ if (!ctx.abortSignal.aborted) throw error;
1104
+ }
1405
1105
  }
1406
1106
  } finally {
1407
1107
  ctx.setStatus({
@@ -1416,8 +1116,7 @@ async function startClickClackGatewayAccount(ctx) {
1416
1116
  * ClickClack channel plugin definition: target parsing, account config, status,
1417
1117
  * gateway startup, and outbound delivery wiring.
1418
1118
  */
1419
- const CHANNEL_ID = "clickclack";
1420
- const meta = { ...getChatChannelMeta(CHANNEL_ID) };
1119
+ const CHANNEL_ID = CLICKCLACK_CHANNEL_ID;
1421
1120
  const clickClackMessageAdapter = defineChannelMessageAdapter({
1422
1121
  id: CHANNEL_ID,
1423
1122
  durableFinal: {
@@ -1502,7 +1201,7 @@ const clickClackMessageAdapter = defineChannelMessageAdapter({
1502
1201
  const clickClackPlugin = createChatChannelPlugin({
1503
1202
  base: {
1504
1203
  id: CHANNEL_ID,
1505
- meta,
1204
+ meta: clickClackMeta,
1506
1205
  capabilities: {
1507
1206
  chatTypes: ["direct", "group"],
1508
1207
  threads: true,
@@ -1510,22 +1209,12 @@ const clickClackPlugin = createChatChannelPlugin({
1510
1209
  },
1511
1210
  reload: { configPrefixes: ["channels.clickclack"] },
1512
1211
  configSchema: clickClackConfigSchema,
1513
- config: {
1514
- listAccountIds: (cfg) => listClickClackAccountIds(cfg),
1515
- resolveAccount: (cfg, accountId) => resolveClickClackAccount({
1516
- cfg,
1517
- accountId
1518
- }),
1519
- defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg),
1520
- isConfigured: (account) => account.configured,
1521
- resolveAllowFrom: ({ cfg, accountId }) => resolveClickClackAccount({
1522
- cfg,
1523
- accountId
1524
- }).allowFrom,
1525
- resolveDefaultTo: ({ cfg, accountId }) => resolveClickClackAccount({
1526
- cfg,
1527
- accountId
1528
- }).defaultTo
1212
+ config: clickClackConfigAdapter,
1213
+ setup: clickClackSetupAdapter,
1214
+ setupWizard: clickClackSetupWizard,
1215
+ secrets: {
1216
+ secretTargetRegistryEntries,
1217
+ collectRuntimeConfigAssignments
1529
1218
  },
1530
1219
  messaging: {
1531
1220
  targetPrefixes: ["clickclack", "cc"],
@@ -1627,4 +1316,4 @@ const clickClackPlugin = createChatChannelPlugin({
1627
1316
  }
1628
1317
  });
1629
1318
  //#endregion
1630
- export { getClickClackRuntime as a, DEFAULT_ACCOUNT_ID as c, resolveClickClackAccount as d, resolveDefaultClickClackAccountId as f, createClickClackClient as i, listClickClackAccountIds as l, buildClickClackTarget as n, setClickClackRuntime as o, parseClickClackTarget as r, clickClackConfigSchema as s, clickClackPlugin as t, listEnabledClickClackAccounts as u };
1319
+ export { setClickClackRuntime as a, getClickClackRuntime as i, buildClickClackTarget as n, parseClickClackTarget as r, clickClackPlugin as t };
@@ -1,2 +1,2 @@
1
- import { t as clickClackPlugin } from "./channel-Ds6d_3nx.js";
1
+ import { t as clickClackPlugin } from "./channel-BnREQIP8.js";
2
2
  export { clickClackPlugin };
@@ -1,3 +1,4 @@
1
- import { d as resolveClickClackAccount, i as createClickClackClient, o as setClickClackRuntime, r as parseClickClackTarget } from "./channel-Ds6d_3nx.js";
1
+ import { a as createClickClackClient, m as resolveClickClackAccount } from "./setup-surface-B346XaAu.js";
2
+ import { a as setClickClackRuntime, r as parseClickClackTarget } from "./channel-BnREQIP8.js";
2
3
  import "./api.js";
3
4
  export { createClickClackClient, parseClickClackTarget, resolveClickClackAccount, setClickClackRuntime };