@openclaw/clickclack 2026.7.2-beta.7 → 2026.7.34

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,1172 @@
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";
8
+ import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
9
+ import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-outbound";
10
+ import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
11
+ 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
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
15
+ import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
16
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
17
+ import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
18
+ import { WebSocket } from "ws";
19
+ import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
20
+ import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
21
+ //#region extensions/clickclack/src/accounts.ts
22
+ /**
23
+ * Resolves ClickClack account configuration from root channel config, named
24
+ * account overrides, and secret-provider references.
25
+ */
26
+ const DEFAULT_RECONNECT_MS = 1500;
27
+ const MIN_RECONNECT_MS = 100;
28
+ const MAX_RECONNECT_MS = 6e4;
29
+ const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
30
+ normalizeAccountId,
31
+ hasImplicitDefaultAccount: (cfg) => {
32
+ const channel = cfg.channels?.clickclack;
33
+ return Boolean(channel?.baseUrl?.trim() && hasConfiguredAccountValue(channel.token) && channel.workspace?.trim());
34
+ }
35
+ });
36
+ function resolveMergedClickClackAccountConfig(cfg, accountId) {
37
+ return resolveMergedAccountConfig({
38
+ channelConfig: cfg.channels?.clickclack,
39
+ accounts: cfg.channels?.clickclack?.accounts,
40
+ accountId,
41
+ omitKeys: ["defaultAccount"],
42
+ normalizeAccountId
43
+ });
44
+ }
45
+ function resolveClickClackToken(params) {
46
+ const resolved = resolveSecretInputString({
47
+ value: params.value,
48
+ path: params.accountId === DEFAULT_ACCOUNT_ID ? "channels.clickclack.token" : `channels.clickclack.accounts.${params.accountId}.token`,
49
+ defaults: params.cfg.secrets?.defaults,
50
+ mode: "inspect"
51
+ });
52
+ if (resolved.status !== "available") {
53
+ if (resolved.status === "configured_unavailable" && resolved.ref.source === "env") {
54
+ const providerConfig = params.cfg.secrets?.providers?.[resolved.ref.provider];
55
+ if (providerConfig) {
56
+ if (providerConfig.source !== "env") throw new Error(`Secret provider "${resolved.ref.provider}" has source "${providerConfig.source}" but ref requests "env".`);
57
+ 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.`);
58
+ } 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}).`);
59
+ return normalizeSecretInputString((params.env ?? process.env)[resolved.ref.id]) ?? "";
60
+ }
61
+ return "";
62
+ }
63
+ return normalizeResolvedSecretInputString({
64
+ value: resolved.value,
65
+ path: "channels.clickclack.token"
66
+ }) ?? "";
67
+ }
68
+ /**
69
+ * Builds the normalized account snapshot used by gateway, outbound delivery,
70
+ * status reporting, and channel routing.
71
+ */
72
+ function resolveClickClackAccount(params) {
73
+ const accountId = normalizeAccountId(params.accountId);
74
+ const merged = resolveMergedClickClackAccountConfig(params.cfg, accountId);
75
+ const enabled = params.cfg.channels?.clickclack?.enabled !== false && merged.enabled !== false;
76
+ const baseUrl = merged.baseUrl?.trim().replace(/\/$/, "") ?? "";
77
+ const token = resolveClickClackToken({
78
+ cfg: params.cfg,
79
+ value: merged.token,
80
+ accountId,
81
+ env: params.env
82
+ });
83
+ const workspace = merged.workspace?.trim() ?? "";
84
+ return {
85
+ accountId,
86
+ enabled,
87
+ configured: Boolean(baseUrl && token && workspace),
88
+ name: normalizeOptionalString(merged.name),
89
+ baseUrl,
90
+ token,
91
+ workspace,
92
+ botUserId: normalizeOptionalString(merged.botUserId),
93
+ agentId: normalizeOptionalString(merged.agentId),
94
+ replyMode: merged.replyMode === "model" ? "model" : "agent",
95
+ model: normalizeOptionalString(merged.model),
96
+ systemPrompt: normalizeOptionalString(merged.systemPrompt),
97
+ timeoutSeconds: merged.timeoutSeconds,
98
+ toolsAllow: merged.toolsAllow,
99
+ defaultTo: merged.defaultTo?.trim() || "channel:general",
100
+ allowFrom: merged.allowFrom ?? ["*"],
101
+ reconnectMs: resolveIntegerOption(merged.reconnectMs, DEFAULT_RECONNECT_MS, {
102
+ min: MIN_RECONNECT_MS,
103
+ max: MAX_RECONNECT_MS
104
+ }),
105
+ agentActivity: merged.agentActivity === true,
106
+ config: {
107
+ ...merged,
108
+ allowFrom: merged.allowFrom ?? ["*"]
109
+ }
110
+ };
111
+ }
112
+ /**
113
+ * Returns all enabled accounts, including the implicit default account when
114
+ * legacy top-level ClickClack config is present.
115
+ */
116
+ function listEnabledClickClackAccounts(cfg) {
117
+ return listClickClackAccountIds(cfg).map((accountId) => resolveClickClackAccount({
118
+ cfg,
119
+ accountId
120
+ })).filter((account) => account.enabled);
121
+ }
122
+ //#endregion
123
+ //#region extensions/clickclack/src/config-schema.ts
124
+ /**
125
+ * Zod-backed config schema for ClickClack channel accounts.
126
+ */
127
+ const ClickClackAccountConfigSchema = z.object({
128
+ name: z.string().optional(),
129
+ enabled: z.boolean().optional(),
130
+ baseUrl: z.string().url().optional(),
131
+ token: buildSecretInputSchema().optional(),
132
+ workspace: z.string().optional(),
133
+ botUserId: z.string().optional(),
134
+ agentId: z.string().optional(),
135
+ replyMode: z.enum(["agent", "model"]).optional(),
136
+ model: z.string().optional(),
137
+ systemPrompt: z.string().optional(),
138
+ timeoutSeconds: z.number().int().min(1).max(3600).optional(),
139
+ toolsAllow: z.array(z.string()).optional(),
140
+ defaultTo: z.string().optional(),
141
+ allowFrom: z.array(z.string()).optional(),
142
+ reconnectMs: z.number().int().min(100).max(6e4).optional(),
143
+ agentActivity: z.boolean().optional()
144
+ }).strict();
145
+ const ClickClackConfigSchema = ClickClackAccountConfigSchema.extend({
146
+ accounts: z.record(z.string(), ClickClackAccountConfigSchema.partial()).optional(),
147
+ defaultAccount: z.string().optional()
148
+ }).strict();
149
+ /**
150
+ * Config schema exported to core so `openclaw doctor` and config validation
151
+ * understand both default and named ClickClack accounts.
152
+ */
153
+ const clickClackConfigSchema = buildChannelConfigSchema(ClickClackConfigSchema);
154
+ //#endregion
155
+ //#region extensions/clickclack/src/runtime.ts
156
+ /**
157
+ * Runtime store for host-provided OpenClaw services used by the ClickClack
158
+ * bundled plugin.
159
+ */
160
+ const { setRuntime: setClickClackRuntime, getRuntime: getClickClackRuntime } = createPluginRuntimeStore({
161
+ pluginId: "clickclack",
162
+ errorMessage: "ClickClack runtime not initialized"
163
+ });
164
+ //#endregion
165
+ //#region extensions/clickclack/src/access.ts
166
+ /**
167
+ * Maps ClickClack senders and conversations onto the shared channel ingress
168
+ * allowlist/command authorization contract.
169
+ */
170
+ const CHANNEL_ID$2 = "clickclack";
171
+ function normalizeClickClackUserId(value) {
172
+ const trimmed = value.trim();
173
+ if (!trimmed) return null;
174
+ const withoutProvider = trimmed.replace(/^(clickclack|cc):/i, "").trim();
175
+ return withoutProvider.match(/^dm:(.+)$/i)?.[1]?.trim() || withoutProvider || null;
176
+ }
177
+ const clickClackIngressIdentity = {
178
+ key: "user-id",
179
+ normalizeEntry: normalizeClickClackUserId,
180
+ normalizeSubject: normalizeClickClackUserId,
181
+ isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
182
+ entryIdPrefix: "clickclack-user"
183
+ };
184
+ /**
185
+ * Resolves whether a ClickClack message should enter the agent pipeline and
186
+ * whether its command-style body may run tools.
187
+ */
188
+ async function resolveClickClackInboundAccess(params) {
189
+ const runtime = getClickClackRuntime();
190
+ const isDirect = Boolean(params.message.direct_conversation_id);
191
+ const cfg = params.config;
192
+ const shouldCheckCommand = runtime.channel.commands.shouldComputeCommandAuthorized(params.message.body, cfg);
193
+ const resolved = await resolveStableChannelMessageIngress({
194
+ channelId: CHANNEL_ID$2,
195
+ accountId: params.account.accountId,
196
+ identity: clickClackIngressIdentity,
197
+ cfg,
198
+ subject: { stableId: params.message.author_id },
199
+ conversation: {
200
+ kind: isDirect ? "direct" : "group",
201
+ id: isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
202
+ },
203
+ allowFrom: params.account.allowFrom,
204
+ dmPolicy: "allowlist",
205
+ groupPolicy: "allowlist",
206
+ command: shouldCheckCommand ? {
207
+ cfg,
208
+ modeWhenAccessGroupsOff: "configured"
209
+ } : false
210
+ });
211
+ return {
212
+ shouldDispatch: resolved.ingress.admission === "dispatch",
213
+ commandAuthorized: resolved.commandAccess.requested ? resolved.commandAccess.authorized : resolved.senderAccess.allowed
214
+ };
215
+ }
216
+ //#endregion
217
+ //#region extensions/clickclack/src/http-client.ts
218
+ /**
219
+ * Thin ClickClack REST/websocket client used by gateway, resolver, and outbound
220
+ * delivery code.
221
+ */
222
+ /**
223
+ * Serializes optional provenance into the wire fields. Unknown JSON fields
224
+ * are ignored by servers without the provenance columns, so these are safe
225
+ * to send unconditionally when present.
226
+ */
227
+ function provenanceFields(provenance) {
228
+ const fields = {};
229
+ if (provenance?.model?.trim()) fields.author_model = provenance.model.trim();
230
+ if (provenance?.thinking?.trim()) fields.author_thinking = provenance.thinking.trim();
231
+ if (provenance?.runtime?.trim()) fields.author_runtime = provenance.runtime.trim();
232
+ return fields;
233
+ }
234
+ const CLICKCLACK_ERROR_BODY_LIMIT_BYTES = 8192;
235
+ /**
236
+ * Creates a typed client for the ClickClack API using bearer-token auth.
237
+ */
238
+ function createClickClackClient(options) {
239
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
240
+ const fetcher = options.fetch ?? fetch;
241
+ const headers = {
242
+ Authorization: `Bearer ${options.token}`,
243
+ Accept: "application/json"
244
+ };
245
+ async function request(path, init = {}) {
246
+ const requestHeaders = new Headers(init.headers);
247
+ for (const [key, value] of Object.entries(headers)) requestHeaders.set(key, value);
248
+ if (init.body && !(init.body instanceof FormData)) requestHeaders.set("Content-Type", "application/json");
249
+ const response = await fetcher(`${baseUrl}${path}`, {
250
+ ...init,
251
+ headers: requestHeaders
252
+ });
253
+ if (!response.ok) {
254
+ const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
255
+ throw new Error(`ClickClack ${response.status}: ${detail}`);
256
+ }
257
+ return await readProviderJsonResponse(response, "ClickClack response");
258
+ }
259
+ return {
260
+ me: async () => {
261
+ return (await request("/api/me")).user;
262
+ },
263
+ workspaces: async () => {
264
+ return (await request("/api/workspaces")).workspaces;
265
+ },
266
+ channels: async (workspaceId) => {
267
+ return (await request(`/api/workspaces/${encodeURIComponent(workspaceId)}/channels`)).channels;
268
+ },
269
+ channelMessages: async (channelId, afterSeq, limit = 20) => {
270
+ return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
271
+ },
272
+ directMessages: async (conversationId, afterSeq, limit = 20) => {
273
+ return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
274
+ },
275
+ thread: async (messageId) => await request(`/api/messages/${encodeURIComponent(messageId)}/thread`),
276
+ createChannelMessage: async (channelId, body, opts) => {
277
+ return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages`, {
278
+ method: "POST",
279
+ body: JSON.stringify({
280
+ body,
281
+ ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
282
+ ...provenanceFields(opts?.provenance)
283
+ })
284
+ })).message;
285
+ },
286
+ createThreadReply: async (messageId, body, opts) => {
287
+ return (await request(`/api/messages/${encodeURIComponent(messageId)}/thread/replies`, {
288
+ method: "POST",
289
+ body: JSON.stringify({
290
+ body,
291
+ ...provenanceFields(opts?.provenance)
292
+ })
293
+ })).message;
294
+ },
295
+ createDirectConversation: async (workspaceId, memberIds) => {
296
+ return (await request("/api/dms", {
297
+ method: "POST",
298
+ body: JSON.stringify({
299
+ workspace_id: workspaceId,
300
+ member_ids: memberIds
301
+ })
302
+ })).conversation;
303
+ },
304
+ /**
305
+ * POSTs a durable agent activity row (agent_commentary / agent_tool)
306
+ * through the normal message create path. Requires a bot token carrying
307
+ * the agent_activity:write scope on the ClickClack side.
308
+ */
309
+ createActivityMessage: async (params) => {
310
+ if (!params.channelId && !params.conversationId) throw new Error("createActivityMessage requires a channelId or conversationId");
311
+ return (await request(params.channelId ? `/api/channels/${encodeURIComponent(params.channelId)}/messages` : `/api/dms/${encodeURIComponent(params.conversationId ?? "")}/messages`, {
312
+ method: "POST",
313
+ body: JSON.stringify({
314
+ body: params.body,
315
+ kind: params.kind,
316
+ turn_id: params.turnId,
317
+ ...provenanceFields(params.provenance)
318
+ })
319
+ })).message;
320
+ },
321
+ /** PATCHes the body of an existing message (activity row coalescing). */
322
+ updateMessageBody: async (messageId, body) => {
323
+ return (await request(`/api/messages/${encodeURIComponent(messageId)}`, {
324
+ method: "PATCH",
325
+ body: JSON.stringify({ body })
326
+ })).message;
327
+ },
328
+ createDirectMessage: async (conversationId, body, opts) => {
329
+ return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages`, {
330
+ method: "POST",
331
+ body: JSON.stringify({
332
+ body,
333
+ ...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {}
334
+ })
335
+ })).message;
336
+ },
337
+ events: async (workspaceId, afterCursor) => {
338
+ const query = new URLSearchParams({ workspace_id: workspaceId });
339
+ if (afterCursor) query.set("after_cursor", afterCursor);
340
+ return (await request(`/api/realtime/events?${query.toString()}`)).events;
341
+ },
342
+ websocket: (workspaceId, afterCursor) => {
343
+ const url = new URL(`${baseUrl}/api/realtime/ws`);
344
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
345
+ url.searchParams.set("workspace_id", workspaceId);
346
+ if (afterCursor) url.searchParams.set("after_cursor", afterCursor);
347
+ return new WebSocket(url, { headers: { Authorization: `Bearer ${options.token}` } });
348
+ }
349
+ };
350
+ }
351
+ //#endregion
352
+ //#region extensions/clickclack/src/activity.ts
353
+ /**
354
+ * Publishes agent activity (streamed commentary + tool progress) into
355
+ * ClickClack as durable `agent_commentary` / `agent_tool` message rows,
356
+ * coalesced so one logical step becomes one row instead of a row per frame.
357
+ *
358
+ * Ported from the clickglass agent-bridge sidecar, adapted from gateway
359
+ * websocket frames to the in-process `replyOptions.onItemEvent` seam:
360
+ *
361
+ * - Commentary/reasoning arrives as cumulative text snapshots per item id.
362
+ * Each segment becomes one durable row: POSTed when the segment starts
363
+ * streaming and PATCHed (debounced) as the snapshot grows, so prose
364
+ * interleaves chronologically with tool rows.
365
+ * - Tool/step items can emit several frames (start/update/complete) for the
366
+ * same call, sometimes with lane-prefixed ids (`tool:X`, `command:X`).
367
+ * Normalize the prefix away to one key per call, POST one row on the first
368
+ * frame, and PATCH it when a later frame carries a strictly longer body.
369
+ */
370
+ /** Debounce window for PATCHing streaming commentary snapshots. */
371
+ const CLICKCLACK_COMMENTARY_FLUSH_MS = 700;
372
+ /** Item kinds rendered as agent_tool rows; everything else is commentary. */
373
+ const TOOL_ITEM_KINDS = /* @__PURE__ */ new Set([
374
+ "tool",
375
+ "command",
376
+ "command_output",
377
+ "patch",
378
+ "search",
379
+ "api"
380
+ ]);
381
+ /** Item kinds that never become durable rows (ephemeral or plumbing lanes). */
382
+ const SKIPPED_ITEM_KINDS = /* @__PURE__ */ new Set(["lifecycle"]);
383
+ /** Item kinds emitted as cumulative commentary snapshots. */
384
+ const STREAMING_COMMENTARY_ITEM_KINDS = /* @__PURE__ */ new Set([
385
+ "preamble",
386
+ "commentary",
387
+ "analysis",
388
+ "thinking",
389
+ "reasoning"
390
+ ]);
391
+ /** Provider-specific reasoning lanes normalized into one ClickClack label. */
392
+ const THINKING_ITEM_KINDS = /* @__PURE__ */ new Set([
393
+ "analysis",
394
+ "thinking",
395
+ "reasoning"
396
+ ]);
397
+ function normalizedItemKind(payload) {
398
+ return payload.kind?.trim().toLowerCase() ?? "";
399
+ }
400
+ function commentaryBody(payload) {
401
+ const text = payload.progressText?.trim() || payload.summary?.trim() || payload.meta?.trim() || "";
402
+ if (!text) return "";
403
+ const kind = normalizedItemKind(payload);
404
+ if (THINKING_ITEM_KINDS.has(kind)) return `**Thinking**\n\n${text}`;
405
+ return text;
406
+ }
407
+ function activityBody(payload) {
408
+ const line = buildChannelProgressDraftLine({
409
+ event: "item",
410
+ itemId: payload.itemId,
411
+ toolCallId: payload.toolCallId,
412
+ itemKind: payload.kind,
413
+ title: payload.title,
414
+ name: payload.name,
415
+ phase: payload.phase,
416
+ status: payload.status,
417
+ summary: payload.summary,
418
+ progressText: payload.progressText,
419
+ meta: payload.meta
420
+ })?.text?.trim();
421
+ if (line) return line;
422
+ const head = payload.name?.trim() || payload.title?.trim();
423
+ const text = payload.progressText?.trim() || payload.summary?.trim();
424
+ if (head && text) return `**${head}**\n\n${text}`;
425
+ if (text) return text;
426
+ if (head) return head;
427
+ return payload.status?.trim() || payload.kind?.trim() || "";
428
+ }
429
+ /**
430
+ * Creates a per-turn activity publisher. Publishing is best-effort: transport
431
+ * failures are reported through `onError` and never interrupt the reply turn.
432
+ */
433
+ function createClickClackActivityPublisher(params) {
434
+ const flushMs = params.flushMs ?? CLICKCLACK_COMMENTARY_FLUSH_MS;
435
+ const commentaryByItem = /* @__PURE__ */ new Map();
436
+ const toolRows = /* @__PURE__ */ new Map();
437
+ let provenance;
438
+ let chain = Promise.resolve();
439
+ const enqueue = (work) => {
440
+ chain = chain.then(work).catch((error) => {
441
+ params.onError?.(error);
442
+ });
443
+ return chain;
444
+ };
445
+ const postRow = (kind, body) => params.client.createActivityMessage({
446
+ channelId: params.target.channelId,
447
+ conversationId: params.target.conversationId,
448
+ body,
449
+ kind,
450
+ turnId: params.turnId,
451
+ provenance
452
+ });
453
+ const flushCommentary = (segmentKey) => {
454
+ const segment = commentaryByItem.get(segmentKey);
455
+ if (!segment) return Promise.resolve();
456
+ if (segment.timer) {
457
+ clearTimeout(segment.timer);
458
+ segment.timer = void 0;
459
+ }
460
+ if (!segment.dirty || !segment.body.trim()) return Promise.resolve();
461
+ segment.dirty = false;
462
+ const body = segment.body;
463
+ return enqueue(async () => {
464
+ if (segment.messageId) {
465
+ await params.client.updateMessageBody(segment.messageId, body);
466
+ return;
467
+ }
468
+ const posted = await postRow("agent_commentary", body);
469
+ segment.messageId = posted.id;
470
+ });
471
+ };
472
+ const flushAllCommentary = () => {
473
+ const flushes = [...commentaryByItem.keys()].map((key) => flushCommentary(key));
474
+ return Promise.all(flushes).then(() => void 0);
475
+ };
476
+ const handleCommentary = (payload) => {
477
+ const body = commentaryBody(payload);
478
+ if (!body.trim()) return;
479
+ const key = payload.itemId?.trim() || "turn";
480
+ let segment = commentaryByItem.get(key);
481
+ if (!segment) {
482
+ segment = {
483
+ body: "",
484
+ dirty: false
485
+ };
486
+ commentaryByItem.set(key, segment);
487
+ }
488
+ if (body.length < segment.body.length || body === segment.body) return;
489
+ segment.body = body;
490
+ segment.dirty = true;
491
+ if (!segment.timer) segment.timer = setTimeout(() => {
492
+ segment.timer = void 0;
493
+ flushCommentary(key);
494
+ }, flushMs);
495
+ };
496
+ const toolRowKey = (payload) => {
497
+ const toolCallId = payload.toolCallId?.trim();
498
+ if (toolCallId) return toolCallId;
499
+ return (payload.itemId?.trim() ?? "").replace(/^(tool|command):/, "");
500
+ };
501
+ const handleDiscreteItem = (payload) => {
502
+ const body = activityBody(payload);
503
+ if (!body) return;
504
+ const kind = TOOL_ITEM_KINDS.has(payload.kind?.trim().toLowerCase() ?? "") ? "agent_tool" : "agent_commentary";
505
+ flushAllCommentary();
506
+ const key = `${kind}:${toolRowKey(payload)}`;
507
+ const existing = toolRows.get(key);
508
+ if (!existing || !toolRowKey(payload)) {
509
+ const row = { body };
510
+ if (toolRowKey(payload)) toolRows.set(key, row);
511
+ enqueue(async () => {
512
+ const posted = await postRow(kind, row.body);
513
+ row.messageId = posted.id;
514
+ row.sentBody = row.body;
515
+ });
516
+ return;
517
+ }
518
+ if (body.length <= existing.body.length) return;
519
+ existing.body = body;
520
+ enqueue(async () => {
521
+ if (existing.messageId && existing.body !== existing.sentBody) {
522
+ await params.client.updateMessageBody(existing.messageId, existing.body);
523
+ existing.sentBody = existing.body;
524
+ }
525
+ });
526
+ };
527
+ return {
528
+ onItemEvent: (payload) => {
529
+ const kind = normalizedItemKind(payload);
530
+ if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
531
+ handleCommentary(payload);
532
+ return;
533
+ }
534
+ if (SKIPPED_ITEM_KINDS.has(kind)) return;
535
+ handleDiscreteItem(payload);
536
+ },
537
+ setProvenance: (next) => {
538
+ provenance = next;
539
+ },
540
+ finalize: async () => {
541
+ await flushAllCommentary();
542
+ await chain;
543
+ }
544
+ };
545
+ }
546
+ //#endregion
547
+ //#region extensions/clickclack/src/resolve.ts
548
+ /**
549
+ * Resolves a workspace slug/name/id from config to a ClickClack workspace id.
550
+ */
551
+ async function resolveWorkspaceId(client, workspace) {
552
+ if (workspace.startsWith("wsp_")) return workspace;
553
+ const found = (await client.workspaces()).find((candidate) => candidate.id === workspace || candidate.slug === workspace || candidate.name === workspace);
554
+ if (!found) throw new Error(`ClickClack workspace not found: ${workspace}`);
555
+ return found.id;
556
+ }
557
+ /**
558
+ * Resolves a channel name/id from config or target input to a ClickClack
559
+ * channel id.
560
+ */
561
+ async function resolveChannelId(client, workspaceId, channel) {
562
+ if (channel.startsWith("chn_")) return channel;
563
+ const found = (await client.channels(workspaceId)).find((candidate) => candidate.id === channel || candidate.name === channel);
564
+ if (!found) throw new Error(`ClickClack channel not found: ${channel}`);
565
+ return found.id;
566
+ }
567
+ //#endregion
568
+ //#region extensions/clickclack/src/target.ts
569
+ /**
570
+ * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
571
+ */
572
+ function parseClickClackTarget(raw) {
573
+ const value = raw.trim();
574
+ if (!value) throw new Error("ClickClack target is required");
575
+ const [prefix, ...rest] = value.split(":");
576
+ const body = rest.join(":").trim();
577
+ if (prefix === "channel" && body) return {
578
+ chatType: "group",
579
+ kind: "channel",
580
+ id: body
581
+ };
582
+ if (prefix === "thread" && body) return {
583
+ chatType: "group",
584
+ kind: "thread",
585
+ id: body
586
+ };
587
+ if (prefix === "dm" && body) return {
588
+ chatType: "direct",
589
+ kind: "dm",
590
+ id: body
591
+ };
592
+ if (!body) return {
593
+ chatType: "group",
594
+ kind: "channel",
595
+ id: value
596
+ };
597
+ throw new Error(`Unsupported ClickClack target: ${raw}`);
598
+ }
599
+ /** Formats a parsed ClickClack target back into canonical target syntax. */
600
+ function buildClickClackTarget(target) {
601
+ return `${target.kind}:${target.id}`;
602
+ }
603
+ /** Normalizes user-entered ClickClack target text for channel routing. */
604
+ function normalizeClickClackTarget(raw) {
605
+ return buildClickClackTarget(parseClickClackTarget(raw));
606
+ }
607
+ /** Reports whether a target string can be offered to the ClickClack parser. */
608
+ function looksLikeClickClackTarget(raw) {
609
+ return /^(channel|thread|dm):/i.test(raw.trim()) || raw.trim().length > 0;
610
+ }
611
+ //#endregion
612
+ //#region extensions/clickclack/src/outbound.ts
613
+ /**
614
+ * Outbound ClickClack delivery helpers for channel messages, thread replies,
615
+ * and direct messages.
616
+ */
617
+ /**
618
+ * Sends visible text to a normalized ClickClack target and returns the created
619
+ * message id, or undefined when sanitization removes all content.
620
+ */
621
+ async function sendClickClackText(params) {
622
+ const text = sanitizeAssistantVisibleText(params.text);
623
+ if (!text) return;
624
+ const account = resolveClickClackAccount({
625
+ cfg: params.cfg,
626
+ accountId: params.accountId
627
+ });
628
+ const client = createClickClackClient({
629
+ baseUrl: account.baseUrl,
630
+ token: account.token
631
+ });
632
+ const workspaceId = await resolveWorkspaceId(client, account.workspace);
633
+ const parsed = parseClickClackTarget(params.to);
634
+ const explicitThreadId = params.threadId == null ? "" : String(params.threadId);
635
+ const replyToId = params.replyToId == null ? "" : String(params.replyToId);
636
+ if (explicitThreadId || parsed.kind === "thread") {
637
+ const rootId = explicitThreadId || parsed.id;
638
+ return (await client.createThreadReply(rootId, text, { provenance: params.provenance })).id;
639
+ }
640
+ if (parsed.kind === "dm") {
641
+ const dm = await client.createDirectConversation(workspaceId, [parsed.id]);
642
+ return (await client.createDirectMessage(dm.id, text, { quotedMessageId: replyToId || void 0 })).id;
643
+ }
644
+ const channelId = await resolveChannelId(client, workspaceId, parsed.id);
645
+ return (await client.createChannelMessage(channelId, text, {
646
+ provenance: params.provenance,
647
+ quotedMessageId: replyToId || void 0
648
+ })).id;
649
+ }
650
+ //#endregion
651
+ //#region extensions/clickclack/src/inbound.ts
652
+ const CHANNEL_ID$1 = "clickclack";
653
+ function resolveAccountAgentRoute(params) {
654
+ const runtime = getClickClackRuntime();
655
+ const route = runtime.channel.routing.resolveAgentRoute({
656
+ cfg: params.cfg,
657
+ channel: CHANNEL_ID$1,
658
+ accountId: params.account.accountId,
659
+ peer: {
660
+ kind: params.isDirect ? "direct" : "channel",
661
+ id: params.target
662
+ }
663
+ });
664
+ const agentId = normalizeAgentId(params.account.agentId ?? route.agentId);
665
+ if (agentId === route.agentId) return route;
666
+ const peer = {
667
+ kind: params.isDirect ? "direct" : "channel",
668
+ id: params.target
669
+ };
670
+ const dmScope = params.cfg.session?.dmScope ?? "main";
671
+ const sessionKey = runtime.channel.routing.buildAgentSessionKey({
672
+ agentId,
673
+ mainKey: params.cfg.session?.mainKey,
674
+ channel: CHANNEL_ID$1,
675
+ accountId: params.account.accountId,
676
+ peer,
677
+ dmScope,
678
+ identityLinks: params.cfg.session?.identityLinks
679
+ });
680
+ const mainSessionKey = runtime.channel.routing.buildAgentSessionKey({
681
+ agentId,
682
+ mainKey: params.cfg.session?.mainKey,
683
+ channel: CHANNEL_ID$1,
684
+ accountId: params.account.accountId,
685
+ dmScope: "main"
686
+ });
687
+ return {
688
+ ...route,
689
+ agentId,
690
+ dmScope,
691
+ sessionKey,
692
+ mainSessionKey,
693
+ lastRoutePolicy: sessionKey === mainSessionKey ? "main" : "session"
694
+ };
695
+ }
696
+ async function dispatchModelReply(params) {
697
+ const text = (await getClickClackRuntime().llm.complete({
698
+ agentId: params.route.agentId,
699
+ model: params.account.model,
700
+ maxTokens: 96,
701
+ purpose: "clickclack bot reply",
702
+ systemPrompt: params.account.systemPrompt,
703
+ messages: [{
704
+ role: "user",
705
+ content: params.message.body
706
+ }]
707
+ })).text.trim();
708
+ if (!text) return;
709
+ await sendClickClackText({
710
+ cfg: params.cfg,
711
+ accountId: params.account.accountId,
712
+ to: params.target,
713
+ text,
714
+ threadId: params.message.parent_message_id ? params.message.thread_root_id : void 0,
715
+ replyToId: params.message.id
716
+ });
717
+ }
718
+ /**
719
+ * Dispatches one already-fetched ClickClack message through the configured
720
+ * reply mode for its account.
721
+ */
722
+ async function handleClickClackInbound(params) {
723
+ const runtime = getClickClackRuntime();
724
+ const message = params.message;
725
+ const access = params.access ?? await resolveClickClackInboundAccess({
726
+ account: params.account,
727
+ config: params.config,
728
+ message
729
+ });
730
+ if (!access.shouldDispatch) return;
731
+ const isDirect = Boolean(message.direct_conversation_id);
732
+ const target = buildClickClackTarget(isDirect ? {
733
+ chatType: "direct",
734
+ kind: "dm",
735
+ id: message.author_id
736
+ } : {
737
+ chatType: "group",
738
+ kind: "channel",
739
+ id: message.channel_id ?? ""
740
+ });
741
+ const route = resolveAccountAgentRoute({
742
+ cfg: params.config,
743
+ account: params.account,
744
+ target,
745
+ isDirect
746
+ });
747
+ if (params.account.replyMode === "model") {
748
+ await dispatchModelReply({
749
+ account: params.account,
750
+ cfg: params.config,
751
+ message,
752
+ route,
753
+ target
754
+ });
755
+ return;
756
+ }
757
+ let turnProvenance;
758
+ let activity;
759
+ if (params.account.agentActivity && (message.channel_id || message.direct_conversation_id)) activity = createClickClackActivityPublisher({
760
+ client: createClickClackClient({
761
+ baseUrl: params.account.baseUrl,
762
+ token: params.account.token
763
+ }),
764
+ target: message.channel_id ? { channelId: message.channel_id } : { conversationId: message.direct_conversation_id },
765
+ turnId: message.id,
766
+ onError: (error) => {
767
+ runtime.logging.getChildLogger({
768
+ plugin: "clickclack",
769
+ feature: "agent-activity"
770
+ }).warn(`clickclack activity publish failed: ${String(error)}`);
771
+ }
772
+ });
773
+ const senderName = message.author?.display_name || message.author_id;
774
+ const previousTimestamp = runtime.channel.session.readSessionUpdatedAt({
775
+ storePath: runtime.channel.session.resolveStorePath(params.config.session?.store, { agentId: route.agentId }),
776
+ sessionKey: route.sessionKey
777
+ });
778
+ const body = runtime.channel.reply.formatAgentEnvelope({
779
+ channel: "ClickClack",
780
+ from: senderName,
781
+ timestamp: new Date(message.created_at),
782
+ previousTimestamp,
783
+ envelope: runtime.channel.reply.resolveEnvelopeFormatOptions(params.config),
784
+ body: message.body
785
+ });
786
+ const storePath = runtime.channel.session.resolveStorePath(params.config.session?.store, { agentId: route.agentId });
787
+ const ctxPayload = runtime.channel.reply.finalizeInboundContext({
788
+ Body: body,
789
+ BodyForAgent: message.body,
790
+ RawBody: message.body,
791
+ CommandBody: message.body,
792
+ From: target,
793
+ To: target,
794
+ SessionKey: route.sessionKey,
795
+ AccountId: route.accountId ?? params.account.accountId,
796
+ ChatType: isDirect ? "direct" : "group",
797
+ WasMentioned: isDirect ? void 0 : true,
798
+ ConversationLabel: isDirect ? senderName : message.channel_id,
799
+ GroupChannel: message.channel_id,
800
+ NativeChannelId: message.channel_id || message.direct_conversation_id,
801
+ MessageThreadId: message.parent_message_id ? message.thread_root_id : void 0,
802
+ ThreadParentId: message.parent_message_id ? message.thread_root_id : void 0,
803
+ SenderName: senderName,
804
+ SenderId: message.author_id,
805
+ Provider: CHANNEL_ID$1,
806
+ Surface: CHANNEL_ID$1,
807
+ MessageSid: message.id,
808
+ MessageSidFull: message.id,
809
+ ReplyToId: message.id,
810
+ Timestamp: message.created_at,
811
+ OriginatingChannel: CHANNEL_ID$1,
812
+ OriginatingTo: target,
813
+ CommandAuthorized: access.commandAuthorized
814
+ });
815
+ const dispatchPromise = runtime.channel.inbound.dispatchReply({
816
+ cfg: params.config,
817
+ channel: CHANNEL_ID$1,
818
+ accountId: params.account.accountId,
819
+ agentId: route.agentId,
820
+ routeSessionKey: route.sessionKey,
821
+ storePath,
822
+ ctxPayload,
823
+ recordInboundSession: runtime.channel.session.recordInboundSession,
824
+ dispatchReplyWithBufferedBlockDispatcher: runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
825
+ toolsAllow: params.account.toolsAllow,
826
+ replyOptions: activity ? {
827
+ onModelSelected: (ctx) => {
828
+ turnProvenance = {
829
+ model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
830
+ thinking: ctx.thinkLevel
831
+ };
832
+ activity?.setProvenance(turnProvenance);
833
+ },
834
+ onItemEvent: activity.onItemEvent,
835
+ commentaryProgressEnabled: true,
836
+ suppressDefaultToolProgressMessages: true,
837
+ allowProgressCallbacksWhenSourceDeliverySuppressed: true
838
+ } : void 0,
839
+ delivery: {
840
+ deliver: async (payload) => {
841
+ const text = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
842
+ if (!text.trim()) return;
843
+ await sendClickClackText({
844
+ cfg: params.config,
845
+ accountId: params.account.accountId,
846
+ to: target,
847
+ text,
848
+ threadId: message.parent_message_id ? message.thread_root_id : void 0,
849
+ replyToId: message.id,
850
+ provenance: turnProvenance
851
+ });
852
+ },
853
+ onError: (error) => {
854
+ throw error instanceof Error ? error : /* @__PURE__ */ new Error(`clickclack dispatch failed: ${String(error)}`);
855
+ }
856
+ },
857
+ replyPipeline: {},
858
+ record: { onRecordError: (error) => {
859
+ throw error instanceof Error ? error : /* @__PURE__ */ new Error(`clickclack session record failed: ${String(error)}`);
860
+ } }
861
+ });
862
+ try {
863
+ await dispatchPromise;
864
+ } finally {
865
+ await activity?.finalize();
866
+ }
867
+ }
868
+ //#endregion
869
+ //#region extensions/clickclack/src/gateway.ts
870
+ function payloadString(event, key) {
871
+ const value = event.payload?.[key];
872
+ return typeof value === "string" ? value : "";
873
+ }
874
+ async function resolveEventMessage(params) {
875
+ const messageId = payloadString(params.event, "message_id");
876
+ if (!messageId) return null;
877
+ const directConversationId = payloadString(params.event, "direct_conversation_id");
878
+ if (directConversationId && typeof params.event.seq === "number") return (await params.client.directMessages(directConversationId, params.event.seq - 1, 10)).find((message) => message.id === messageId) ?? null;
879
+ if (params.event.type === "thread.reply_created") {
880
+ const rootId = payloadString(params.event, "root_message_id");
881
+ if (!rootId) return null;
882
+ return (await params.client.thread(rootId)).replies.find((message) => message.id === messageId) ?? null;
883
+ }
884
+ if (params.event.channel_id && typeof params.event.seq === "number") return (await params.client.channelMessages(params.event.channel_id, params.event.seq - 1, 10)).find((message) => message.id === messageId) ?? null;
885
+ return null;
886
+ }
887
+ function decodeSocketMessage(data) {
888
+ if (typeof data === "string") return data;
889
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
890
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
891
+ return Buffer.concat(data).toString("utf8");
892
+ }
893
+ function parseSocketEvent(data) {
894
+ try {
895
+ return JSON.parse(decodeSocketMessage(data));
896
+ } catch {
897
+ return null;
898
+ }
899
+ }
900
+ async function processEvent(params) {
901
+ if (params.event.type !== "message.created" && params.event.type !== "thread.reply_created") return;
902
+ if (payloadString(params.event, "author_id") === params.botUserId) return;
903
+ const message = await resolveEventMessage({
904
+ client: params.client,
905
+ event: params.event
906
+ });
907
+ if (!message || message.author_id === params.botUserId) return;
908
+ if (message.author?.kind === "bot") return;
909
+ const access = await resolveClickClackInboundAccess({
910
+ account: params.account,
911
+ config: params.config,
912
+ message
913
+ });
914
+ if (!access.shouldDispatch) return;
915
+ await handleClickClackInbound({
916
+ account: params.account,
917
+ config: params.config,
918
+ message,
919
+ access
920
+ });
921
+ }
922
+ async function startClickClackGatewayAccount(ctx) {
923
+ const configuredAccount = resolveClickClackAccount({
924
+ cfg: ctx.cfg,
925
+ accountId: ctx.account.accountId
926
+ });
927
+ if (!configuredAccount.configured) throw new Error(`ClickClack is not configured for account "${configuredAccount.accountId}"`);
928
+ const client = createClickClackClient({
929
+ baseUrl: configuredAccount.baseUrl,
930
+ token: configuredAccount.token
931
+ });
932
+ const workspaceId = await resolveWorkspaceId(client, configuredAccount.workspace);
933
+ const me = await client.me();
934
+ const account = {
935
+ ...configuredAccount,
936
+ workspace: workspaceId,
937
+ botUserId: configuredAccount.botUserId ?? me.id
938
+ };
939
+ ctx.setStatus({
940
+ accountId: account.accountId,
941
+ running: true,
942
+ configured: true,
943
+ enabled: account.enabled,
944
+ baseUrl: account.baseUrl
945
+ });
946
+ let afterCursor = "";
947
+ let initialized = false;
948
+ try {
949
+ while (!ctx.abortSignal.aborted) {
950
+ const backlog = await client.events(workspaceId, afterCursor);
951
+ if (!initialized) {
952
+ for (const event of backlog) afterCursor = event.cursor || afterCursor;
953
+ initialized = true;
954
+ } else for (const event of backlog) {
955
+ afterCursor = event.cursor || afterCursor;
956
+ await processEvent({
957
+ account,
958
+ config: ctx.cfg,
959
+ client,
960
+ event,
961
+ botUserId: account.botUserId
962
+ });
963
+ }
964
+ const socket = client.websocket(workspaceId, afterCursor);
965
+ await new Promise((resolve, reject) => {
966
+ let settled = false;
967
+ let removeAbortListener;
968
+ const finishSocketCycle = () => {
969
+ if (settled) return;
970
+ settled = true;
971
+ removeAbortListener?.();
972
+ removeAbortListener = void 0;
973
+ resolve();
974
+ };
975
+ const abort = () => {
976
+ socket.close();
977
+ finishSocketCycle();
978
+ };
979
+ ctx.abortSignal.addEventListener("abort", abort, { once: true });
980
+ removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
981
+ socket.on("message", (data) => {
982
+ (async () => {
983
+ const event = parseSocketEvent(data);
984
+ if (!event) {
985
+ ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
986
+ return;
987
+ }
988
+ afterCursor = event.cursor || afterCursor;
989
+ await processEvent({
990
+ account,
991
+ config: ctx.cfg,
992
+ client,
993
+ event,
994
+ botUserId: account.botUserId ?? ""
995
+ });
996
+ })().catch((e) => reject(e instanceof Error ? e : new Error(`ClickClack ws message failed: ${formatErrorMessage(e)}`, { cause: e })));
997
+ });
998
+ socket.on("close", finishSocketCycle);
999
+ socket.on("error", (error) => {
1000
+ if (settled || ctx.abortSignal.aborted) {
1001
+ finishSocketCycle();
1002
+ return;
1003
+ }
1004
+ ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
1005
+ finishSocketCycle();
1006
+ socket.close();
1007
+ });
1008
+ });
1009
+ if (!ctx.abortSignal.aborted) await new Promise((resolve) => {
1010
+ setTimeout(resolve, account.reconnectMs);
1011
+ });
1012
+ }
1013
+ } finally {
1014
+ ctx.setStatus({
1015
+ accountId: account.accountId,
1016
+ running: false
1017
+ });
1018
+ }
1019
+ }
1020
+ //#endregion
1021
+ //#region extensions/clickclack/src/channel.ts
1022
+ /**
1023
+ * ClickClack channel plugin definition: target parsing, account config, status,
1024
+ * gateway startup, and outbound delivery wiring.
1025
+ */
1026
+ const CHANNEL_ID = "clickclack";
1027
+ const meta = { ...getChatChannelMeta(CHANNEL_ID) };
1028
+ const clickClackMessageAdapter = defineChannelMessageAdapter({
1029
+ id: CHANNEL_ID,
1030
+ durableFinal: { capabilities: {
1031
+ text: true,
1032
+ replyTo: true,
1033
+ thread: true,
1034
+ messageSendingHooks: true
1035
+ } },
1036
+ send: { text: async (ctx) => {
1037
+ const messageId = await sendClickClackText({
1038
+ cfg: ctx.cfg,
1039
+ accountId: ctx.accountId,
1040
+ to: ctx.to,
1041
+ text: ctx.text,
1042
+ threadId: ctx.threadId,
1043
+ replyToId: ctx.replyToId
1044
+ });
1045
+ const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
1046
+ const replyToId = ctx.replyToId ?? void 0;
1047
+ return {
1048
+ ...messageId ? { messageId } : {},
1049
+ receipt: createMessageReceiptFromOutboundResults({
1050
+ results: messageId ? [{
1051
+ channel: CHANNEL_ID,
1052
+ messageId
1053
+ }] : [],
1054
+ threadId,
1055
+ replyToId,
1056
+ kind: "text"
1057
+ })
1058
+ };
1059
+ } }
1060
+ });
1061
+ /**
1062
+ * Channel plugin instance registered by the bundled ClickClack entry.
1063
+ */
1064
+ const clickClackPlugin = createChatChannelPlugin({
1065
+ base: {
1066
+ id: CHANNEL_ID,
1067
+ meta,
1068
+ capabilities: {
1069
+ chatTypes: ["direct", "group"],
1070
+ threads: true,
1071
+ blockStreaming: true
1072
+ },
1073
+ reload: { configPrefixes: ["channels.clickclack"] },
1074
+ configSchema: clickClackConfigSchema,
1075
+ config: {
1076
+ listAccountIds: (cfg) => listClickClackAccountIds(cfg),
1077
+ resolveAccount: (cfg, accountId) => resolveClickClackAccount({
1078
+ cfg,
1079
+ accountId
1080
+ }),
1081
+ defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg),
1082
+ isConfigured: (account) => account.configured,
1083
+ resolveAllowFrom: ({ cfg, accountId }) => resolveClickClackAccount({
1084
+ cfg,
1085
+ accountId
1086
+ }).allowFrom,
1087
+ resolveDefaultTo: ({ cfg, accountId }) => resolveClickClackAccount({
1088
+ cfg,
1089
+ accountId
1090
+ }).defaultTo
1091
+ },
1092
+ messaging: {
1093
+ targetPrefixes: ["clickclack", "cc"],
1094
+ normalizeTarget: normalizeClickClackTarget,
1095
+ inferTargetChatType: ({ to }) => parseClickClackTarget(to).chatType,
1096
+ targetResolver: {
1097
+ looksLikeId: looksLikeClickClackTarget,
1098
+ hint: "<channel:name|dm:usr_id|thread:msg_id>"
1099
+ },
1100
+ resolveOutboundSessionRoute: ({ cfg, agentId, accountId, target, replyToId, threadId, currentSessionKey }) => {
1101
+ const parsed = parseClickClackTarget(target);
1102
+ const baseRoute = buildChannelOutboundSessionRoute({
1103
+ cfg,
1104
+ agentId,
1105
+ channel: CHANNEL_ID,
1106
+ accountId,
1107
+ recipientSessionExact: parsed.kind === "dm",
1108
+ peer: {
1109
+ kind: parsed.chatType === "direct" ? "direct" : "channel",
1110
+ id: buildClickClackTarget(parsed)
1111
+ },
1112
+ chatType: parsed.chatType,
1113
+ from: `clickclack:${accountId ?? DEFAULT_ACCOUNT_ID}`,
1114
+ to: buildClickClackTarget(parsed)
1115
+ });
1116
+ return buildThreadAwareOutboundSessionRoute({
1117
+ route: baseRoute,
1118
+ replyToId,
1119
+ threadId: threadId ?? (parsed.kind === "thread" ? parsed.id : void 0),
1120
+ currentSessionKey,
1121
+ useSuffix: false,
1122
+ canRecoverCurrentThread: () => true
1123
+ });
1124
+ },
1125
+ resolveSessionConversation: ({ rawId }) => {
1126
+ const parsed = parseClickClackTarget(rawId);
1127
+ if (parsed.kind === "dm") return null;
1128
+ return {
1129
+ id: parsed.id,
1130
+ threadId: parsed.kind === "thread" ? parsed.id : void 0,
1131
+ baseConversationId: parsed.id,
1132
+ parentConversationCandidates: [parsed.id]
1133
+ };
1134
+ }
1135
+ },
1136
+ status: createComputedAccountStatusAdapter({
1137
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
1138
+ buildChannelSummary: ({ snapshot }) => ({
1139
+ ok: snapshot.configured,
1140
+ label: snapshot.configured ? "configured" : "missing config",
1141
+ detail: snapshot.baseUrl ?? ""
1142
+ }),
1143
+ resolveAccountSnapshot: ({ account }) => ({
1144
+ accountId: account.accountId,
1145
+ name: account.name,
1146
+ enabled: account.enabled,
1147
+ configured: account.configured,
1148
+ baseUrl: account.baseUrl
1149
+ })
1150
+ }),
1151
+ gateway: { startAccount: startClickClackGatewayAccount },
1152
+ message: clickClackMessageAdapter
1153
+ },
1154
+ outbound: {
1155
+ base: { deliveryMode: "direct" },
1156
+ attachedResults: {
1157
+ channel: CHANNEL_ID,
1158
+ sendText: async ({ cfg, to, text, accountId, threadId, replyToId }) => {
1159
+ return { messageId: await sendClickClackText({
1160
+ cfg,
1161
+ accountId,
1162
+ to,
1163
+ text,
1164
+ threadId,
1165
+ replyToId
1166
+ }) ?? "" };
1167
+ }
1168
+ }
1169
+ }
1170
+ });
1171
+ //#endregion
1172
+ 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 };