@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.
@@ -1,158 +1,18 @@
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-CNjp76Xx.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";
11
+ import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
19
12
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
20
13
  import { createHash } from "node:crypto";
21
14
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
22
15
  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
16
  //#region extensions/clickclack/src/runtime.ts
157
17
  /**
158
18
  * Runtime store for host-provided OpenClaw services used by the ClickClack
@@ -215,223 +75,62 @@ async function resolveClickClackInboundAccess(params) {
215
75
  };
216
76
  }
217
77
  //#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;
78
+ //#region extensions/clickclack/src/command-menu.ts
79
+ const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
80
+ const CLICKCLACK_MAX_COMMANDS = 100;
81
+ const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
82
+ const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
83
+ function truncateCodePoints(value, maxLength) {
84
+ return Array.from(value).slice(0, maxLength).join("");
234
85
  }
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;
86
+ function commandArgsHint(spec) {
87
+ if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
88
+ return spec.acceptsArgs ? "[args]" : "";
253
89
  }
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));
90
+ function errorStatus(error) {
91
+ if (!error || typeof error !== "object" || !("status" in error)) return;
92
+ const status = error.status;
93
+ return typeof status === "number" ? status : void 0;
94
+ }
95
+ function mapNativeCommandSpecsToClickClackMenu(specs, log) {
96
+ const commands = [];
97
+ const seen = /* @__PURE__ */ new Set();
98
+ const invalidNames = /* @__PURE__ */ new Set();
99
+ for (const spec of specs) {
100
+ if (spec.isAlias) continue;
101
+ const command = spec.name.trim().toLowerCase();
102
+ if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
103
+ invalidNames.add(spec.name.trim() || "<empty>");
104
+ continue;
277
105
  }
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
- };
106
+ if (seen.has(command)) continue;
107
+ seen.add(command);
108
+ const description = spec.description.trim() || command;
109
+ commands.push({
110
+ command,
111
+ description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
112
+ args_hint: commandArgsHint(spec)
113
+ });
290
114
  }
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
- });
115
+ if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
116
+ return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
117
+ }
118
+ async function syncClickClackCommandMenu(params) {
119
+ try {
120
+ const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
121
+ await params.client.setBotCommands(commands);
122
+ } catch (error) {
123
+ const status = errorStatus(error);
124
+ if (status === 403) {
125
+ params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
126
+ return;
433
127
  }
434
- };
128
+ if (status === 404) {
129
+ params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
130
+ return;
131
+ }
132
+ params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
133
+ }
435
134
  }
436
135
  //#endregion
437
136
  //#region extensions/clickclack/src/activity.ts
@@ -629,27 +328,6 @@ function createClickClackActivityPublisher(params) {
629
328
  };
630
329
  }
631
330
  //#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
331
  //#region extensions/clickclack/src/target.ts
654
332
  /**
655
333
  * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
@@ -1063,6 +741,8 @@ async function handleClickClackInbound(params) {
1063
741
  message
1064
742
  });
1065
743
  if (!access.shouldDispatch) return;
744
+ const conversationId = message.channel_id || message.direct_conversation_id;
745
+ if (!conversationId) return;
1066
746
  const isDirect = Boolean(message.direct_conversation_id);
1067
747
  const target = buildClickClackTarget(isDirect ? {
1068
748
  chatType: "direct",
@@ -1108,46 +788,60 @@ async function handleClickClackInbound(params) {
1108
788
  }
1109
789
  });
1110
790
  const senderName = message.author?.display_name || message.author_id;
1111
- const previousTimestamp = runtime.channel.session.readSessionUpdatedAt({
1112
- storePath: runtime.channel.session.resolveStorePath(params.config.session?.store, { agentId: route.agentId }),
1113
- sessionKey: route.sessionKey
1114
- });
1115
- const body = runtime.channel.reply.formatAgentEnvelope({
791
+ const body = createChannelInboundEnvelopeBuilder({
792
+ cfg: params.config,
793
+ route
794
+ })({
1116
795
  channel: "ClickClack",
1117
796
  from: senderName,
1118
797
  timestamp: new Date(message.created_at),
1119
- previousTimestamp,
1120
- envelope: runtime.channel.reply.resolveEnvelopeFormatOptions(params.config),
1121
798
  body: message.body
1122
799
  });
1123
- const storePath = runtime.channel.session.resolveStorePath(params.config.session?.store, { agentId: route.agentId });
1124
- const ctxPayload = runtime.channel.reply.finalizeInboundContext({
1125
- Body: body,
1126
- BodyForAgent: message.body,
1127
- RawBody: message.body,
1128
- CommandBody: message.body,
1129
- From: target,
1130
- To: target,
1131
- SessionKey: route.sessionKey,
1132
- AccountId: route.accountId ?? params.account.accountId,
1133
- ChatType: isDirect ? "direct" : "group",
1134
- WasMentioned: isDirect ? void 0 : true,
1135
- ConversationLabel: isDirect ? senderName : message.channel_id,
1136
- GroupChannel: message.channel_id,
1137
- NativeChannelId: message.channel_id || message.direct_conversation_id,
1138
- MessageThreadId: message.parent_message_id ? message.thread_root_id : void 0,
1139
- ThreadParentId: message.parent_message_id ? message.thread_root_id : void 0,
1140
- SenderName: senderName,
1141
- SenderId: message.author_id,
1142
- Provider: CHANNEL_ID$1,
1143
- Surface: CHANNEL_ID$1,
1144
- MessageSid: message.id,
1145
- MessageSidFull: message.id,
1146
- ReplyToId: message.id,
1147
- Timestamp: message.created_at,
1148
- OriginatingChannel: CHANNEL_ID$1,
1149
- OriginatingTo: target,
1150
- CommandAuthorized: access.commandAuthorized
800
+ const ctxPayload = runtime.channel.inbound.buildContext({
801
+ channel: CHANNEL_ID$1,
802
+ accountId: route.accountId ?? params.account.accountId,
803
+ messageId: message.id,
804
+ messageIdFull: message.id,
805
+ timestamp: new Date(message.created_at).getTime(),
806
+ from: target,
807
+ sender: {
808
+ id: message.author_id,
809
+ name: senderName
810
+ },
811
+ conversation: {
812
+ kind: isDirect ? "direct" : "group",
813
+ id: conversationId,
814
+ label: isDirect ? senderName : message.channel_id,
815
+ threadId: message.parent_message_id ? message.thread_root_id : void 0,
816
+ nativeChannelId: conversationId
817
+ },
818
+ route: {
819
+ agentId: route.agentId,
820
+ dmScope: route.dmScope,
821
+ accountId: route.accountId,
822
+ routeSessionKey: route.sessionKey
823
+ },
824
+ reply: {
825
+ to: target,
826
+ originatingTo: target,
827
+ replyToId: message.id,
828
+ messageThreadId: message.parent_message_id ? message.thread_root_id : void 0,
829
+ threadParentId: message.parent_message_id ? message.thread_root_id : void 0
830
+ },
831
+ message: {
832
+ body,
833
+ bodyForAgent: message.body,
834
+ rawBody: message.body,
835
+ commandBody: message.body
836
+ },
837
+ access: {
838
+ commands: { authorized: access.commandAuthorized },
839
+ mentions: {
840
+ canDetectMention: !isDirect,
841
+ wasMentioned: !isDirect
842
+ }
843
+ },
844
+ extra: { GroupChannel: message.channel_id }
1151
845
  });
1152
846
  const runId = resolveClickClackAgentRunId(message.id);
1153
847
  const activityReplyOptions = activity ? {
@@ -1163,16 +857,16 @@ async function handleClickClackInbound(params) {
1163
857
  suppressDefaultToolProgressMessages: true,
1164
858
  allowProgressCallbacksWhenSourceDeliverySuppressed: true
1165
859
  } : void 0;
1166
- const dispatchPromise = runtime.channel.inbound.dispatchReply({
860
+ const dispatchPromise = runtime.channel.inbound.dispatch({
1167
861
  cfg: params.config,
1168
862
  channel: CHANNEL_ID$1,
1169
863
  accountId: params.account.accountId,
1170
- agentId: route.agentId,
1171
- routeSessionKey: route.sessionKey,
1172
- storePath,
864
+ route: {
865
+ agentId: route.agentId,
866
+ dmScope: route.dmScope,
867
+ sessionKey: route.sessionKey
868
+ },
1173
869
  ctxPayload,
1174
- recordInboundSession: runtime.channel.session.recordInboundSession,
1175
- dispatchReplyWithBufferedBlockDispatcher: runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
1176
870
  toolsAllow: params.account.toolsAllow,
1177
871
  replyOptions: runId || activityReplyOptions ? {
1178
872
  ...runId ? { runId } : {},
@@ -1322,6 +1016,18 @@ async function startClickClackGatewayAccount(ctx) {
1322
1016
  workspace: workspaceId,
1323
1017
  botUserId: configuredAccount.botUserId ?? me.id
1324
1018
  };
1019
+ const processIncomingEvent = (event) => processEvent({
1020
+ account,
1021
+ config: ctx.cfg,
1022
+ client,
1023
+ event,
1024
+ botUserId: account.botUserId
1025
+ });
1026
+ if (account.commandMenu) await syncClickClackCommandMenu({
1027
+ cfg: ctx.cfg,
1028
+ client,
1029
+ log: ctx.log
1030
+ });
1325
1031
  ctx.setStatus({
1326
1032
  accountId: account.accountId,
1327
1033
  running: true,
@@ -1343,20 +1049,15 @@ async function startClickClackGatewayAccount(ctx) {
1343
1049
  workspaceId,
1344
1050
  afterCursor,
1345
1051
  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
- }
1052
+ onEvent: processIncomingEvent
1355
1053
  });
1356
1054
  if (ctx.abortSignal.aborted) break;
1357
1055
  const socket = client.websocket(workspaceId, afterCursor);
1358
- await new Promise((resolve, reject) => {
1056
+ await new Promise((resolve) => {
1359
1057
  let settled = false;
1058
+ let closing = false;
1059
+ let loggedMessageFailure = false;
1060
+ let messageQueue = Promise.resolve();
1360
1061
  let removeAbortListener;
1361
1062
  const finishSocketCycle = () => {
1362
1063
  if (settled) return;
@@ -1365,6 +1066,20 @@ async function startClickClackGatewayAccount(ctx) {
1365
1066
  removeAbortListener = void 0;
1366
1067
  resolve();
1367
1068
  };
1069
+ const finishAfterQueuedMessages = () => {
1070
+ messageQueue.then(() => finishSocketCycle(), () => finishSocketCycle());
1071
+ };
1072
+ const reconnectAfterMessageFailure = (error) => {
1073
+ if (settled || ctx.abortSignal.aborted) return;
1074
+ if (!loggedMessageFailure) {
1075
+ loggedMessageFailure = true;
1076
+ ctx.log?.warn?.(`[${account.accountId}] ClickClack event processing failed; reconnecting: ${error instanceof Error ? error.message : formatErrorMessage(error)}`);
1077
+ }
1078
+ if (!closing) {
1079
+ closing = true;
1080
+ socket.close();
1081
+ }
1082
+ };
1368
1083
  const abort = () => {
1369
1084
  socket.close();
1370
1085
  finishSocketCycle();
@@ -1372,36 +1087,38 @@ async function startClickClackGatewayAccount(ctx) {
1372
1087
  ctx.abortSignal.addEventListener("abort", abort, { once: true });
1373
1088
  removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
1374
1089
  socket.on("message", (data) => {
1375
- (async () => {
1090
+ if (closing || settled) return;
1091
+ messageQueue = messageQueue.then(async () => {
1376
1092
  const event = parseSocketEvent(data);
1377
1093
  if (!event) {
1378
1094
  ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
1379
1095
  return;
1380
1096
  }
1097
+ await processIncomingEvent(event);
1381
1098
  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 })));
1099
+ });
1100
+ messageQueue.catch(reconnectAfterMessageFailure);
1101
+ });
1102
+ socket.on("close", () => {
1103
+ closing = true;
1104
+ finishAfterQueuedMessages();
1390
1105
  });
1391
- socket.on("close", finishSocketCycle);
1392
1106
  socket.on("error", (error) => {
1393
1107
  if (settled || ctx.abortSignal.aborted) {
1394
1108
  finishSocketCycle();
1395
1109
  return;
1396
1110
  }
1111
+ if (closing) return;
1397
1112
  ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
1398
- finishSocketCycle();
1113
+ closing = true;
1399
1114
  socket.close();
1400
1115
  });
1401
1116
  });
1402
- if (!ctx.abortSignal.aborted) await new Promise((resolve) => {
1403
- setTimeout(resolve, account.reconnectMs);
1404
- });
1117
+ if (!ctx.abortSignal.aborted) try {
1118
+ await sleepWithAbort(account.reconnectMs, ctx.abortSignal);
1119
+ } catch (error) {
1120
+ if (!ctx.abortSignal.aborted) throw error;
1121
+ }
1405
1122
  }
1406
1123
  } finally {
1407
1124
  ctx.setStatus({
@@ -1416,8 +1133,7 @@ async function startClickClackGatewayAccount(ctx) {
1416
1133
  * ClickClack channel plugin definition: target parsing, account config, status,
1417
1134
  * gateway startup, and outbound delivery wiring.
1418
1135
  */
1419
- const CHANNEL_ID = "clickclack";
1420
- const meta = { ...getChatChannelMeta(CHANNEL_ID) };
1136
+ const CHANNEL_ID = CLICKCLACK_CHANNEL_ID;
1421
1137
  const clickClackMessageAdapter = defineChannelMessageAdapter({
1422
1138
  id: CHANNEL_ID,
1423
1139
  durableFinal: {
@@ -1502,7 +1218,7 @@ const clickClackMessageAdapter = defineChannelMessageAdapter({
1502
1218
  const clickClackPlugin = createChatChannelPlugin({
1503
1219
  base: {
1504
1220
  id: CHANNEL_ID,
1505
- meta,
1221
+ meta: clickClackMeta,
1506
1222
  capabilities: {
1507
1223
  chatTypes: ["direct", "group"],
1508
1224
  threads: true,
@@ -1510,22 +1226,12 @@ const clickClackPlugin = createChatChannelPlugin({
1510
1226
  },
1511
1227
  reload: { configPrefixes: ["channels.clickclack"] },
1512
1228
  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
1229
+ config: clickClackConfigAdapter,
1230
+ setup: clickClackSetupAdapter,
1231
+ setupWizard: clickClackSetupWizard,
1232
+ secrets: {
1233
+ secretTargetRegistryEntries,
1234
+ collectRuntimeConfigAssignments
1529
1235
  },
1530
1236
  messaging: {
1531
1237
  targetPrefixes: ["clickclack", "cc"],
@@ -1627,4 +1333,4 @@ const clickClackPlugin = createChatChannelPlugin({
1627
1333
  }
1628
1334
  });
1629
1335
  //#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 };
1336
+ export { setClickClackRuntime as a, getClickClackRuntime as i, buildClickClackTarget as n, parseClickClackTarget as r, clickClackPlugin as t };