@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.
- package/README.md +0 -152
- package/dist/.setup/channel-DGqOQ-o2.mjs +1172 -0
- package/dist/api.js +1 -2
- package/dist/channel-plugin-api.js +1 -1
- package/dist/index.js +1 -3
- package/dist/runtime-api.js +3 -4
- package/node_modules/ws/lib/permessage-deflate.js +3 -1
- package/node_modules/ws/package.json +1 -1
- package/openclaw.plugin.json +21 -129
- package/package.json +6 -94
- package/dist/channel-CECIpaS0.js +0 -1852
- package/dist/doctor-contract-api.js +0 -48
- package/dist/runtime-api-CZ1rmHjv.js +0 -1029
- package/dist/secret-contract-BKkzzjBA.js +0 -35
- package/dist/secret-contract-api.js +0 -2
- package/dist/setup-claim-CP-QZ5bt.js +0 -136
- package/dist/setup-entry.js +0 -15
- package/dist/setup-plugin-api.js +0 -19
- package/dist/setup-surface-DAy8vDJK.js +0 -1131
|
@@ -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 };
|