@openclaw/clickclack 2026.7.2-beta.7 → 2026.7.33

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,1852 +0,0 @@
1
- import { _ as clickClackMeta, b as listEnabledClickClackAccounts, c as resolveChannelId, d as createClickClackClient, g as clickClackConfigAdapter, h as CLICKCLACK_CHANNEL_ID, l as resolveWorkspaceId, m as clickClackConfigSchema, n as clickClackSetupAdapter, p as normalizeClickClackCorrelationId, r as clickClackSetupContract, t as clickClackSetupWizard, v as DEFAULT_ACCOUNT_ID, x as resolveClickClackAccount } from "./setup-surface-DAy8vDJK.js";
2
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
3
- import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
4
- import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
5
- import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
6
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
- import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
8
- import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
9
- import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
10
- import { createHash, randomUUID } from "node:crypto";
11
- import { buildMentionRegexes, normalizeMentionText } from "openclaw/plugin-sdk/channel-mention-gating";
12
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
13
- import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
14
- import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
15
- import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
16
- import { FormatCapabilityProfile, renderMarkdownWithMarkers, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
17
- //#region extensions/clickclack/src/discussions/binding-generation.ts
18
- const DISCUSSION_GENERATIONS_NAMESPACE = "discussion-binding-generations";
19
- const MAX_PENDING_DISCUSSION_GENERATIONS = 1e4;
20
- const storesByRuntime$2 = /* @__PURE__ */ new WeakMap();
21
- function getGenerationStore(runtime) {
22
- const existing = storesByRuntime$2.get(runtime);
23
- if (existing) return existing;
24
- const created = runtime.state.openSyncKeyedStore({
25
- namespace: DISCUSSION_GENERATIONS_NAMESPACE,
26
- maxEntries: MAX_PENDING_DISCUSSION_GENERATIONS,
27
- overflowPolicy: "reject-new"
28
- });
29
- storesByRuntime$2.set(runtime, created);
30
- return created;
31
- }
32
- /** Reserves a generation so an interrupted channel create can be adopted on retry. */
33
- function reserveDiscussionBindingGeneration(params) {
34
- const store = getGenerationStore(params.runtime);
35
- const existing = store.lookup(params.sessionKey);
36
- if (existing?.destinationIdentity === params.destinationIdentity) return existing.generation;
37
- const generation = (params.createGeneration ?? randomUUID)();
38
- store.register(params.sessionKey, {
39
- destinationIdentity: params.destinationIdentity,
40
- generation
41
- });
42
- return generation;
43
- }
44
- /** Clears only the completed reservation; future opens must mint a new ownership ref. */
45
- function clearDiscussionBindingGeneration(params) {
46
- const store = getGenerationStore(params.runtime);
47
- const existing = store.lookup(params.sessionKey);
48
- if (!existing) return;
49
- if (params.expectedGeneration && existing.generation !== params.expectedGeneration) return;
50
- store.delete(params.sessionKey);
51
- }
52
- /** Quarantines a destination before the first fallible channel create. */
53
- function recordPendingDiscussionOpen(params) {
54
- const store = getGenerationStore(params.runtime);
55
- const existing = store.lookup(params.sessionKey);
56
- if (!existing || existing.generation !== params.generation) throw new Error("ClickClack discussion generation changed before channel creation");
57
- store.register(params.sessionKey, {
58
- ...existing,
59
- pending: params.pending
60
- });
61
- }
62
- function listPendingDiscussionOpens(runtime) {
63
- return getGenerationStore(runtime).entries().flatMap((entry) => entry.value.pending ? [{
64
- sessionKey: entry.key,
65
- generation: entry.value.generation,
66
- ...entry.value.pending
67
- }] : []);
68
- }
69
- function hasPendingDiscussionOpenForDestination(params) {
70
- const serverBaseUrl = params.serverBaseUrl.replace(/\/+$/u, "");
71
- return listPendingDiscussionOpens(params.runtime).some((pending) => pending.serverBaseUrl === serverBaseUrl && pending.workspaceId === params.workspaceId);
72
- }
73
- //#endregion
74
- //#region extensions/clickclack/src/discussions/naming.ts
75
- function shortSessionHash(sessionKey) {
76
- return createHash("sha256").update(sessionKey).digest("hex").slice(0, 32);
77
- }
78
- function slugifyLabel(label) {
79
- return label.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80).replace(/-+$/gu, "");
80
- }
81
- function fallbackDiscussionLabel(sessionKey, agentId) {
82
- const agentSegment = agentId?.trim() ? slugifyLabel(agentId) : "";
83
- const hash = shortSessionHash(sessionKey).slice(0, 8);
84
- return `s-${agentSegment ? `${agentSegment}-` : ""}${hash}`;
85
- }
86
- function resolveDiscussionLabel(entry, sessionKey, agentId) {
87
- return entry.label?.trim() || entry.displayName?.trim() || entry.subject?.trim() || fallbackDiscussionLabel(sessionKey, agentId);
88
- }
89
- function truncateDiscussionDisplayTitle(label) {
90
- return Array.from(label).slice(0, 200).join("").trim();
91
- }
92
- function slugifyDiscussionLabel(label, sessionKey) {
93
- return slugifyLabel(label) || fallbackDiscussionLabel(sessionKey);
94
- }
95
- function discussionSessionPeerId(identity) {
96
- return `disc-${createHash("sha256").update([
97
- identity.mainSessionKey,
98
- identity.sessionId,
99
- identity.serverBaseUrl,
100
- identity.channelId,
101
- identity.externalRef
102
- ].join("\0")).digest("hex").slice(0, 32)}`;
103
- }
104
- function isDiscussionSessionKey(sessionKey) {
105
- return sessionKey.includes(":clickclack:") && /:channel:disc-[0-9a-f]{32}$/u.test(sessionKey);
106
- }
107
- function discussionExternalRef(installationId, mainSessionKey, sessionId, destinationIdentity, bindingGeneration) {
108
- return `openclaw:${installationId}:${shortSessionHash([
109
- mainSessionKey,
110
- sessionId,
111
- destinationIdentity,
112
- bindingGeneration
113
- ].join("\0"))}`;
114
- }
115
- function discussionCredentialFingerprint(token) {
116
- return createHash("sha256").update(token).digest("hex");
117
- }
118
- function discussionSessionKey(params) {
119
- return params.runtime.channel.routing.buildAgentSessionKey({
120
- agentId: params.agentId,
121
- channel: "clickclack",
122
- accountId: params.accountId,
123
- peer: {
124
- kind: "channel",
125
- id: discussionSessionPeerId(params)
126
- }
127
- });
128
- }
129
- //#endregion
130
- //#region extensions/clickclack/src/discussions/binding-store.ts
131
- function bindingMatchesSessionIncarnation(runtime, sessionKey, binding) {
132
- const entry = runtime.agent.session.getSessionEntry({
133
- sessionKey,
134
- readConsistency: "latest"
135
- });
136
- return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId);
137
- }
138
- function bindingMatchesActiveSessionIncarnation(runtime, sessionKey, binding) {
139
- const entry = runtime.agent.session.getSessionEntry({
140
- sessionKey,
141
- readConsistency: "latest"
142
- });
143
- return Boolean(entry && binding.sessionId && entry.sessionId === binding.sessionId && entry.archivedAt === void 0);
144
- }
145
- const DISCUSSION_BINDINGS_NAMESPACE = "discussion-bindings";
146
- const MAX_DISCUSSION_BINDINGS = 1e4;
147
- const storesByRuntime$1 = /* @__PURE__ */ new WeakMap();
148
- function channelKey(serverBaseUrl, channelId) {
149
- return `${serverBaseUrl.replace(/\/+$/u, "")}\0${channelId}`;
150
- }
151
- /** SQLite-backed session/channel bindings with a process-local inbound lookup index. */
152
- var ClickClackDiscussionBindingStore = class {
153
- #store;
154
- #sessionByChannel = /* @__PURE__ */ new Map();
155
- #mainByDiscussionSession = /* @__PURE__ */ new Map();
156
- #runtime;
157
- constructor(runtime) {
158
- this.#runtime = runtime;
159
- this.#store = runtime.state.openSyncKeyedStore({
160
- namespace: DISCUSSION_BINDINGS_NAMESPACE,
161
- maxEntries: MAX_DISCUSSION_BINDINGS,
162
- overflowPolicy: "reject-new"
163
- });
164
- for (const entry of this.#store.entries()) this.#index(entry.key, entry.value);
165
- }
166
- get(sessionKey) {
167
- return this.#store.lookup(sessionKey);
168
- }
169
- hasCapacity(sessionKey) {
170
- return this.get(sessionKey) !== void 0 || this.#store.entries().length < MAX_DISCUSSION_BINDINGS;
171
- }
172
- getByChannel(serverBaseUrl, channelId) {
173
- const key = channelKey(serverBaseUrl, channelId);
174
- const sessionKey = this.#sessionByChannel.get(key);
175
- if (!sessionKey) return;
176
- const binding = this.get(sessionKey);
177
- if (!binding) {
178
- this.#sessionByChannel.delete(key);
179
- return;
180
- }
181
- return {
182
- sessionKey,
183
- binding
184
- };
185
- }
186
- set(sessionKey, binding) {
187
- const previous = this.get(sessionKey);
188
- this.#store.register(sessionKey, binding);
189
- if (previous) this.#unindex(sessionKey, previous);
190
- this.#index(sessionKey, binding);
191
- }
192
- delete(sessionKey) {
193
- const previous = this.get(sessionKey);
194
- const deleted = this.#store.delete(sessionKey);
195
- if (deleted && previous) this.#unindex(sessionKey, previous);
196
- return deleted;
197
- }
198
- getByDiscussionSession(sideSessionKey) {
199
- const sessionKey = this.#mainByDiscussionSession.get(sideSessionKey);
200
- if (!sessionKey) return;
201
- const binding = this.get(sessionKey);
202
- return binding ? {
203
- sessionKey,
204
- binding
205
- } : void 0;
206
- }
207
- entries() {
208
- return this.#store.entries().map((entry) => ({
209
- sessionKey: entry.key,
210
- binding: entry.value
211
- }));
212
- }
213
- #index(sessionKey, binding) {
214
- this.#sessionByChannel.set(channelKey(binding.serverBaseUrl, binding.channelId), sessionKey);
215
- const sideSessionKey = discussionSessionKey({
216
- runtime: this.#runtime,
217
- agentId: binding.agentId,
218
- mainSessionKey: sessionKey,
219
- sessionId: binding.sessionId,
220
- accountId: binding.accountId,
221
- serverBaseUrl: binding.serverBaseUrl,
222
- channelId: binding.channelId,
223
- externalRef: binding.externalRef
224
- });
225
- if (sideSessionKey) this.#mainByDiscussionSession.set(sideSessionKey, sessionKey);
226
- }
227
- #unindex(sessionKey, binding) {
228
- this.#sessionByChannel.delete(channelKey(binding.serverBaseUrl, binding.channelId));
229
- const sideSessionKey = discussionSessionKey({
230
- runtime: this.#runtime,
231
- agentId: binding.agentId,
232
- mainSessionKey: sessionKey,
233
- sessionId: binding.sessionId,
234
- accountId: binding.accountId,
235
- serverBaseUrl: binding.serverBaseUrl,
236
- channelId: binding.channelId,
237
- externalRef: binding.externalRef
238
- });
239
- if (sideSessionKey) this.#mainByDiscussionSession.delete(sideSessionKey);
240
- }
241
- };
242
- function getClickClackDiscussionBindingStore(runtime) {
243
- const existing = storesByRuntime$1.get(runtime);
244
- if (existing) return existing;
245
- const created = new ClickClackDiscussionBindingStore(runtime);
246
- storesByRuntime$1.set(runtime, created);
247
- return created;
248
- }
249
- //#endregion
250
- //#region extensions/clickclack/src/discussions/eligibility.ts
251
- function discussionAccounts(cfg) {
252
- return listEnabledClickClackAccounts(cfg).filter((account) => account.configured && account.discussions.enabled);
253
- }
254
- function normalizedServerBaseUrl(account) {
255
- return account.baseUrl.replace(/\/+$/u, "");
256
- }
257
- /** Resolves the sole live account and rejects bindings pinned to an older destination. */
258
- function resolveDiscussionBindingAccount(cfg, binding) {
259
- const accounts = discussionAccounts(cfg);
260
- if (accounts.length !== 1) return { state: "unavailable" };
261
- const account = accounts[0];
262
- if (!account) return { state: "unavailable" };
263
- if (account.accountId !== binding.accountId || normalizedServerBaseUrl(account) !== binding.serverBaseUrl || account.discussions.workspace !== binding.workspaceRef || binding.credentialFingerprint !== void 0 && discussionCredentialFingerprint(account.token) !== binding.credentialFingerprint) return {
264
- state: "stale",
265
- account
266
- };
267
- return {
268
- state: "active",
269
- account
270
- };
271
- }
272
- /** Public embed/open URLs for one bound discussion channel. */
273
- function discussionInfoForBinding(binding, account) {
274
- const baseUrl = normalizedServerBaseUrl(account);
275
- return {
276
- state: "open",
277
- embedUrl: `${baseUrl}/embed/channel/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}?openclawHostTheme=1`,
278
- openUrl: `${baseUrl}/app/${encodeURIComponent(binding.workspaceRouteId)}/${encodeURIComponent(binding.channelRouteId)}`
279
- };
280
- }
281
- //#endregion
282
- //#region extensions/clickclack/src/discussions/revoked-channel-store.ts
283
- const REVOKED_CHANNELS_NAMESPACE = "discussion-revoked-channels";
284
- const MAX_REVOKED_CHANNELS = 1e5;
285
- const storesByRuntime = /* @__PURE__ */ new WeakMap();
286
- function revokedChannelKey(params) {
287
- return [params.serverBaseUrl.replace(/\/+$/u, ""), params.channelId].join("\0");
288
- }
289
- function getStore(runtime) {
290
- const existing = storesByRuntime.get(runtime);
291
- if (existing) return existing;
292
- const created = runtime.state.openSyncKeyedStore({
293
- namespace: REVOKED_CHANNELS_NAMESPACE,
294
- maxEntries: MAX_REVOKED_CHANNELS,
295
- overflowPolicy: "reject-new"
296
- });
297
- storesByRuntime.set(runtime, created);
298
- return created;
299
- }
300
- /** Records managed ownership before its live binding is released. */
301
- function markClickClackDiscussionChannelRevoked(runtime, binding) {
302
- const value = {
303
- accountId: binding.accountId,
304
- serverBaseUrl: binding.serverBaseUrl,
305
- channelId: binding.channelId,
306
- revokedAt: Date.now()
307
- };
308
- getStore(runtime).register(revokedChannelKey(value), value);
309
- }
310
- function markClickClackDiscussionChannelIdentityRevoked(params) {
311
- const value = {
312
- accountId: params.accountId,
313
- serverBaseUrl: params.serverBaseUrl.replace(/\/+$/u, ""),
314
- channelId: params.channelId,
315
- revokedAt: Date.now()
316
- };
317
- getStore(params.runtime).register(revokedChannelKey(value), value);
318
- }
319
- function clearClickClackDiscussionChannelRevoked(params) {
320
- getStore(params.runtime).delete(revokedChannelKey(params));
321
- }
322
- /** Distinguishes a released managed channel from a genuinely ordinary channel. */
323
- function isClickClackDiscussionChannelRevoked(params) {
324
- return Boolean(getStore(params.runtime).lookup(revokedChannelKey(params)));
325
- }
326
- //#endregion
327
- //#region extensions/clickclack/src/discussions/routing.ts
328
- function resolveClickClackDiscussionRoute(params) {
329
- if (isClickClackDiscussionChannelRevoked(params)) return { state: "revoked" };
330
- const matched = getClickClackDiscussionBindingStore(params.runtime).getByChannel(params.serverBaseUrl, params.channelId);
331
- if (!matched) return { state: hasPendingDiscussionOpenForDestination(params) ? "revoked" : "unbound" };
332
- if (matched.binding.accountId !== params.accountId) return { state: "revoked" };
333
- if (matched.binding.serverBaseUrl !== params.serverBaseUrl.replace(/\/+$/u, "")) return { state: "revoked" };
334
- if (matched.binding.archived) return { state: "revoked" };
335
- if (resolveDiscussionBindingAccount(params.config, matched.binding).state !== "active") return { state: "revoked" };
336
- if (!bindingMatchesActiveSessionIncarnation(params.runtime, matched.sessionKey, matched.binding)) return { state: "revoked" };
337
- const sessionKey = discussionSessionKey({
338
- runtime: params.runtime,
339
- agentId: matched.binding.agentId,
340
- mainSessionKey: matched.sessionKey,
341
- sessionId: matched.binding.sessionId,
342
- accountId: params.accountId,
343
- serverBaseUrl: matched.binding.serverBaseUrl,
344
- channelId: matched.binding.channelId,
345
- externalRef: matched.binding.externalRef
346
- });
347
- if (!sessionKey) return { state: "revoked" };
348
- return {
349
- state: "active",
350
- route: {
351
- agentId: matched.binding.agentId,
352
- sessionKey,
353
- systemPrompt: [
354
- "You are the side agent for a ClickClack discussion attached to an OpenClaw session.",
355
- `The main session key is ${matched.sessionKey}.`,
356
- "Observe it with sessions_history and session_status (using changesSince for incremental checks).",
357
- "Use sessions_send to relay or steer the main session only when the humans in this discussion ask you to.",
358
- "These session tools are host-scoped to the attached main session; do not attempt session discovery or alternate targets."
359
- ].join(" ")
360
- }
361
- };
362
- }
363
- //#endregion
364
- //#region extensions/clickclack/src/group-policy.ts
365
- /**
366
- * Resolves the effective group policy for a ClickClack channel.
367
- *
368
- * Lookup order:
369
- * 1. Exact channel ID in `groups`
370
- * 2. Wildcard `'*'` entry in `groups`
371
- * 3. Account-level `requireMention` / `mentionPatterns`
372
- * 4. Backward-compatible default: { requireMention: false, mentionPatterns: [] }
373
- */
374
- function resolveClickClackGroupPolicy(params) {
375
- const { account, channelId } = params;
376
- const accountPolicy = {
377
- requireMention: account.requireMention === true,
378
- mentionPatterns: account.mentionPatterns ?? []
379
- };
380
- const wildcard = account.groups?.["*"];
381
- const channelKey = channelId?.trim();
382
- const exact = channelKey ? account.groups?.[channelKey] : void 0;
383
- return {
384
- requireMention: exact?.requireMention ?? wildcard?.requireMention ?? accountPolicy.requireMention,
385
- mentionPatterns: exact?.mentionPatterns ?? wildcard?.mentionPatterns ?? accountPolicy.mentionPatterns
386
- };
387
- }
388
- //#endregion
389
- //#region extensions/clickclack/src/mention-facts.ts
390
- /**
391
- * Detects whether a ClickClack group message contains a direct mention of the
392
- * current account.
393
- *
394
- * Pure helper – no side effects, no runtime imports.
395
- */
396
- const CLICKCLACK_MENTION_PATTERN = /(?:^|[^a-z0-9_@-])@([a-z0-9][a-z0-9_-]{1,31})(?![a-z0-9_-])/giu;
397
- function buildLocalMentionRegexes(params) {
398
- if (params.mentionPatterns.length === 0) return [];
399
- const cfg = params.cfg;
400
- return buildMentionRegexes({
401
- ...cfg,
402
- messages: {
403
- ...cfg?.messages,
404
- groupChat: {
405
- ...cfg?.messages?.groupChat,
406
- mentionPatterns: params.mentionPatterns
407
- }
408
- }
409
- }, void 0, {
410
- provider: "clickclack",
411
- conversationId: params.channelId
412
- });
413
- }
414
- function resolveMentionHandles(body) {
415
- return [...body.matchAll(CLICKCLACK_MENTION_PATTERN)].map((match) => match[1]?.toLowerCase()).filter((handle) => Boolean(handle));
416
- }
417
- /**
418
- * Builds mention facts for a ClickClack message.
419
- *
420
- * Rules:
421
- * - DMs always have canDetectMention: false, wasMentioned: false
422
- * (DMs bypass mention gating).
423
- * - Group messages: canDetectMention: true when body text is available.
424
- * - Checks the message body against shared and account-local mention patterns.
425
- * - If botHandle is provided and the message body contains its ClickClack
426
- * `@handle`, treat it as a mention.
427
- * - Plain display names do not count unless explicitly configured as a pattern.
428
- */
429
- function resolveClickClackMentionFacts(params) {
430
- const { isDirect, body, mentionPatterns, botHandle, cfg, agentId, channelId } = params;
431
- if (isDirect) return {
432
- canDetectMention: false,
433
- wasMentioned: false
434
- };
435
- if (!body) return {
436
- canDetectMention: true,
437
- wasMentioned: false,
438
- hasAnyMention: false
439
- };
440
- const sharedMentionRegexes = buildMentionRegexes(cfg, agentId, {
441
- provider: "clickclack",
442
- conversationId: channelId
443
- });
444
- const localMentionRegexes = buildLocalMentionRegexes({
445
- cfg,
446
- mentionPatterns,
447
- channelId
448
- });
449
- const mentionRegexes = [...sharedMentionRegexes, ...localMentionRegexes];
450
- const bodyForRegex = normalizeMentionText(body);
451
- const hasConfiguredMention = mentionRegexes.some((regex) => regex.test(bodyForRegex));
452
- const mentionHandles = resolveMentionHandles(body);
453
- const normalizedBotHandle = botHandle?.replace(/^@/u, "").trim().toLowerCase();
454
- return {
455
- canDetectMention: true,
456
- wasMentioned: (normalizedBotHandle ? mentionHandles.includes(normalizedBotHandle) : false) || hasConfiguredMention,
457
- hasAnyMention: mentionHandles.length > 0 || hasConfiguredMention
458
- };
459
- }
460
- //#endregion
461
- //#region extensions/clickclack/src/runtime.ts
462
- /**
463
- * Runtime store for host-provided OpenClaw services used by the ClickClack
464
- * bundled plugin.
465
- */
466
- const { setRuntime: setClickClackRuntime, getRuntime: getClickClackRuntime } = createPluginRuntimeStore({
467
- pluginId: "clickclack",
468
- errorMessage: "ClickClack runtime not initialized"
469
- });
470
- //#endregion
471
- //#region extensions/clickclack/src/target.ts
472
- /**
473
- * Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
474
- */
475
- function parseClickClackTarget(raw) {
476
- const value = raw.trim();
477
- if (!value) throw new Error("ClickClack target is required");
478
- const [prefix, ...rest] = value.split(":");
479
- const body = rest.join(":").trim();
480
- if (prefix === "channel" && body) return {
481
- chatType: "group",
482
- kind: "channel",
483
- id: body
484
- };
485
- if (prefix === "thread" && body) return {
486
- chatType: "group",
487
- kind: "thread",
488
- id: body
489
- };
490
- if (prefix === "dm" && body) return {
491
- chatType: "direct",
492
- kind: "dm",
493
- id: body
494
- };
495
- if (!body) return {
496
- chatType: "group",
497
- kind: "channel",
498
- id: value
499
- };
500
- throw new Error(`Unsupported ClickClack target: ${raw}`);
501
- }
502
- /** Formats a parsed ClickClack target back into canonical target syntax. */
503
- function buildClickClackTarget(target) {
504
- return `${target.kind}:${target.id}`;
505
- }
506
- /** Normalizes user-entered ClickClack target text for channel routing. */
507
- function normalizeClickClackTarget(raw) {
508
- return buildClickClackTarget(parseClickClackTarget(raw));
509
- }
510
- /** Reports whether a target string can be offered to the ClickClack parser. */
511
- function looksLikeClickClackTarget(raw) {
512
- return /^(channel|thread|dm):/i.test(raw.trim()) || raw.trim().length > 0;
513
- }
514
- //#endregion
515
- //#region extensions/clickclack/src/access.ts
516
- /**
517
- * Maps ClickClack senders and conversations onto the shared channel ingress
518
- * allowlist/command authorization contract.
519
- */
520
- const CHANNEL_ID$2 = "clickclack";
521
- function normalizeClickClackUserId(value) {
522
- const trimmed = value.trim();
523
- if (!trimmed) return null;
524
- const withoutProvider = trimmed.replace(/^(clickclack|cc):/i, "").trim();
525
- return withoutProvider.match(/^dm:(.+)$/i)?.[1]?.trim() || withoutProvider || null;
526
- }
527
- const clickClackIngressIdentity = {
528
- key: "user-id",
529
- normalizeEntry: normalizeClickClackUserId,
530
- normalizeSubject: normalizeClickClackUserId,
531
- isWildcardEntry: (entry) => normalizeClickClackUserId(entry) === "*",
532
- entryIdPrefix: "clickclack-user"
533
- };
534
- function resolveAccountAgentRoute(params) {
535
- const runtime = getClickClackRuntime();
536
- const peer = {
537
- kind: params.isDirect ? "direct" : "channel",
538
- id: params.target
539
- };
540
- const route = runtime.channel.routing.resolveAgentRoute({
541
- cfg: params.cfg,
542
- channel: CHANNEL_ID$2,
543
- accountId: params.account.accountId,
544
- peer
545
- });
546
- const agentId = normalizeAgentId(params.account.agentId ?? route.agentId);
547
- if (agentId === route.agentId) return route;
548
- const dmScope = params.cfg.session?.dmScope ?? "main";
549
- const sessionKey = runtime.channel.routing.buildAgentSessionKey({
550
- agentId,
551
- mainKey: params.cfg.session?.mainKey,
552
- channel: CHANNEL_ID$2,
553
- accountId: params.account.accountId,
554
- peer,
555
- dmScope,
556
- identityLinks: params.cfg.session?.identityLinks
557
- });
558
- const mainSessionKey = runtime.channel.routing.buildAgentSessionKey({
559
- agentId,
560
- mainKey: params.cfg.session?.mainKey,
561
- channel: CHANNEL_ID$2,
562
- accountId: params.account.accountId,
563
- dmScope: "main"
564
- });
565
- return {
566
- ...route,
567
- agentId,
568
- dmScope,
569
- sessionKey,
570
- mainSessionKey,
571
- lastRoutePolicy: sessionKey === mainSessionKey ? "main" : "session"
572
- };
573
- }
574
- function resolvePreparedInboundRoute(params) {
575
- const runtime = getClickClackRuntime();
576
- const isDirect = Boolean(params.message.direct_conversation_id);
577
- const target = buildClickClackTarget(isDirect ? {
578
- chatType: "direct",
579
- kind: "dm",
580
- id: params.message.author_id
581
- } : {
582
- chatType: "group",
583
- kind: "channel",
584
- id: params.message.channel_id ?? ""
585
- });
586
- const accountRoute = resolveAccountAgentRoute({
587
- cfg: params.config,
588
- account: params.account,
589
- target,
590
- isDirect
591
- });
592
- const discussionResolution = !isDirect && params.message.channel_id ? resolveClickClackDiscussionRoute({
593
- runtime,
594
- config: params.config,
595
- accountId: params.account.accountId,
596
- serverBaseUrl: params.account.baseUrl,
597
- workspaceId: params.message.workspace_id,
598
- channelId: params.message.channel_id
599
- }) : { state: "unbound" };
600
- const discussionRoute = discussionResolution.state === "active" ? discussionResolution.route : void 0;
601
- return {
602
- isDirect,
603
- target,
604
- route: discussionRoute ? {
605
- ...accountRoute,
606
- agentId: discussionRoute.agentId,
607
- sessionKey: discussionRoute.sessionKey,
608
- lastRoutePolicy: "session"
609
- } : accountRoute,
610
- discussionRoute,
611
- revoked: discussionResolution.state === "revoked"
612
- };
613
- }
614
- /**
615
- * Resolves whether a ClickClack message should enter the agent pipeline and
616
- * whether its command-style body may run tools.
617
- */
618
- async function resolveClickClackInboundAccess(params) {
619
- const runtime = getClickClackRuntime();
620
- const cfg = params.config;
621
- const preparedRoute = resolvePreparedInboundRoute(params);
622
- const shouldCheckCommand = runtime.channel.commands.shouldComputeCommandAuthorized(params.message.body, cfg);
623
- const effectiveGroupPolicy = resolveClickClackGroupPolicy({
624
- account: params.account,
625
- channelId: params.message.channel_id
626
- });
627
- const mentionFacts = resolveClickClackMentionFacts({
628
- isDirect: preparedRoute.isDirect,
629
- body: params.message.body,
630
- mentionPatterns: effectiveGroupPolicy.mentionPatterns,
631
- botHandle: params.account.botHandle,
632
- cfg,
633
- agentId: preparedRoute.route.agentId,
634
- channelId: params.message.channel_id
635
- });
636
- const allowTextCommands = params.account.replyMode === "agent" && runtime.channel.commands.shouldHandleTextCommands({
637
- cfg,
638
- surface: CHANNEL_ID$2,
639
- commandSource: "text"
640
- });
641
- const resolved = await resolveStableChannelMessageIngress({
642
- channelId: CHANNEL_ID$2,
643
- accountId: params.account.accountId,
644
- identity: clickClackIngressIdentity,
645
- cfg,
646
- subject: { stableId: params.message.author_id },
647
- conversation: {
648
- kind: preparedRoute.isDirect ? "direct" : "group",
649
- id: preparedRoute.isDirect ? params.message.direct_conversation_id ?? params.message.author_id : params.message.channel_id ?? params.message.thread_root_id
650
- },
651
- allowFrom: params.account.allowFrom,
652
- dmPolicy: "allowlist",
653
- groupPolicy: "allowlist",
654
- mentionFacts,
655
- policy: { activation: {
656
- requireMention: effectiveGroupPolicy.requireMention,
657
- allowTextCommands
658
- } },
659
- command: shouldCheckCommand ? {
660
- cfg,
661
- modeWhenAccessGroupsOff: "configured"
662
- } : false
663
- });
664
- return {
665
- shouldDispatch: !preparedRoute.revoked && resolved.ingress.admission === "dispatch",
666
- commandAuthorized: resolved.commandAccess.requested ? resolved.commandAccess.authorized : resolved.senderAccess.allowed,
667
- requireMention: effectiveGroupPolicy.requireMention,
668
- mentionFacts,
669
- preparedRoute
670
- };
671
- }
672
- //#endregion
673
- //#region extensions/clickclack/src/command-menu.ts
674
- const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
675
- const CLICKCLACK_MAX_COMMANDS = 100;
676
- const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
677
- const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
678
- function truncateCodePoints(value, maxLength) {
679
- return Array.from(value).slice(0, maxLength).join("");
680
- }
681
- function commandArgsHint(spec) {
682
- if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
683
- return spec.acceptsArgs ? "[args]" : "";
684
- }
685
- function errorStatus(error) {
686
- if (!error || typeof error !== "object" || !("status" in error)) return;
687
- const status = error.status;
688
- return typeof status === "number" ? status : void 0;
689
- }
690
- function mapNativeCommandSpecsToClickClackMenu(specs, log) {
691
- const commands = [];
692
- const seen = /* @__PURE__ */ new Set();
693
- const invalidNames = /* @__PURE__ */ new Set();
694
- for (const spec of specs) {
695
- if (spec.isAlias) continue;
696
- const command = spec.name.trim().toLowerCase();
697
- if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
698
- invalidNames.add(spec.name.trim() || "<empty>");
699
- continue;
700
- }
701
- if (seen.has(command)) continue;
702
- seen.add(command);
703
- const description = spec.description.trim() || command;
704
- commands.push({
705
- command,
706
- description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
707
- args_hint: commandArgsHint(spec)
708
- });
709
- }
710
- if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
711
- return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
712
- }
713
- async function syncClickClackCommandMenu(params) {
714
- try {
715
- const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
716
- await params.client.setBotCommands(commands);
717
- } catch (error) {
718
- const status = errorStatus(error);
719
- if (status === 403) {
720
- params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
721
- return;
722
- }
723
- if (status === 404) {
724
- params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
725
- return;
726
- }
727
- params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
728
- }
729
- }
730
- //#endregion
731
- //#region extensions/clickclack/src/activity.ts
732
- /**
733
- * Publishes agent activity (streamed commentary + tool progress) into
734
- * ClickClack as durable `agent_commentary` / `agent_tool` message rows,
735
- * coalesced so one logical step becomes one row instead of a row per frame.
736
- *
737
- * Ported from the clickglass agent-bridge sidecar, adapted from gateway
738
- * websocket frames to the in-process `replyOptions.onItemEvent` seam:
739
- *
740
- * - Commentary/reasoning arrives as cumulative text snapshots per item id.
741
- * Each segment becomes one durable row: POSTed when the segment starts
742
- * streaming and PATCHed (debounced) as the snapshot grows, so prose
743
- * interleaves chronologically with tool rows.
744
- * - Tool/step items can emit several frames (start/update/complete) for the
745
- * same call, sometimes with lane-prefixed ids (`tool:X`, `command:X`).
746
- * Normalize the prefix away to one key per call, POST one row on the first
747
- * frame, and PATCH it when a later frame carries a strictly longer body.
748
- */
749
- /** Debounce window for PATCHing streaming commentary snapshots. */
750
- const CLICKCLACK_COMMENTARY_FLUSH_MS = 700;
751
- /** Item kinds rendered as agent_tool rows; everything else is commentary. */
752
- const TOOL_ITEM_KINDS = /* @__PURE__ */ new Set([
753
- "tool",
754
- "command",
755
- "command_output",
756
- "patch",
757
- "search",
758
- "api"
759
- ]);
760
- /** Item kinds that never become durable rows (ephemeral or plumbing lanes). */
761
- const SKIPPED_ITEM_KINDS = /* @__PURE__ */ new Set(["lifecycle"]);
762
- /** Item kinds emitted as cumulative commentary snapshots. */
763
- const STREAMING_COMMENTARY_ITEM_KINDS = /* @__PURE__ */ new Set([
764
- "preamble",
765
- "commentary",
766
- "analysis",
767
- "thinking",
768
- "reasoning"
769
- ]);
770
- /** Provider-specific reasoning lanes normalized into one ClickClack label. */
771
- const THINKING_ITEM_KINDS = /* @__PURE__ */ new Set([
772
- "analysis",
773
- "thinking",
774
- "reasoning"
775
- ]);
776
- function normalizedItemKind(payload) {
777
- return payload.kind?.trim().toLowerCase() ?? "";
778
- }
779
- function commentaryBody(payload) {
780
- const text = payload.progressText?.trim() || payload.summary?.trim() || payload.meta?.trim() || "";
781
- if (!text) return "";
782
- const kind = normalizedItemKind(payload);
783
- if (THINKING_ITEM_KINDS.has(kind)) return `**Thinking**\n\n${text}`;
784
- return text;
785
- }
786
- function activityBody(payload) {
787
- const line = buildChannelProgressDraftLine({
788
- event: "item",
789
- itemId: payload.itemId,
790
- toolCallId: payload.toolCallId,
791
- itemKind: payload.kind,
792
- title: payload.title,
793
- name: payload.name,
794
- phase: payload.phase,
795
- status: payload.status,
796
- summary: payload.summary,
797
- progressText: payload.progressText,
798
- meta: payload.meta
799
- })?.text?.trim();
800
- if (line) return line;
801
- const head = payload.name?.trim() || payload.title?.trim();
802
- const text = payload.progressText?.trim() || payload.summary?.trim();
803
- if (head && text) return `**${head}**\n\n${text}`;
804
- if (text) return text;
805
- if (head) return head;
806
- return payload.status?.trim() || payload.kind?.trim() || "";
807
- }
808
- /**
809
- * Creates a per-turn activity publisher. Publishing is best-effort: transport
810
- * failures are reported through `onError` and never interrupt the reply turn.
811
- */
812
- function createClickClackActivityPublisher(params) {
813
- const flushMs = params.flushMs ?? CLICKCLACK_COMMENTARY_FLUSH_MS;
814
- const commentaryByItem = /* @__PURE__ */ new Map();
815
- const toolRows = /* @__PURE__ */ new Map();
816
- let provenance;
817
- let chain = Promise.resolve();
818
- const enqueue = (work) => {
819
- chain = chain.then(work).catch((error) => {
820
- params.onError?.(error);
821
- });
822
- return chain;
823
- };
824
- const postRow = (kind, body) => params.client.createActivityMessage({
825
- channelId: params.target.channelId,
826
- conversationId: params.target.conversationId,
827
- body,
828
- kind,
829
- turnId: params.turnId,
830
- provenance
831
- });
832
- const flushCommentary = (segmentKey) => {
833
- const segment = commentaryByItem.get(segmentKey);
834
- if (!segment) return Promise.resolve();
835
- if (segment.timer) {
836
- clearTimeout(segment.timer);
837
- segment.timer = void 0;
838
- }
839
- if (!segment.dirty || !segment.body.trim()) return Promise.resolve();
840
- segment.dirty = false;
841
- const body = segment.body;
842
- return enqueue(async () => {
843
- if (segment.messageId) {
844
- await params.client.updateMessageBody(segment.messageId, body);
845
- return;
846
- }
847
- const posted = await postRow("agent_commentary", body);
848
- segment.messageId = posted.id;
849
- });
850
- };
851
- const flushAllCommentary = () => {
852
- const flushes = [...commentaryByItem.keys()].map((key) => flushCommentary(key));
853
- return Promise.all(flushes).then(() => void 0);
854
- };
855
- const handleCommentary = (payload) => {
856
- const body = commentaryBody(payload);
857
- if (!body.trim()) return;
858
- const key = payload.itemId?.trim() || "turn";
859
- let segment = commentaryByItem.get(key);
860
- if (!segment) {
861
- segment = {
862
- body: "",
863
- dirty: false
864
- };
865
- commentaryByItem.set(key, segment);
866
- }
867
- if (body.length < segment.body.length || body === segment.body) return;
868
- segment.body = body;
869
- segment.dirty = true;
870
- if (!segment.timer) segment.timer = setTimeout(() => {
871
- segment.timer = void 0;
872
- flushCommentary(key);
873
- }, flushMs);
874
- };
875
- const toolRowKey = (payload) => {
876
- const toolCallId = payload.toolCallId?.trim();
877
- if (toolCallId) return toolCallId;
878
- return (payload.itemId?.trim() ?? "").replace(/^(tool|command):/, "");
879
- };
880
- const handleDiscreteItem = (payload) => {
881
- const body = activityBody(payload);
882
- if (!body) return;
883
- const kind = TOOL_ITEM_KINDS.has(payload.kind?.trim().toLowerCase() ?? "") ? "agent_tool" : "agent_commentary";
884
- flushAllCommentary();
885
- const key = `${kind}:${toolRowKey(payload)}`;
886
- const existing = toolRows.get(key);
887
- if (!existing || !toolRowKey(payload)) {
888
- const row = { body };
889
- if (toolRowKey(payload)) toolRows.set(key, row);
890
- enqueue(async () => {
891
- const posted = await postRow(kind, row.body);
892
- row.messageId = posted.id;
893
- row.sentBody = row.body;
894
- });
895
- return;
896
- }
897
- if (body.length <= existing.body.length) return;
898
- existing.body = body;
899
- enqueue(async () => {
900
- if (existing.messageId && existing.body !== existing.sentBody) {
901
- await params.client.updateMessageBody(existing.messageId, existing.body);
902
- existing.sentBody = existing.body;
903
- }
904
- });
905
- };
906
- return {
907
- onItemEvent: (payload) => {
908
- const kind = normalizedItemKind(payload);
909
- if (STREAMING_COMMENTARY_ITEM_KINDS.has(kind)) {
910
- handleCommentary(payload);
911
- return;
912
- }
913
- if (SKIPPED_ITEM_KINDS.has(kind)) return;
914
- handleDiscreteItem(payload);
915
- },
916
- setProvenance: (next) => {
917
- provenance = next;
918
- },
919
- finalize: async () => {
920
- await flushAllCommentary();
921
- await chain;
922
- }
923
- };
924
- }
925
- //#endregion
926
- //#region extensions/clickclack/src/outbound.ts
927
- /**
928
- * Outbound ClickClack delivery helpers for channel messages, thread replies,
929
- * and direct messages.
930
- */
931
- const CLICKCLACK_MAX_UPLOAD_BYTES = 64 * 1024 * 1024;
932
- const CLICKCLACK_FORMAT_PROFILE = FormatCapabilityProfile.define({
933
- mechanism: "markdown",
934
- chunk: {
935
- limit: 1024 * 1024,
936
- unit: "bytes"
937
- }
938
- });
939
- function renderClickClackMarkdown(markdown) {
940
- return renderMarkdownWithMarkers({
941
- text: markdown,
942
- styles: [],
943
- links: []
944
- }, {
945
- styleMarkers: {},
946
- escapeText: (text) => text
947
- }, CLICKCLACK_FORMAT_PROFILE);
948
- }
949
- async function createTargetMessage(params) {
950
- const parsed = parseClickClackTarget(params.to);
951
- const explicitThreadId = params.threadId == null ? "" : String(params.threadId);
952
- const replyToId = params.replyToId == null ? "" : String(params.replyToId);
953
- if (explicitThreadId || parsed.kind === "thread") {
954
- const rootId = explicitThreadId || parsed.id;
955
- await params.onPlatformSendDispatch?.();
956
- return await params.client.createThreadReply(rootId, params.text, {
957
- provenance: params.provenance,
958
- nonce: params.nonce
959
- });
960
- }
961
- if (parsed.kind === "dm") {
962
- await params.onPlatformSendDispatch?.();
963
- const dm = await params.client.createDirectConversation(params.workspaceId, [parsed.id]);
964
- return await params.client.createDirectMessage(dm.id, params.text, {
965
- quotedMessageId: replyToId || void 0,
966
- nonce: params.nonce
967
- });
968
- }
969
- const channelId = await resolveChannelId(params.client, params.workspaceId, parsed.id);
970
- await params.onPlatformSendDispatch?.();
971
- return await params.client.createChannelMessage(channelId, params.text, {
972
- provenance: params.provenance,
973
- quotedMessageId: replyToId || void 0,
974
- nonce: params.nonce
975
- });
976
- }
977
- function durableDeliveryDigest(params) {
978
- if (!params.deliveryQueueId) return;
979
- if (!Number.isSafeInteger(params.deliveryPartIndex) || (params.deliveryPartIndex ?? -1) < 0) throw new Error("ClickClack durable delivery requires a stable delivery part index");
980
- return createHash("sha256").update(`${params.deliveryQueueId}\n${params.deliveryPartIndex}`).digest("hex");
981
- }
982
- function mediaDeliveryNonces(params) {
983
- const digest = durableDeliveryDigest(params);
984
- if (!digest) return {};
985
- return {
986
- message: `openclaw-media:${digest}`,
987
- upload: `openclaw-upload:${digest}`
988
- };
989
- }
990
- function textDeliveryNonce(params) {
991
- const digest = durableDeliveryDigest(params);
992
- return digest ? `openclaw-text:${digest}` : void 0;
993
- }
994
- function createDispatchOnce(onPlatformSendDispatch) {
995
- let dispatched = false;
996
- return async () => {
997
- if (dispatched) return;
998
- await onPlatformSendDispatch?.();
999
- dispatched = true;
1000
- };
1001
- }
1002
- async function attachUploadRetrySafe(params) {
1003
- try {
1004
- await params.client.attachUpload(params.messageId, params.uploadId);
1005
- } catch (firstError) {
1006
- try {
1007
- if ((await params.client.message(params.messageId)).attachments?.some((attachment) => attachment.id === params.uploadId)) return;
1008
- } catch {}
1009
- try {
1010
- await params.client.attachUpload(params.messageId, params.uploadId);
1011
- } catch {
1012
- throw firstError;
1013
- }
1014
- }
1015
- }
1016
- function createOutboundContext(params) {
1017
- const account = resolveClickClackAccount({
1018
- cfg: params.cfg,
1019
- accountId: params.accountId
1020
- });
1021
- return {
1022
- account,
1023
- client: createClickClackClient({
1024
- baseUrl: account.apiEndpoint,
1025
- token: account.token,
1026
- correlationId: params.correlationId
1027
- })
1028
- };
1029
- }
1030
- /**
1031
- * Sends visible text to a normalized ClickClack target and returns the created
1032
- * message id, or undefined when sanitization removes all content.
1033
- */
1034
- async function sendClickClackText(params) {
1035
- const text = renderClickClackMarkdown(sanitizeAssistantVisibleText(params.text));
1036
- if (!text) return;
1037
- const { account, client } = createOutboundContext(params);
1038
- const workspaceId = await resolveWorkspaceId(client, account.workspace);
1039
- const dispatch = createDispatchOnce(params.onPlatformSendDispatch);
1040
- return (await createTargetMessage({
1041
- client,
1042
- workspaceId,
1043
- to: params.to,
1044
- text,
1045
- threadId: params.threadId,
1046
- replyToId: params.replyToId,
1047
- provenance: params.provenance,
1048
- nonce: textDeliveryNonce({
1049
- deliveryQueueId: params.deliveryQueueId,
1050
- deliveryPartIndex: params.deliveryPartIndex
1051
- }),
1052
- onPlatformSendDispatch: dispatch
1053
- })).id;
1054
- }
1055
- /** Resolves, uploads, sends, then attaches one file to a ClickClack message. */
1056
- async function sendClickClackMedia(params) {
1057
- const nonces = mediaDeliveryNonces({
1058
- deliveryQueueId: params.deliveryQueueId,
1059
- deliveryPartIndex: params.deliveryPartIndex
1060
- });
1061
- const preloadedMedia = nonces.upload ? void 0 : await loadOutboundMediaFromUrl(params.mediaUrl, {
1062
- maxBytes: CLICKCLACK_MAX_UPLOAD_BYTES,
1063
- mediaAccess: params.mediaAccess,
1064
- mediaLocalRoots: params.mediaLocalRoots,
1065
- mediaReadFile: params.mediaReadFile
1066
- });
1067
- const { account, client } = createOutboundContext(params);
1068
- const workspaceId = await resolveWorkspaceId(client, account.workspace);
1069
- const persistedUpload = nonces.upload ? await client.findUploadByNonce({
1070
- workspaceId,
1071
- nonce: nonces.upload
1072
- }) : void 0;
1073
- const dispatch = createDispatchOnce(params.onPlatformSendDispatch);
1074
- let upload = persistedUpload;
1075
- let mediaFilename = preloadedMedia?.fileName?.trim();
1076
- if (!upload) {
1077
- const media = preloadedMedia ?? await loadOutboundMediaFromUrl(params.mediaUrl, {
1078
- maxBytes: CLICKCLACK_MAX_UPLOAD_BYTES,
1079
- mediaAccess: params.mediaAccess,
1080
- mediaLocalRoots: params.mediaLocalRoots,
1081
- mediaReadFile: params.mediaReadFile
1082
- });
1083
- const filename = media.fileName?.trim() || "attachment";
1084
- mediaFilename = filename;
1085
- const contentType = media.contentType?.trim() || "application/octet-stream";
1086
- await dispatch();
1087
- upload = await client.createUpload({
1088
- workspaceId,
1089
- buffer: media.buffer,
1090
- filename,
1091
- contentType,
1092
- ...nonces.upload ? { nonce: nonces.upload } : {}
1093
- });
1094
- }
1095
- const text = renderClickClackMarkdown(sanitizeAssistantVisibleText(params.text)) || mediaFilename || upload.filename || "attachment";
1096
- const message = await createTargetMessage({
1097
- client,
1098
- workspaceId,
1099
- to: params.to,
1100
- text,
1101
- threadId: params.threadId,
1102
- replyToId: params.replyToId,
1103
- nonce: nonces.message,
1104
- onPlatformSendDispatch: dispatch
1105
- });
1106
- await attachUploadRetrySafe({
1107
- client,
1108
- messageId: message.id,
1109
- uploadId: upload.id
1110
- });
1111
- return message.id;
1112
- }
1113
- function collectReconciliationMediaUrls(ctx) {
1114
- const planned = ctx.renderedBatchPlan?.items[0]?.mediaUrls;
1115
- if (planned?.length) return planned.map((url) => url.trim()).filter(Boolean);
1116
- const payload = ctx.payloads[0];
1117
- return [payload?.mediaUrl, ...payload?.mediaUrls ?? []].map((url) => url?.trim()).filter((url) => Boolean(url));
1118
- }
1119
- /**
1120
- * Completes an unknown durable send through ClickClack's message/upload nonces.
1121
- * Media recovery never rereads the original source after restart.
1122
- */
1123
- async function reconcileClickClackUnknownSend(ctx) {
1124
- if (ctx.payloads.length !== 1 || (ctx.renderedBatchPlan?.items.length ?? 1) !== 1) return {
1125
- status: "unresolved",
1126
- error: "ClickClack reconciliation requires exactly one payload"
1127
- };
1128
- const mediaUrls = collectReconciliationMediaUrls(ctx);
1129
- const { account, client } = createOutboundContext({
1130
- cfg: ctx.cfg,
1131
- accountId: ctx.accountId
1132
- });
1133
- const workspaceId = await resolveWorkspaceId(client, account.workspace);
1134
- const effectiveReplyToId = ctx.effectiveReplyToId !== void 0 ? ctx.effectiveReplyToId : ctx.replyToMode === "off" ? void 0 : ctx.replyToId;
1135
- const payload = ctx.payloads[0];
1136
- const caption = ctx.renderedBatchPlan?.items[0]?.text ?? payload?.text ?? "";
1137
- if (mediaUrls.length === 0) {
1138
- const nonce = textDeliveryNonce({
1139
- deliveryQueueId: ctx.queueId,
1140
- deliveryPartIndex: 0
1141
- });
1142
- if (!nonce || !sanitizeAssistantVisibleText(caption)) return { status: "not_sent" };
1143
- const message = await client.findMessageByNonce({
1144
- workspaceId,
1145
- nonce
1146
- });
1147
- if (!message) return { status: "not_sent" };
1148
- const receipt = createMessageReceiptFromOutboundResults({
1149
- results: [{
1150
- channel: "clickclack",
1151
- messageId: message.id
1152
- }],
1153
- threadId: ctx.threadId == null ? void 0 : String(ctx.threadId),
1154
- replyToId: effectiveReplyToId ?? void 0,
1155
- kind: "text"
1156
- });
1157
- return {
1158
- status: "sent",
1159
- messageId: message.id,
1160
- receipt
1161
- };
1162
- }
1163
- const parts = await Promise.all(mediaUrls.map(async (_mediaUrl, index) => {
1164
- const nonces = mediaDeliveryNonces({
1165
- deliveryQueueId: ctx.queueId,
1166
- deliveryPartIndex: index
1167
- });
1168
- if (!nonces.upload || !nonces.message) throw new Error("ClickClack durable media nonces were not derived");
1169
- const [upload, message] = await Promise.all([client.findUploadByNonce({
1170
- workspaceId,
1171
- nonce: nonces.upload
1172
- }), client.findMessageByNonce({
1173
- workspaceId,
1174
- nonce: nonces.message
1175
- })]);
1176
- return {
1177
- upload,
1178
- message
1179
- };
1180
- }));
1181
- for (const part of parts) {
1182
- if (part.message && !part.upload) return {
1183
- status: "unresolved",
1184
- error: `ClickClack message ${part.message.id} exists without its nonce-keyed upload`,
1185
- retryable: false
1186
- };
1187
- if (!part.message) return { status: "not_sent" };
1188
- }
1189
- const messageIds = [];
1190
- for (const part of parts) {
1191
- const message = part.message;
1192
- const upload = part.upload;
1193
- if (!message || !upload) throw new Error("ClickClack reconciliation state changed unexpectedly");
1194
- if (!message.attachments?.some((attachment) => attachment.id === upload.id)) await attachUploadRetrySafe({
1195
- client,
1196
- messageId: message.id,
1197
- uploadId: upload.id
1198
- });
1199
- messageIds.push(message.id);
1200
- }
1201
- const receipt = createMessageReceiptFromOutboundResults({
1202
- results: messageIds.map((messageId) => ({
1203
- channel: "clickclack",
1204
- messageId
1205
- })),
1206
- threadId: ctx.threadId == null ? void 0 : String(ctx.threadId),
1207
- replyToId: effectiveReplyToId ?? void 0,
1208
- kind: "media"
1209
- });
1210
- const messageId = messageIds.at(-1);
1211
- return {
1212
- status: "sent",
1213
- ...messageId ? { messageId } : {},
1214
- receipt
1215
- };
1216
- }
1217
- //#endregion
1218
- //#region extensions/clickclack/src/inbound.ts
1219
- const CHANNEL_ID$1 = "clickclack";
1220
- const CLICKCLACK_MESSAGE_ID_PATTERN = /^msg_[0-9a-hjkmnp-tv-z]{26}$/u;
1221
- function hasClickClackReplyMedia(payload) {
1222
- return Boolean(payload.mediaUrl?.trim() || payload.mediaUrls?.some((mediaUrl) => typeof mediaUrl === "string" && mediaUrl.trim()));
1223
- }
1224
- function resolveClickClackAgentRunId(messageId) {
1225
- return CLICKCLACK_MESSAGE_ID_PATTERN.test(messageId) ? `${CHANNEL_ID$1}:${messageId}` : void 0;
1226
- }
1227
- async function dispatchModelReply(params) {
1228
- const runtime = getClickClackRuntime();
1229
- const text = (await runtime.llm.complete({
1230
- agentId: params.route.agentId,
1231
- model: params.account.model,
1232
- purpose: "clickclack bot reply",
1233
- systemPrompt: params.account.systemPrompt,
1234
- messages: [{
1235
- role: "user",
1236
- content: params.message.body
1237
- }]
1238
- })).text.trim();
1239
- if (!text) {
1240
- runtime.logging.getChildLogger({
1241
- plugin: "clickclack",
1242
- feature: "model-reply"
1243
- }).warn(`[${params.account.accountId}] ClickClack model reply produced no sendable text`);
1244
- return;
1245
- }
1246
- await sendClickClackText({
1247
- cfg: params.cfg,
1248
- accountId: params.account.accountId,
1249
- to: params.target,
1250
- text,
1251
- threadId: params.message.parent_message_id ? params.message.thread_root_id : void 0,
1252
- replyToId: params.message.id,
1253
- correlationId: params.correlationId
1254
- });
1255
- }
1256
- /**
1257
- * Dispatches one already-fetched ClickClack message through the configured
1258
- * reply mode for its account.
1259
- */
1260
- async function handleClickClackInbound(params) {
1261
- const runtime = getClickClackRuntime();
1262
- const message = params.message;
1263
- const access = params.access ?? await resolveClickClackInboundAccess({
1264
- account: params.account,
1265
- config: params.config,
1266
- message
1267
- });
1268
- if (!access.shouldDispatch) return;
1269
- const conversationId = message.channel_id || message.direct_conversation_id;
1270
- if (!conversationId) return;
1271
- const { discussionRoute, isDirect, route, target } = access.preparedRoute;
1272
- if (params.account.replyMode === "model" && !discussionRoute) {
1273
- await dispatchModelReply({
1274
- account: params.account,
1275
- cfg: params.config,
1276
- message,
1277
- route,
1278
- target,
1279
- correlationId: params.correlationId
1280
- });
1281
- return;
1282
- }
1283
- let turnProvenance;
1284
- let activity;
1285
- if (params.account.agentActivity && (message.channel_id || message.direct_conversation_id)) activity = createClickClackActivityPublisher({
1286
- client: createClickClackClient({
1287
- baseUrl: params.account.apiEndpoint,
1288
- token: params.account.token,
1289
- correlationId: params.correlationId
1290
- }),
1291
- target: message.channel_id ? { channelId: message.channel_id } : { conversationId: message.direct_conversation_id },
1292
- turnId: message.id,
1293
- onError: (error) => {
1294
- runtime.logging.getChildLogger({
1295
- plugin: "clickclack",
1296
- feature: "agent-activity"
1297
- }).warn(`clickclack activity publish failed: ${String(error)}`);
1298
- }
1299
- });
1300
- const senderName = message.author?.display_name || message.author_id;
1301
- const body = createChannelInboundEnvelopeBuilder({
1302
- cfg: params.config,
1303
- route
1304
- })({
1305
- channel: "ClickClack",
1306
- from: senderName,
1307
- timestamp: new Date(message.created_at),
1308
- body: message.body
1309
- });
1310
- const ctxPayload = runtime.channel.inbound.buildContext({
1311
- channel: CHANNEL_ID$1,
1312
- accountId: route.accountId ?? params.account.accountId,
1313
- messageId: message.id,
1314
- messageIdFull: message.id,
1315
- timestamp: new Date(message.created_at).getTime(),
1316
- from: target,
1317
- sender: {
1318
- id: message.author_id,
1319
- name: senderName
1320
- },
1321
- conversation: {
1322
- kind: isDirect ? "direct" : "group",
1323
- id: conversationId,
1324
- label: isDirect ? senderName : message.channel_id,
1325
- threadId: message.parent_message_id ? message.thread_root_id : void 0,
1326
- nativeChannelId: conversationId
1327
- },
1328
- route: {
1329
- agentId: route.agentId,
1330
- dmScope: route.dmScope,
1331
- accountId: route.accountId,
1332
- routeSessionKey: route.sessionKey
1333
- },
1334
- reply: {
1335
- to: target,
1336
- originatingTo: target,
1337
- replyToId: message.id,
1338
- messageThreadId: message.parent_message_id ? message.thread_root_id : void 0,
1339
- threadParentId: message.parent_message_id ? message.thread_root_id : void 0
1340
- },
1341
- message: {
1342
- body,
1343
- bodyForAgent: message.body,
1344
- rawBody: message.body,
1345
- commandBody: message.body
1346
- },
1347
- access: {
1348
- commands: { authorized: access.commandAuthorized },
1349
- mentions: access.mentionFacts
1350
- },
1351
- extra: {
1352
- GroupChannel: message.channel_id,
1353
- ...discussionRoute ? { GroupSystemPrompt: discussionRoute.systemPrompt } : {}
1354
- }
1355
- });
1356
- const runId = resolveClickClackAgentRunId(message.id);
1357
- const activityReplyOptions = activity ? {
1358
- onModelSelected: (ctx) => {
1359
- turnProvenance = {
1360
- model: ctx.provider && ctx.model ? `${ctx.provider}/${ctx.model}` : ctx.model,
1361
- thinking: ctx.thinkLevel
1362
- };
1363
- activity?.setProvenance(turnProvenance);
1364
- },
1365
- onItemEvent: activity.onItemEvent,
1366
- commentaryProgressEnabled: true,
1367
- suppressDefaultToolProgressMessages: true,
1368
- allowProgressCallbacksWhenSourceDeliverySuppressed: true
1369
- } : void 0;
1370
- const dispatchPromise = runtime.channel.inbound.dispatch({
1371
- cfg: params.config,
1372
- channel: CHANNEL_ID$1,
1373
- accountId: params.account.accountId,
1374
- route: {
1375
- agentId: route.agentId,
1376
- dmScope: route.dmScope,
1377
- sessionKey: route.sessionKey
1378
- },
1379
- ctxPayload,
1380
- toolsAllow: params.account.toolsAllow,
1381
- replyOptions: runId || activityReplyOptions ? {
1382
- ...runId ? { runId } : {},
1383
- ...activityReplyOptions
1384
- } : void 0,
1385
- delivery: {
1386
- deliver: async (payload) => {
1387
- if (hasClickClackReplyMedia(payload)) throw new Error("ClickClack media reply requires durable delivery");
1388
- const text = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
1389
- if (!text.trim()) return;
1390
- await sendClickClackText({
1391
- cfg: params.config,
1392
- accountId: params.account.accountId,
1393
- to: target,
1394
- text,
1395
- threadId: message.parent_message_id ? message.thread_root_id : void 0,
1396
- replyToId: message.id,
1397
- provenance: turnProvenance,
1398
- correlationId: params.correlationId
1399
- });
1400
- },
1401
- durable: (payload) => {
1402
- if (!hasClickClackReplyMedia(payload)) return false;
1403
- const threadId = message.parent_message_id ? message.thread_root_id : void 0;
1404
- return {
1405
- to: target,
1406
- threadId,
1407
- replyToId: message.id,
1408
- requiredCapabilities: deriveDurableFinalDeliveryRequirements({
1409
- payload,
1410
- threadId,
1411
- replyToId: message.id,
1412
- reconcileUnknownSend: true
1413
- })
1414
- };
1415
- },
1416
- onError: (error) => {
1417
- throw error instanceof Error ? error : /* @__PURE__ */ new Error(`clickclack dispatch failed: ${String(error)}`);
1418
- }
1419
- },
1420
- replyPipeline: {},
1421
- record: { onRecordError: (error) => {
1422
- throw error instanceof Error ? error : /* @__PURE__ */ new Error(`clickclack session record failed: ${String(error)}`);
1423
- } }
1424
- });
1425
- try {
1426
- await dispatchPromise;
1427
- } finally {
1428
- await activity?.finalize();
1429
- }
1430
- }
1431
- //#endregion
1432
- //#region extensions/clickclack/src/gateway.ts
1433
- const CLICKCLACK_EVENT_PAGE_LIMIT = 500;
1434
- function payloadString(event, key) {
1435
- const value = event.payload?.[key];
1436
- return typeof value === "string" ? value : "";
1437
- }
1438
- function eventCorrelationId(event) {
1439
- return normalizeClickClackCorrelationId(event.payload?.correlation_id);
1440
- }
1441
- async function resolveEventMessage(params) {
1442
- const messageId = payloadString(params.event, "message_id");
1443
- if (!messageId) return null;
1444
- const directConversationId = payloadString(params.event, "direct_conversation_id");
1445
- if (directConversationId && typeof params.event.seq === "number") return (await params.client.directMessages(directConversationId, params.event.seq - 1, 10)).find((message) => message.id === messageId) ?? null;
1446
- if (params.event.type === "thread.reply_created") {
1447
- const rootId = payloadString(params.event, "root_message_id");
1448
- if (!rootId) return null;
1449
- return (await params.client.thread(rootId)).replies.find((message) => message.id === messageId) ?? null;
1450
- }
1451
- 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;
1452
- return null;
1453
- }
1454
- function decodeSocketMessage(data) {
1455
- if (typeof data === "string") return data;
1456
- if (Buffer.isBuffer(data)) return data.toString("utf8");
1457
- if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
1458
- return Buffer.concat(data).toString("utf8");
1459
- }
1460
- function parseSocketEvent(data) {
1461
- try {
1462
- return JSON.parse(decodeSocketMessage(data));
1463
- } catch {
1464
- return null;
1465
- }
1466
- }
1467
- async function processEvent(params) {
1468
- if (params.event.type !== "message.created" && params.event.type !== "thread.reply_created") return;
1469
- if (payloadString(params.event, "author_id") === params.botUserId) return;
1470
- const correlationId = eventCorrelationId(params.event);
1471
- const message = await resolveEventMessage({
1472
- client: correlationId ? createClickClackClient({
1473
- baseUrl: params.account.apiEndpoint,
1474
- token: params.account.token,
1475
- correlationId
1476
- }) : params.client,
1477
- event: params.event
1478
- });
1479
- if (!message || message.author_id === params.botUserId) return;
1480
- if (message.author?.kind === "bot") return;
1481
- const access = await resolveClickClackInboundAccess({
1482
- account: params.account,
1483
- config: params.config,
1484
- message
1485
- });
1486
- if (!access.shouldDispatch) {
1487
- params.log?.info(`[${params.account.accountId}] skipped ClickClack message before agent dispatch: kind=${message.direct_conversation_id ? "dm" : "group"} requireMention=${access.requireMention ?? "unknown"} wasMentioned=${access.mentionFacts.wasMentioned} hasAnyMention=${access.mentionFacts.hasAnyMention ?? "unknown"} commandAuthorized=${access.commandAuthorized}`);
1488
- return;
1489
- }
1490
- await handleClickClackInbound({
1491
- account: params.account,
1492
- config: params.config,
1493
- message,
1494
- access,
1495
- ...correlationId ? { correlationId } : {}
1496
- });
1497
- }
1498
- async function drainEventBacklog(params) {
1499
- let afterCursor = params.afterCursor;
1500
- while (!params.abortSignal.aborted) {
1501
- const events = (await params.client.eventPage(params.workspaceId, {
1502
- afterCursor,
1503
- limit: CLICKCLACK_EVENT_PAGE_LIMIT
1504
- })).events;
1505
- for (const event of events) {
1506
- if (params.abortSignal.aborted) return afterCursor;
1507
- if (!event.cursor || event.cursor === afterCursor) throw new Error("ClickClack event backlog returned a non-advancing cursor");
1508
- await params.onEvent(event);
1509
- afterCursor = event.cursor;
1510
- }
1511
- if (events.length === 0) return afterCursor;
1512
- }
1513
- return afterCursor;
1514
- }
1515
- async function startClickClackGatewayAccount(ctx) {
1516
- const configuredAccount = resolveClickClackAccount({
1517
- cfg: ctx.cfg,
1518
- accountId: ctx.account.accountId
1519
- });
1520
- if (!configuredAccount.configured) throw new Error(`ClickClack is not configured for account "${configuredAccount.accountId}"`);
1521
- const client = createClickClackClient({
1522
- baseUrl: configuredAccount.apiEndpoint,
1523
- token: configuredAccount.token
1524
- });
1525
- const workspaceId = await resolveWorkspaceId(client, configuredAccount.workspace);
1526
- const me = await client.me();
1527
- const account = {
1528
- ...configuredAccount,
1529
- workspace: workspaceId,
1530
- botUserId: configuredAccount.botUserId ?? me.id,
1531
- botHandle: me.handle
1532
- };
1533
- const processIncomingEvent = (event) => processEvent({
1534
- account,
1535
- config: ctx.cfg,
1536
- client,
1537
- event,
1538
- botUserId: account.botUserId,
1539
- log: ctx.log
1540
- });
1541
- if (account.commandMenu) await syncClickClackCommandMenu({
1542
- cfg: ctx.cfg,
1543
- client,
1544
- log: ctx.log
1545
- });
1546
- ctx.setStatus({
1547
- accountId: account.accountId,
1548
- running: true,
1549
- configured: true,
1550
- enabled: account.enabled,
1551
- baseUrl: account.baseUrl
1552
- });
1553
- let afterCursor = "";
1554
- let initialized = false;
1555
- try {
1556
- while (!ctx.abortSignal.aborted) {
1557
- if (!initialized) {
1558
- const page = await client.eventPage(workspaceId, { includeTail: true });
1559
- if (page.tailCursor !== void 0) afterCursor = page.tailCursor;
1560
- else for (const event of page.events) afterCursor = event.cursor || afterCursor;
1561
- initialized = true;
1562
- } else afterCursor = await drainEventBacklog({
1563
- client,
1564
- workspaceId,
1565
- afterCursor,
1566
- abortSignal: ctx.abortSignal,
1567
- onEvent: processIncomingEvent
1568
- });
1569
- if (ctx.abortSignal.aborted) break;
1570
- const socket = client.websocket(workspaceId, afterCursor);
1571
- await new Promise((resolve) => {
1572
- let settled = false;
1573
- let closing = false;
1574
- let loggedMessageFailure = false;
1575
- let messageQueue = Promise.resolve();
1576
- let removeAbortListener;
1577
- const finishSocketCycle = () => {
1578
- if (settled) return;
1579
- settled = true;
1580
- removeAbortListener?.();
1581
- removeAbortListener = void 0;
1582
- resolve();
1583
- };
1584
- const finishAfterQueuedMessages = () => {
1585
- messageQueue.then(() => finishSocketCycle(), () => finishSocketCycle());
1586
- };
1587
- const reconnectAfterMessageFailure = (error) => {
1588
- if (settled || ctx.abortSignal.aborted) return;
1589
- if (!loggedMessageFailure) {
1590
- loggedMessageFailure = true;
1591
- ctx.log?.warn?.(`[${account.accountId}] ClickClack event processing failed; reconnecting: ${error instanceof Error ? error.message : formatErrorMessage(error)}`);
1592
- }
1593
- if (!closing) {
1594
- closing = true;
1595
- socket.close();
1596
- }
1597
- };
1598
- const abort = () => {
1599
- socket.close();
1600
- finishSocketCycle();
1601
- };
1602
- ctx.abortSignal.addEventListener("abort", abort, { once: true });
1603
- removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
1604
- socket.on("message", (data) => {
1605
- if (closing || settled) return;
1606
- messageQueue = messageQueue.then(async () => {
1607
- const event = parseSocketEvent(data);
1608
- if (!event) {
1609
- ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
1610
- return;
1611
- }
1612
- await processIncomingEvent(event);
1613
- afterCursor = event.cursor || afterCursor;
1614
- });
1615
- messageQueue.catch(reconnectAfterMessageFailure);
1616
- });
1617
- socket.on("close", () => {
1618
- closing = true;
1619
- finishAfterQueuedMessages();
1620
- });
1621
- socket.on("error", (error) => {
1622
- if (settled || ctx.abortSignal.aborted) {
1623
- finishSocketCycle();
1624
- return;
1625
- }
1626
- if (closing) return;
1627
- ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
1628
- closing = true;
1629
- socket.close();
1630
- });
1631
- });
1632
- if (!ctx.abortSignal.aborted) try {
1633
- await sleepWithAbort(account.reconnectMs, ctx.abortSignal);
1634
- } catch (error) {
1635
- if (!ctx.abortSignal.aborted) throw error;
1636
- }
1637
- }
1638
- } finally {
1639
- ctx.setStatus({
1640
- accountId: account.accountId,
1641
- running: false
1642
- });
1643
- }
1644
- }
1645
- //#endregion
1646
- //#region extensions/clickclack/src/channel.ts
1647
- /**
1648
- * ClickClack channel plugin definition: target parsing, account config, status,
1649
- * gateway startup, and outbound delivery wiring.
1650
- */
1651
- const CHANNEL_ID = CLICKCLACK_CHANNEL_ID;
1652
- const clickClackMessageAdapter = defineChannelMessageAdapter({
1653
- id: CHANNEL_ID,
1654
- durableFinal: {
1655
- capabilities: {
1656
- text: true,
1657
- media: true,
1658
- replyTo: true,
1659
- thread: true,
1660
- messageSendingHooks: true,
1661
- reconcileUnknownSend: true
1662
- },
1663
- reconcileUnknownSendKinds: {
1664
- text: true,
1665
- media: true
1666
- },
1667
- reconcileUnknownSend: reconcileClickClackUnknownSend
1668
- },
1669
- send: {
1670
- text: async (ctx) => {
1671
- const messageId = await sendClickClackText({
1672
- cfg: ctx.cfg,
1673
- accountId: ctx.accountId,
1674
- to: ctx.to,
1675
- text: ctx.text,
1676
- threadId: ctx.threadId,
1677
- replyToId: ctx.replyToId,
1678
- deliveryQueueId: ctx.deliveryQueueId,
1679
- deliveryPartIndex: ctx.deliveryPartIndex,
1680
- onPlatformSendDispatch: ctx.onPlatformSendDispatch
1681
- });
1682
- const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
1683
- const replyToId = ctx.replyToId ?? void 0;
1684
- return {
1685
- ...messageId ? { messageId } : {},
1686
- receipt: createMessageReceiptFromOutboundResults({
1687
- results: messageId ? [{
1688
- channel: CHANNEL_ID,
1689
- messageId
1690
- }] : [],
1691
- threadId,
1692
- replyToId,
1693
- kind: "text"
1694
- })
1695
- };
1696
- },
1697
- media: async (ctx) => {
1698
- const messageId = await sendClickClackMedia({
1699
- cfg: ctx.cfg,
1700
- accountId: ctx.accountId,
1701
- to: ctx.to,
1702
- text: ctx.text,
1703
- mediaUrl: ctx.mediaUrl,
1704
- mediaAccess: ctx.mediaAccess,
1705
- mediaLocalRoots: ctx.mediaLocalRoots,
1706
- mediaReadFile: ctx.mediaReadFile,
1707
- threadId: ctx.threadId,
1708
- replyToId: ctx.replyToId,
1709
- deliveryQueueId: ctx.deliveryQueueId,
1710
- deliveryPartIndex: ctx.deliveryPartIndex,
1711
- onPlatformSendDispatch: ctx.onPlatformSendDispatch
1712
- });
1713
- const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
1714
- const replyToId = ctx.replyToId ?? void 0;
1715
- return {
1716
- messageId,
1717
- receipt: createMessageReceiptFromOutboundResults({
1718
- results: [{
1719
- channel: CHANNEL_ID,
1720
- messageId
1721
- }],
1722
- threadId,
1723
- replyToId,
1724
- kind: "media"
1725
- })
1726
- };
1727
- }
1728
- }
1729
- });
1730
- /**
1731
- * Channel plugin instance registered by the bundled ClickClack entry.
1732
- */
1733
- const clickClackPlugin = createChatChannelPlugin({
1734
- base: {
1735
- id: CHANNEL_ID,
1736
- meta: clickClackMeta,
1737
- capabilities: {
1738
- chatTypes: ["direct", "group"],
1739
- threads: true,
1740
- blockStreaming: true
1741
- },
1742
- reload: { configPrefixes: ["channels.clickclack"] },
1743
- configSchema: clickClackConfigSchema,
1744
- config: clickClackConfigAdapter,
1745
- setup: clickClackSetupAdapter,
1746
- setupContract: clickClackSetupContract,
1747
- setupWizard: clickClackSetupWizard,
1748
- secrets: {
1749
- secretTargetRegistryEntries,
1750
- collectRuntimeConfigAssignments
1751
- },
1752
- messaging: {
1753
- targetPrefixes: ["clickclack", "cc"],
1754
- normalizeTarget: normalizeClickClackTarget,
1755
- inferTargetChatType: ({ to }) => parseClickClackTarget(to).chatType,
1756
- targetResolver: {
1757
- looksLikeId: looksLikeClickClackTarget,
1758
- hint: "<channel:name|dm:usr_id|thread:msg_id>"
1759
- },
1760
- resolveOutboundSessionRoute: ({ cfg, agentId, accountId, target, replyToId, threadId, currentSessionKey }) => {
1761
- const parsed = parseClickClackTarget(target);
1762
- return buildThreadAwareOutboundSessionRoute({
1763
- route: buildChannelOutboundSessionRoute({
1764
- cfg,
1765
- agentId,
1766
- channel: CHANNEL_ID,
1767
- accountId,
1768
- recipientSessionExact: parsed.kind === "dm",
1769
- peer: {
1770
- kind: parsed.chatType === "direct" ? "direct" : "channel",
1771
- id: buildClickClackTarget(parsed)
1772
- },
1773
- chatType: parsed.chatType,
1774
- from: `clickclack:${accountId ?? DEFAULT_ACCOUNT_ID}`,
1775
- to: buildClickClackTarget(parsed)
1776
- }),
1777
- replyToId,
1778
- threadId: threadId ?? (parsed.kind === "thread" ? parsed.id : void 0),
1779
- currentSessionKey,
1780
- useSuffix: false,
1781
- canRecoverCurrentThread: () => true
1782
- });
1783
- },
1784
- resolveSessionConversation: ({ rawId }) => {
1785
- const parsed = parseClickClackTarget(rawId);
1786
- if (parsed.kind === "dm") return null;
1787
- return {
1788
- id: parsed.id,
1789
- threadId: parsed.kind === "thread" ? parsed.id : void 0,
1790
- baseConversationId: parsed.id,
1791
- parentConversationCandidates: [parsed.id]
1792
- };
1793
- }
1794
- },
1795
- status: createComputedAccountStatusAdapter({
1796
- defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
1797
- buildChannelSummary: ({ snapshot }) => ({
1798
- ok: snapshot.configured,
1799
- label: snapshot.configured ? "configured" : "missing config",
1800
- detail: snapshot.baseUrl ?? ""
1801
- }),
1802
- resolveAccountSnapshot: ({ account }) => ({
1803
- accountId: account.accountId,
1804
- name: account.name,
1805
- enabled: account.enabled,
1806
- configured: account.configured,
1807
- baseUrl: account.baseUrl
1808
- })
1809
- }),
1810
- gateway: { startAccount: startClickClackGatewayAccount },
1811
- message: clickClackMessageAdapter
1812
- },
1813
- outbound: {
1814
- base: { deliveryMode: "direct" },
1815
- attachedResults: {
1816
- channel: CHANNEL_ID,
1817
- sendText: async ({ cfg, to, text, accountId, threadId, replyToId, deliveryQueueId, deliveryPartIndex, onPlatformSendDispatch }) => {
1818
- return { messageId: await sendClickClackText({
1819
- cfg,
1820
- accountId,
1821
- to,
1822
- text,
1823
- threadId,
1824
- replyToId,
1825
- deliveryQueueId,
1826
- deliveryPartIndex,
1827
- onPlatformSendDispatch
1828
- }) ?? "" };
1829
- },
1830
- sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId, threadId, replyToId, deliveryQueueId, deliveryPartIndex, onPlatformSendDispatch }) => {
1831
- if (!mediaUrl) throw new Error("ClickClack media send requires mediaUrl");
1832
- return { messageId: await sendClickClackMedia({
1833
- cfg,
1834
- accountId,
1835
- to,
1836
- text,
1837
- mediaUrl,
1838
- mediaAccess,
1839
- mediaLocalRoots,
1840
- mediaReadFile,
1841
- threadId,
1842
- replyToId,
1843
- deliveryQueueId,
1844
- deliveryPartIndex,
1845
- onPlatformSendDispatch
1846
- }) };
1847
- }
1848
- }
1849
- }
1850
- });
1851
- //#endregion
1852
- export { truncateDiscussionDisplayTitle as C, reserveDiscussionBindingGeneration as D, recordPendingDiscussionOpen as E, slugifyDiscussionLabel as S, listPendingDiscussionOpens as T, discussionCredentialFingerprint as _, setClickClackRuntime as a, isDiscussionSessionKey as b, markClickClackDiscussionChannelIdentityRevoked as c, discussionInfoForBinding as d, normalizedServerBaseUrl as f, getClickClackDiscussionBindingStore as g, bindingMatchesSessionIncarnation as h, getClickClackRuntime as i, markClickClackDiscussionChannelRevoked as l, bindingMatchesActiveSessionIncarnation as m, buildClickClackTarget as n, clearClickClackDiscussionChannelRevoked as o, resolveDiscussionBindingAccount as p, parseClickClackTarget as r, isClickClackDiscussionChannelRevoked as s, clickClackPlugin as t, discussionAccounts as u, discussionExternalRef as v, clearDiscussionBindingGeneration as w, resolveDiscussionLabel as x, fallbackDiscussionLabel as y };