@openclaw/clickclack 2026.7.2-beta.1 → 2026.7.2-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/api.js +2 -1
- package/dist/{channel-Ds6d_3nx.js → channel-CEYthZ9N.js} +169 -463
- package/dist/channel-plugin-api.js +1 -1
- package/dist/runtime-api.js +2 -1
- package/dist/secret-contract-BKkzzjBA.js +35 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/setup-claim-BUWvZjtu.js +114 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +18 -0
- package/dist/setup-surface-CNjp76Xx.js +911 -0
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +12 -0
- package/package.json +18 -6
|
@@ -1,158 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
|
|
4
|
-
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
|
|
5
|
-
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
|
|
6
|
-
import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
7
|
-
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
1
|
+
import { a as createClickClackClient, c as CLICKCLACK_CHANNEL_ID, d as DEFAULT_ACCOUNT_ID, i as resolveWorkspaceId, l as clickClackConfigAdapter, m as resolveClickClackAccount, n as clickClackSetupAdapter, o as normalizeClickClackCorrelationId, r as resolveChannelId, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-CNjp76Xx.js";
|
|
2
|
+
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-BKkzzjBA.js";
|
|
8
3
|
import { buildChannelOutboundSessionRoute, buildThreadAwareOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
9
4
|
import { buildChannelProgressDraftLine, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, deriveDurableFinalDeliveryRequirements } from "openclaw/plugin-sdk/channel-outbound";
|
|
10
|
-
import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
11
5
|
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
|
|
12
|
-
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
13
|
-
import { z } from "zod";
|
|
14
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
|
+
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
15
8
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
16
9
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
10
|
+
import { listNativeCommandSpecsForConfig } from "openclaw/plugin-sdk/native-command-registry";
|
|
11
|
+
import { createChannelInboundEnvelopeBuilder } from "openclaw/plugin-sdk/channel-inbound";
|
|
19
12
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
20
13
|
import { createHash } from "node:crypto";
|
|
21
14
|
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
22
15
|
import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
|
|
23
|
-
//#region extensions/clickclack/src/accounts.ts
|
|
24
|
-
/**
|
|
25
|
-
* Resolves ClickClack account configuration from root channel config, named
|
|
26
|
-
* account overrides, and secret-provider references.
|
|
27
|
-
*/
|
|
28
|
-
const DEFAULT_RECONNECT_MS = 1500;
|
|
29
|
-
const MIN_RECONNECT_MS = 100;
|
|
30
|
-
const MAX_RECONNECT_MS = 6e4;
|
|
31
|
-
const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
|
|
32
|
-
normalizeAccountId,
|
|
33
|
-
hasImplicitDefaultAccount: (cfg) => {
|
|
34
|
-
const channel = cfg.channels?.clickclack;
|
|
35
|
-
return Boolean(channel?.baseUrl?.trim() && hasConfiguredAccountValue(channel.token) && channel.workspace?.trim());
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
function resolveMergedClickClackAccountConfig(cfg, accountId) {
|
|
39
|
-
return resolveMergedAccountConfig({
|
|
40
|
-
channelConfig: cfg.channels?.clickclack,
|
|
41
|
-
accounts: cfg.channels?.clickclack?.accounts,
|
|
42
|
-
accountId,
|
|
43
|
-
omitKeys: ["defaultAccount"],
|
|
44
|
-
normalizeAccountId
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function resolveClickClackToken(params) {
|
|
48
|
-
const resolved = resolveSecretInputString({
|
|
49
|
-
value: params.value,
|
|
50
|
-
path: params.accountId === DEFAULT_ACCOUNT_ID ? "channels.clickclack.token" : `channels.clickclack.accounts.${params.accountId}.token`,
|
|
51
|
-
defaults: params.cfg.secrets?.defaults,
|
|
52
|
-
mode: "inspect"
|
|
53
|
-
});
|
|
54
|
-
if (resolved.status !== "available") {
|
|
55
|
-
if (resolved.status === "configured_unavailable" && resolved.ref.source === "env") {
|
|
56
|
-
const providerConfig = params.cfg.secrets?.providers?.[resolved.ref.provider];
|
|
57
|
-
if (providerConfig) {
|
|
58
|
-
if (providerConfig.source !== "env") throw new Error(`Secret provider "${resolved.ref.provider}" has source "${providerConfig.source}" but ref requests "env".`);
|
|
59
|
-
if (providerConfig.allowlist && !providerConfig.allowlist.includes(resolved.ref.id)) throw new Error(`Environment variable "${resolved.ref.id}" is not allowlisted in secrets.providers.${resolved.ref.provider}.allowlist.`);
|
|
60
|
-
} else if (resolved.ref.provider !== resolveDefaultSecretProviderAlias({ secrets: params.cfg.secrets }, "env")) throw new Error(`Secret provider "${resolved.ref.provider}" is not configured (ref: env:${resolved.ref.provider}:${resolved.ref.id}).`);
|
|
61
|
-
return normalizeSecretInputString((params.env ?? process.env)[resolved.ref.id]) ?? "";
|
|
62
|
-
}
|
|
63
|
-
return "";
|
|
64
|
-
}
|
|
65
|
-
return normalizeResolvedSecretInputString({
|
|
66
|
-
value: resolved.value,
|
|
67
|
-
path: "channels.clickclack.token"
|
|
68
|
-
}) ?? "";
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Builds the normalized account snapshot used by gateway, outbound delivery,
|
|
72
|
-
* status reporting, and channel routing.
|
|
73
|
-
*/
|
|
74
|
-
function resolveClickClackAccount(params) {
|
|
75
|
-
const accountId = normalizeAccountId(params.accountId);
|
|
76
|
-
const merged = resolveMergedClickClackAccountConfig(params.cfg, accountId);
|
|
77
|
-
const enabled = params.cfg.channels?.clickclack?.enabled !== false && merged.enabled !== false;
|
|
78
|
-
const baseUrl = merged.baseUrl?.trim().replace(/\/$/, "") ?? "";
|
|
79
|
-
const token = resolveClickClackToken({
|
|
80
|
-
cfg: params.cfg,
|
|
81
|
-
value: merged.token,
|
|
82
|
-
accountId,
|
|
83
|
-
env: params.env
|
|
84
|
-
});
|
|
85
|
-
const workspace = merged.workspace?.trim() ?? "";
|
|
86
|
-
return {
|
|
87
|
-
accountId,
|
|
88
|
-
enabled,
|
|
89
|
-
configured: Boolean(baseUrl && token && workspace),
|
|
90
|
-
name: normalizeOptionalString(merged.name),
|
|
91
|
-
baseUrl,
|
|
92
|
-
token,
|
|
93
|
-
workspace,
|
|
94
|
-
botUserId: normalizeOptionalString(merged.botUserId),
|
|
95
|
-
agentId: normalizeOptionalString(merged.agentId),
|
|
96
|
-
replyMode: merged.replyMode === "model" ? "model" : "agent",
|
|
97
|
-
model: normalizeOptionalString(merged.model),
|
|
98
|
-
systemPrompt: normalizeOptionalString(merged.systemPrompt),
|
|
99
|
-
timeoutSeconds: merged.timeoutSeconds,
|
|
100
|
-
toolsAllow: merged.toolsAllow,
|
|
101
|
-
defaultTo: merged.defaultTo?.trim() || "channel:general",
|
|
102
|
-
allowFrom: merged.allowFrom ?? ["*"],
|
|
103
|
-
reconnectMs: resolveIntegerOption(merged.reconnectMs, DEFAULT_RECONNECT_MS, {
|
|
104
|
-
min: MIN_RECONNECT_MS,
|
|
105
|
-
max: MAX_RECONNECT_MS
|
|
106
|
-
}),
|
|
107
|
-
agentActivity: merged.agentActivity === true,
|
|
108
|
-
config: {
|
|
109
|
-
...merged,
|
|
110
|
-
allowFrom: merged.allowFrom ?? ["*"]
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Returns all enabled accounts, including the implicit default account when
|
|
116
|
-
* legacy top-level ClickClack config is present.
|
|
117
|
-
*/
|
|
118
|
-
function listEnabledClickClackAccounts(cfg) {
|
|
119
|
-
return listClickClackAccountIds(cfg).map((accountId) => resolveClickClackAccount({
|
|
120
|
-
cfg,
|
|
121
|
-
accountId
|
|
122
|
-
})).filter((account) => account.enabled);
|
|
123
|
-
}
|
|
124
|
-
//#endregion
|
|
125
|
-
//#region extensions/clickclack/src/config-schema.ts
|
|
126
|
-
/**
|
|
127
|
-
* Zod-backed config schema for ClickClack channel accounts.
|
|
128
|
-
*/
|
|
129
|
-
const ClickClackAccountConfigSchema = z.object({
|
|
130
|
-
name: z.string().optional(),
|
|
131
|
-
enabled: z.boolean().optional(),
|
|
132
|
-
baseUrl: z.string().url().optional(),
|
|
133
|
-
token: buildSecretInputSchema().optional(),
|
|
134
|
-
workspace: z.string().optional(),
|
|
135
|
-
botUserId: z.string().optional(),
|
|
136
|
-
agentId: z.string().optional(),
|
|
137
|
-
replyMode: z.enum(["agent", "model"]).optional(),
|
|
138
|
-
model: z.string().optional(),
|
|
139
|
-
systemPrompt: z.string().optional(),
|
|
140
|
-
timeoutSeconds: z.number().int().min(1).max(3600).optional(),
|
|
141
|
-
toolsAllow: z.array(z.string()).optional(),
|
|
142
|
-
defaultTo: z.string().optional(),
|
|
143
|
-
allowFrom: z.array(z.string()).optional(),
|
|
144
|
-
reconnectMs: z.number().int().min(100).max(6e4).optional(),
|
|
145
|
-
agentActivity: z.boolean().optional()
|
|
146
|
-
}).strict();
|
|
147
|
-
/**
|
|
148
|
-
* Config schema exported to core so `openclaw doctor` and config validation
|
|
149
|
-
* understand both default and named ClickClack accounts.
|
|
150
|
-
*/
|
|
151
|
-
const clickClackConfigSchema = buildChannelConfigSchema(ClickClackAccountConfigSchema.extend({
|
|
152
|
-
accounts: z.record(z.string(), ClickClackAccountConfigSchema.partial()).optional(),
|
|
153
|
-
defaultAccount: z.string().optional()
|
|
154
|
-
}).strict());
|
|
155
|
-
//#endregion
|
|
156
16
|
//#region extensions/clickclack/src/runtime.ts
|
|
157
17
|
/**
|
|
158
18
|
* Runtime store for host-provided OpenClaw services used by the ClickClack
|
|
@@ -215,223 +75,62 @@ async function resolveClickClackInboundAccess(params) {
|
|
|
215
75
|
};
|
|
216
76
|
}
|
|
217
77
|
//#endregion
|
|
218
|
-
//#region extensions/clickclack/src/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
* are ignored by servers without the provenance columns, so these are safe
|
|
226
|
-
* to send unconditionally when present.
|
|
227
|
-
*/
|
|
228
|
-
function provenanceFields(provenance) {
|
|
229
|
-
const fields = {};
|
|
230
|
-
if (provenance?.model?.trim()) fields.author_model = provenance.model.trim();
|
|
231
|
-
if (provenance?.thinking?.trim()) fields.author_thinking = provenance.thinking.trim();
|
|
232
|
-
if (provenance?.runtime?.trim()) fields.author_runtime = provenance.runtime.trim();
|
|
233
|
-
return fields;
|
|
78
|
+
//#region extensions/clickclack/src/command-menu.ts
|
|
79
|
+
const CLICKCLACK_COMMAND_PATTERN = /^[a-z0-9_-]{1,32}$/u;
|
|
80
|
+
const CLICKCLACK_MAX_COMMANDS = 100;
|
|
81
|
+
const CLICKCLACK_MAX_DESCRIPTION_LENGTH = 100;
|
|
82
|
+
const CLICKCLACK_MAX_ARGS_HINT_LENGTH = 100;
|
|
83
|
+
function truncateCodePoints(value, maxLength) {
|
|
84
|
+
return Array.from(value).slice(0, maxLength).join("");
|
|
234
85
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const CLICKCLACK_CORRELATION_ID_HEADER = "X-Correlation-ID";
|
|
239
|
-
const CLICKCLACK_INBOUND_JSON_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
240
|
-
var ClickClackHttpError = class extends Error {
|
|
241
|
-
constructor(status, detail, headers) {
|
|
242
|
-
super(`ClickClack ${status}: ${detail}`);
|
|
243
|
-
this.status = status;
|
|
244
|
-
this.headers = headers;
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
/** Accepts the same bounded request-correlation shape as the ClickClack API. */
|
|
248
|
-
function normalizeClickClackCorrelationId(value) {
|
|
249
|
-
if (typeof value !== "string") return;
|
|
250
|
-
const normalized = value.trim();
|
|
251
|
-
if (!normalized || normalized.length > CLICKCLACK_CORRELATION_ID_MAX_LENGTH || !CLICKCLACK_CORRELATION_ID_PATTERN.test(normalized)) return;
|
|
252
|
-
return normalized;
|
|
86
|
+
function commandArgsHint(spec) {
|
|
87
|
+
if (spec.args?.length) return truncateCodePoints(spec.args.map((arg) => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(" "), CLICKCLACK_MAX_ARGS_HINT_LENGTH);
|
|
88
|
+
return spec.acceptsArgs ? "[args]" : "";
|
|
253
89
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (init.body && !(init.body instanceof FormData)) requestHeaders.set("Content-Type", "application/json");
|
|
270
|
-
const response = await fetcher(`${baseUrl}${path}`, {
|
|
271
|
-
...init,
|
|
272
|
-
headers: requestHeaders
|
|
273
|
-
});
|
|
274
|
-
if (!response.ok) {
|
|
275
|
-
const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
|
|
276
|
-
throw new ClickClackHttpError(response.status, detail, new Headers(response.headers));
|
|
90
|
+
function errorStatus(error) {
|
|
91
|
+
if (!error || typeof error !== "object" || !("status" in error)) return;
|
|
92
|
+
const status = error.status;
|
|
93
|
+
return typeof status === "number" ? status : void 0;
|
|
94
|
+
}
|
|
95
|
+
function mapNativeCommandSpecsToClickClackMenu(specs, log) {
|
|
96
|
+
const commands = [];
|
|
97
|
+
const seen = /* @__PURE__ */ new Set();
|
|
98
|
+
const invalidNames = /* @__PURE__ */ new Set();
|
|
99
|
+
for (const spec of specs) {
|
|
100
|
+
if (spec.isAlias) continue;
|
|
101
|
+
const command = spec.name.trim().toLowerCase();
|
|
102
|
+
if (!CLICKCLACK_COMMAND_PATTERN.test(command)) {
|
|
103
|
+
invalidNames.add(spec.name.trim() || "<empty>");
|
|
104
|
+
continue;
|
|
277
105
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
events: data.events,
|
|
288
|
-
...typeof data.tail_cursor === "string" ? { tailCursor: data.tail_cursor } : {}
|
|
289
|
-
};
|
|
106
|
+
if (seen.has(command)) continue;
|
|
107
|
+
seen.add(command);
|
|
108
|
+
const description = spec.description.trim() || command;
|
|
109
|
+
commands.push({
|
|
110
|
+
command,
|
|
111
|
+
description: truncateCodePoints(description, CLICKCLACK_MAX_DESCRIPTION_LENGTH),
|
|
112
|
+
args_hint: commandArgsHint(spec)
|
|
113
|
+
});
|
|
290
114
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return
|
|
303
|
-
},
|
|
304
|
-
directMessages: async (conversationId, afterSeq, limit = 20) => {
|
|
305
|
-
return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
|
|
306
|
-
},
|
|
307
|
-
thread: async (messageId) => await request(`/api/messages/${encodeURIComponent(messageId)}/thread`),
|
|
308
|
-
message: async (messageId) => {
|
|
309
|
-
return (await request(`/api/messages/${encodeURIComponent(messageId)}`)).message;
|
|
310
|
-
},
|
|
311
|
-
findMessageByNonce: async (params) => {
|
|
312
|
-
const query = new URLSearchParams({
|
|
313
|
-
workspace_id: params.workspaceId,
|
|
314
|
-
nonce: params.nonce
|
|
315
|
-
});
|
|
316
|
-
try {
|
|
317
|
-
return (await request(`/api/messages/by-nonce?${query.toString()}`)).message;
|
|
318
|
-
} catch (error) {
|
|
319
|
-
if (error instanceof ClickClackHttpError && error.status === 404) {
|
|
320
|
-
if (error.headers.get("X-ClickClack-Message-Nonce") === "supported") return;
|
|
321
|
-
throw new Error("ClickClack server does not support durable message nonce lookup", { cause: error });
|
|
322
|
-
}
|
|
323
|
-
throw error;
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
createChannelMessage: async (channelId, body, opts) => {
|
|
327
|
-
return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages`, {
|
|
328
|
-
method: "POST",
|
|
329
|
-
body: JSON.stringify({
|
|
330
|
-
body,
|
|
331
|
-
...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
|
|
332
|
-
...opts?.nonce ? { nonce: opts.nonce } : {},
|
|
333
|
-
...provenanceFields(opts?.provenance)
|
|
334
|
-
})
|
|
335
|
-
})).message;
|
|
336
|
-
},
|
|
337
|
-
createThreadReply: async (messageId, body, opts) => {
|
|
338
|
-
return (await request(`/api/messages/${encodeURIComponent(messageId)}/thread/replies`, {
|
|
339
|
-
method: "POST",
|
|
340
|
-
body: JSON.stringify({
|
|
341
|
-
body,
|
|
342
|
-
...opts?.nonce ? { nonce: opts.nonce } : {},
|
|
343
|
-
...provenanceFields(opts?.provenance)
|
|
344
|
-
})
|
|
345
|
-
})).message;
|
|
346
|
-
},
|
|
347
|
-
createDirectConversation: async (workspaceId, memberIds) => {
|
|
348
|
-
return (await request("/api/dms", {
|
|
349
|
-
method: "POST",
|
|
350
|
-
body: JSON.stringify({
|
|
351
|
-
workspace_id: workspaceId,
|
|
352
|
-
member_ids: memberIds
|
|
353
|
-
})
|
|
354
|
-
})).conversation;
|
|
355
|
-
},
|
|
356
|
-
createUpload: async (params) => {
|
|
357
|
-
const form = new FormData();
|
|
358
|
-
const bytes = new Uint8Array(params.buffer);
|
|
359
|
-
form.append("file", new Blob([bytes], { type: params.contentType }), params.filename);
|
|
360
|
-
const query = new URLSearchParams({ workspace_id: params.workspaceId });
|
|
361
|
-
if (params.nonce) query.set("nonce", params.nonce);
|
|
362
|
-
return (await request(`/api/uploads?${query.toString()}`, {
|
|
363
|
-
method: "POST",
|
|
364
|
-
body: form
|
|
365
|
-
})).upload;
|
|
366
|
-
},
|
|
367
|
-
findUploadByNonce: async (params) => {
|
|
368
|
-
const query = new URLSearchParams({
|
|
369
|
-
workspace_id: params.workspaceId,
|
|
370
|
-
nonce: params.nonce
|
|
371
|
-
});
|
|
372
|
-
try {
|
|
373
|
-
return (await request(`/api/uploads/by-nonce?${query.toString()}`)).upload;
|
|
374
|
-
} catch (error) {
|
|
375
|
-
if (error instanceof ClickClackHttpError && error.status === 404) {
|
|
376
|
-
if (error.headers.get("X-ClickClack-Upload-Nonce") === "supported") return;
|
|
377
|
-
throw new Error("ClickClack server does not support durable upload nonce lookup", { cause: error });
|
|
378
|
-
}
|
|
379
|
-
throw error;
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
attachUpload: async (messageId, uploadId) => {
|
|
383
|
-
await request(`/api/messages/${encodeURIComponent(messageId)}/attachments`, {
|
|
384
|
-
method: "POST",
|
|
385
|
-
body: JSON.stringify({ upload_id: uploadId })
|
|
386
|
-
});
|
|
387
|
-
},
|
|
388
|
-
/**
|
|
389
|
-
* POSTs a durable agent activity row (agent_commentary / agent_tool)
|
|
390
|
-
* through the normal message create path. Requires a bot token carrying
|
|
391
|
-
* the agent_activity:write scope on the ClickClack side.
|
|
392
|
-
*/
|
|
393
|
-
createActivityMessage: async (params) => {
|
|
394
|
-
if (!params.channelId && !params.conversationId) throw new Error("createActivityMessage requires a channelId or conversationId");
|
|
395
|
-
return (await request(params.channelId ? `/api/channels/${encodeURIComponent(params.channelId)}/messages` : `/api/dms/${encodeURIComponent(params.conversationId ?? "")}/messages`, {
|
|
396
|
-
method: "POST",
|
|
397
|
-
body: JSON.stringify({
|
|
398
|
-
body: params.body,
|
|
399
|
-
kind: params.kind,
|
|
400
|
-
turn_id: params.turnId,
|
|
401
|
-
...provenanceFields(params.provenance)
|
|
402
|
-
})
|
|
403
|
-
})).message;
|
|
404
|
-
},
|
|
405
|
-
/** PATCHes the body of an existing message (activity row coalescing). */
|
|
406
|
-
updateMessageBody: async (messageId, body) => {
|
|
407
|
-
return (await request(`/api/messages/${encodeURIComponent(messageId)}`, {
|
|
408
|
-
method: "PATCH",
|
|
409
|
-
body: JSON.stringify({ body })
|
|
410
|
-
})).message;
|
|
411
|
-
},
|
|
412
|
-
createDirectMessage: async (conversationId, body, opts) => {
|
|
413
|
-
return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages`, {
|
|
414
|
-
method: "POST",
|
|
415
|
-
body: JSON.stringify({
|
|
416
|
-
body,
|
|
417
|
-
...opts?.quotedMessageId ? { quoted_message_id: opts.quotedMessageId } : {},
|
|
418
|
-
...opts?.nonce ? { nonce: opts.nonce } : {}
|
|
419
|
-
})
|
|
420
|
-
})).message;
|
|
421
|
-
},
|
|
422
|
-
events: async (workspaceId, afterCursor) => (await fetchEventPage(workspaceId, { afterCursor })).events,
|
|
423
|
-
eventPage: fetchEventPage,
|
|
424
|
-
websocket: (workspaceId, afterCursor) => {
|
|
425
|
-
const url = new URL(`${baseUrl}/api/realtime/ws`);
|
|
426
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
427
|
-
url.searchParams.set("workspace_id", workspaceId);
|
|
428
|
-
if (afterCursor) url.searchParams.set("after_cursor", afterCursor);
|
|
429
|
-
return new WebSocket(url, {
|
|
430
|
-
headers: { Authorization: `Bearer ${options.token}` },
|
|
431
|
-
maxPayload: CLICKCLACK_INBOUND_JSON_LIMIT_BYTES
|
|
432
|
-
});
|
|
115
|
+
if (invalidNames.size > 0) log?.warn?.(`ClickClack command menu skipped invalid native command names: ${[...invalidNames].map((name) => JSON.stringify(name)).join(", ")}`);
|
|
116
|
+
return commands.slice(0, CLICKCLACK_MAX_COMMANDS);
|
|
117
|
+
}
|
|
118
|
+
async function syncClickClackCommandMenu(params) {
|
|
119
|
+
try {
|
|
120
|
+
const commands = mapNativeCommandSpecsToClickClackMenu(listNativeCommandSpecsForConfig(params.cfg, { provider: "clickclack" }), params.log);
|
|
121
|
+
await params.client.setBotCommands(commands);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const status = errorStatus(error);
|
|
124
|
+
if (status === 403) {
|
|
125
|
+
params.log?.warn?.("ClickClack command menu sync skipped: bot token lacks commands:write");
|
|
126
|
+
return;
|
|
433
127
|
}
|
|
434
|
-
|
|
128
|
+
if (status === 404) {
|
|
129
|
+
params.log?.debug?.("ClickClack command menu sync skipped: server does not support /api/bots/self/commands");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
params.log?.warn?.(`ClickClack command menu sync failed: ${formatErrorMessage(error)}`);
|
|
133
|
+
}
|
|
435
134
|
}
|
|
436
135
|
//#endregion
|
|
437
136
|
//#region extensions/clickclack/src/activity.ts
|
|
@@ -629,27 +328,6 @@ function createClickClackActivityPublisher(params) {
|
|
|
629
328
|
};
|
|
630
329
|
}
|
|
631
330
|
//#endregion
|
|
632
|
-
//#region extensions/clickclack/src/resolve.ts
|
|
633
|
-
/**
|
|
634
|
-
* Resolves a workspace slug/name/id from config to a ClickClack workspace id.
|
|
635
|
-
*/
|
|
636
|
-
async function resolveWorkspaceId(client, workspace) {
|
|
637
|
-
if (workspace.startsWith("wsp_")) return workspace;
|
|
638
|
-
const found = (await client.workspaces()).find((candidate) => candidate.id === workspace || candidate.slug === workspace || candidate.name === workspace);
|
|
639
|
-
if (!found) throw new Error(`ClickClack workspace not found: ${workspace}`);
|
|
640
|
-
return found.id;
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Resolves a channel name/id from config or target input to a ClickClack
|
|
644
|
-
* channel id.
|
|
645
|
-
*/
|
|
646
|
-
async function resolveChannelId(client, workspaceId, channel) {
|
|
647
|
-
if (channel.startsWith("chn_")) return channel;
|
|
648
|
-
const found = (await client.channels(workspaceId)).find((candidate) => candidate.id === channel || candidate.name === channel);
|
|
649
|
-
if (!found) throw new Error(`ClickClack channel not found: ${channel}`);
|
|
650
|
-
return found.id;
|
|
651
|
-
}
|
|
652
|
-
//#endregion
|
|
653
331
|
//#region extensions/clickclack/src/target.ts
|
|
654
332
|
/**
|
|
655
333
|
* Parses `channel:name`, `thread:msg_id`, `dm:usr_id`, or a bare channel name.
|
|
@@ -1063,6 +741,8 @@ async function handleClickClackInbound(params) {
|
|
|
1063
741
|
message
|
|
1064
742
|
});
|
|
1065
743
|
if (!access.shouldDispatch) return;
|
|
744
|
+
const conversationId = message.channel_id || message.direct_conversation_id;
|
|
745
|
+
if (!conversationId) return;
|
|
1066
746
|
const isDirect = Boolean(message.direct_conversation_id);
|
|
1067
747
|
const target = buildClickClackTarget(isDirect ? {
|
|
1068
748
|
chatType: "direct",
|
|
@@ -1108,46 +788,60 @@ async function handleClickClackInbound(params) {
|
|
|
1108
788
|
}
|
|
1109
789
|
});
|
|
1110
790
|
const senderName = message.author?.display_name || message.author_id;
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
})
|
|
1115
|
-
const body = runtime.channel.reply.formatAgentEnvelope({
|
|
791
|
+
const body = createChannelInboundEnvelopeBuilder({
|
|
792
|
+
cfg: params.config,
|
|
793
|
+
route
|
|
794
|
+
})({
|
|
1116
795
|
channel: "ClickClack",
|
|
1117
796
|
from: senderName,
|
|
1118
797
|
timestamp: new Date(message.created_at),
|
|
1119
|
-
previousTimestamp,
|
|
1120
|
-
envelope: runtime.channel.reply.resolveEnvelopeFormatOptions(params.config),
|
|
1121
798
|
body: message.body
|
|
1122
799
|
});
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
800
|
+
const ctxPayload = runtime.channel.inbound.buildContext({
|
|
801
|
+
channel: CHANNEL_ID$1,
|
|
802
|
+
accountId: route.accountId ?? params.account.accountId,
|
|
803
|
+
messageId: message.id,
|
|
804
|
+
messageIdFull: message.id,
|
|
805
|
+
timestamp: new Date(message.created_at).getTime(),
|
|
806
|
+
from: target,
|
|
807
|
+
sender: {
|
|
808
|
+
id: message.author_id,
|
|
809
|
+
name: senderName
|
|
810
|
+
},
|
|
811
|
+
conversation: {
|
|
812
|
+
kind: isDirect ? "direct" : "group",
|
|
813
|
+
id: conversationId,
|
|
814
|
+
label: isDirect ? senderName : message.channel_id,
|
|
815
|
+
threadId: message.parent_message_id ? message.thread_root_id : void 0,
|
|
816
|
+
nativeChannelId: conversationId
|
|
817
|
+
},
|
|
818
|
+
route: {
|
|
819
|
+
agentId: route.agentId,
|
|
820
|
+
dmScope: route.dmScope,
|
|
821
|
+
accountId: route.accountId,
|
|
822
|
+
routeSessionKey: route.sessionKey
|
|
823
|
+
},
|
|
824
|
+
reply: {
|
|
825
|
+
to: target,
|
|
826
|
+
originatingTo: target,
|
|
827
|
+
replyToId: message.id,
|
|
828
|
+
messageThreadId: message.parent_message_id ? message.thread_root_id : void 0,
|
|
829
|
+
threadParentId: message.parent_message_id ? message.thread_root_id : void 0
|
|
830
|
+
},
|
|
831
|
+
message: {
|
|
832
|
+
body,
|
|
833
|
+
bodyForAgent: message.body,
|
|
834
|
+
rawBody: message.body,
|
|
835
|
+
commandBody: message.body
|
|
836
|
+
},
|
|
837
|
+
access: {
|
|
838
|
+
commands: { authorized: access.commandAuthorized },
|
|
839
|
+
mentions: {
|
|
840
|
+
canDetectMention: !isDirect,
|
|
841
|
+
wasMentioned: !isDirect
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
extra: { GroupChannel: message.channel_id }
|
|
1151
845
|
});
|
|
1152
846
|
const runId = resolveClickClackAgentRunId(message.id);
|
|
1153
847
|
const activityReplyOptions = activity ? {
|
|
@@ -1163,16 +857,16 @@ async function handleClickClackInbound(params) {
|
|
|
1163
857
|
suppressDefaultToolProgressMessages: true,
|
|
1164
858
|
allowProgressCallbacksWhenSourceDeliverySuppressed: true
|
|
1165
859
|
} : void 0;
|
|
1166
|
-
const dispatchPromise = runtime.channel.inbound.
|
|
860
|
+
const dispatchPromise = runtime.channel.inbound.dispatch({
|
|
1167
861
|
cfg: params.config,
|
|
1168
862
|
channel: CHANNEL_ID$1,
|
|
1169
863
|
accountId: params.account.accountId,
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
864
|
+
route: {
|
|
865
|
+
agentId: route.agentId,
|
|
866
|
+
dmScope: route.dmScope,
|
|
867
|
+
sessionKey: route.sessionKey
|
|
868
|
+
},
|
|
1173
869
|
ctxPayload,
|
|
1174
|
-
recordInboundSession: runtime.channel.session.recordInboundSession,
|
|
1175
|
-
dispatchReplyWithBufferedBlockDispatcher: runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
1176
870
|
toolsAllow: params.account.toolsAllow,
|
|
1177
871
|
replyOptions: runId || activityReplyOptions ? {
|
|
1178
872
|
...runId ? { runId } : {},
|
|
@@ -1322,6 +1016,18 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1322
1016
|
workspace: workspaceId,
|
|
1323
1017
|
botUserId: configuredAccount.botUserId ?? me.id
|
|
1324
1018
|
};
|
|
1019
|
+
const processIncomingEvent = (event) => processEvent({
|
|
1020
|
+
account,
|
|
1021
|
+
config: ctx.cfg,
|
|
1022
|
+
client,
|
|
1023
|
+
event,
|
|
1024
|
+
botUserId: account.botUserId
|
|
1025
|
+
});
|
|
1026
|
+
if (account.commandMenu) await syncClickClackCommandMenu({
|
|
1027
|
+
cfg: ctx.cfg,
|
|
1028
|
+
client,
|
|
1029
|
+
log: ctx.log
|
|
1030
|
+
});
|
|
1325
1031
|
ctx.setStatus({
|
|
1326
1032
|
accountId: account.accountId,
|
|
1327
1033
|
running: true,
|
|
@@ -1343,20 +1049,15 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1343
1049
|
workspaceId,
|
|
1344
1050
|
afterCursor,
|
|
1345
1051
|
abortSignal: ctx.abortSignal,
|
|
1346
|
-
onEvent:
|
|
1347
|
-
await processEvent({
|
|
1348
|
-
account,
|
|
1349
|
-
config: ctx.cfg,
|
|
1350
|
-
client,
|
|
1351
|
-
event,
|
|
1352
|
-
botUserId: account.botUserId
|
|
1353
|
-
});
|
|
1354
|
-
}
|
|
1052
|
+
onEvent: processIncomingEvent
|
|
1355
1053
|
});
|
|
1356
1054
|
if (ctx.abortSignal.aborted) break;
|
|
1357
1055
|
const socket = client.websocket(workspaceId, afterCursor);
|
|
1358
|
-
await new Promise((resolve
|
|
1056
|
+
await new Promise((resolve) => {
|
|
1359
1057
|
let settled = false;
|
|
1058
|
+
let closing = false;
|
|
1059
|
+
let loggedMessageFailure = false;
|
|
1060
|
+
let messageQueue = Promise.resolve();
|
|
1360
1061
|
let removeAbortListener;
|
|
1361
1062
|
const finishSocketCycle = () => {
|
|
1362
1063
|
if (settled) return;
|
|
@@ -1365,6 +1066,20 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1365
1066
|
removeAbortListener = void 0;
|
|
1366
1067
|
resolve();
|
|
1367
1068
|
};
|
|
1069
|
+
const finishAfterQueuedMessages = () => {
|
|
1070
|
+
messageQueue.then(() => finishSocketCycle(), () => finishSocketCycle());
|
|
1071
|
+
};
|
|
1072
|
+
const reconnectAfterMessageFailure = (error) => {
|
|
1073
|
+
if (settled || ctx.abortSignal.aborted) return;
|
|
1074
|
+
if (!loggedMessageFailure) {
|
|
1075
|
+
loggedMessageFailure = true;
|
|
1076
|
+
ctx.log?.warn?.(`[${account.accountId}] ClickClack event processing failed; reconnecting: ${error instanceof Error ? error.message : formatErrorMessage(error)}`);
|
|
1077
|
+
}
|
|
1078
|
+
if (!closing) {
|
|
1079
|
+
closing = true;
|
|
1080
|
+
socket.close();
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1368
1083
|
const abort = () => {
|
|
1369
1084
|
socket.close();
|
|
1370
1085
|
finishSocketCycle();
|
|
@@ -1372,36 +1087,38 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1372
1087
|
ctx.abortSignal.addEventListener("abort", abort, { once: true });
|
|
1373
1088
|
removeAbortListener = () => ctx.abortSignal.removeEventListener("abort", abort);
|
|
1374
1089
|
socket.on("message", (data) => {
|
|
1375
|
-
(
|
|
1090
|
+
if (closing || settled) return;
|
|
1091
|
+
messageQueue = messageQueue.then(async () => {
|
|
1376
1092
|
const event = parseSocketEvent(data);
|
|
1377
1093
|
if (!event) {
|
|
1378
1094
|
ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
|
|
1379
1095
|
return;
|
|
1380
1096
|
}
|
|
1097
|
+
await processIncomingEvent(event);
|
|
1381
1098
|
afterCursor = event.cursor || afterCursor;
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
});
|
|
1389
|
-
})().catch((e) => reject(e instanceof Error ? e : new Error(`ClickClack ws message failed: ${formatErrorMessage(e)}`, { cause: e })));
|
|
1099
|
+
});
|
|
1100
|
+
messageQueue.catch(reconnectAfterMessageFailure);
|
|
1101
|
+
});
|
|
1102
|
+
socket.on("close", () => {
|
|
1103
|
+
closing = true;
|
|
1104
|
+
finishAfterQueuedMessages();
|
|
1390
1105
|
});
|
|
1391
|
-
socket.on("close", finishSocketCycle);
|
|
1392
1106
|
socket.on("error", (error) => {
|
|
1393
1107
|
if (settled || ctx.abortSignal.aborted) {
|
|
1394
1108
|
finishSocketCycle();
|
|
1395
1109
|
return;
|
|
1396
1110
|
}
|
|
1111
|
+
if (closing) return;
|
|
1397
1112
|
ctx.log?.warn?.(`[${account.accountId}] ClickClack websocket error; reconnecting: ${error instanceof Error ? error.message : String(error)}`);
|
|
1398
|
-
|
|
1113
|
+
closing = true;
|
|
1399
1114
|
socket.close();
|
|
1400
1115
|
});
|
|
1401
1116
|
});
|
|
1402
|
-
if (!ctx.abortSignal.aborted)
|
|
1403
|
-
|
|
1404
|
-
})
|
|
1117
|
+
if (!ctx.abortSignal.aborted) try {
|
|
1118
|
+
await sleepWithAbort(account.reconnectMs, ctx.abortSignal);
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (!ctx.abortSignal.aborted) throw error;
|
|
1121
|
+
}
|
|
1405
1122
|
}
|
|
1406
1123
|
} finally {
|
|
1407
1124
|
ctx.setStatus({
|
|
@@ -1416,8 +1133,7 @@ async function startClickClackGatewayAccount(ctx) {
|
|
|
1416
1133
|
* ClickClack channel plugin definition: target parsing, account config, status,
|
|
1417
1134
|
* gateway startup, and outbound delivery wiring.
|
|
1418
1135
|
*/
|
|
1419
|
-
const CHANNEL_ID =
|
|
1420
|
-
const meta = { ...getChatChannelMeta(CHANNEL_ID) };
|
|
1136
|
+
const CHANNEL_ID = CLICKCLACK_CHANNEL_ID;
|
|
1421
1137
|
const clickClackMessageAdapter = defineChannelMessageAdapter({
|
|
1422
1138
|
id: CHANNEL_ID,
|
|
1423
1139
|
durableFinal: {
|
|
@@ -1502,7 +1218,7 @@ const clickClackMessageAdapter = defineChannelMessageAdapter({
|
|
|
1502
1218
|
const clickClackPlugin = createChatChannelPlugin({
|
|
1503
1219
|
base: {
|
|
1504
1220
|
id: CHANNEL_ID,
|
|
1505
|
-
meta,
|
|
1221
|
+
meta: clickClackMeta,
|
|
1506
1222
|
capabilities: {
|
|
1507
1223
|
chatTypes: ["direct", "group"],
|
|
1508
1224
|
threads: true,
|
|
@@ -1510,22 +1226,12 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1510
1226
|
},
|
|
1511
1227
|
reload: { configPrefixes: ["channels.clickclack"] },
|
|
1512
1228
|
configSchema: clickClackConfigSchema,
|
|
1513
|
-
config:
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
defaultAccountId: (cfg) => resolveDefaultClickClackAccountId(cfg),
|
|
1520
|
-
isConfigured: (account) => account.configured,
|
|
1521
|
-
resolveAllowFrom: ({ cfg, accountId }) => resolveClickClackAccount({
|
|
1522
|
-
cfg,
|
|
1523
|
-
accountId
|
|
1524
|
-
}).allowFrom,
|
|
1525
|
-
resolveDefaultTo: ({ cfg, accountId }) => resolveClickClackAccount({
|
|
1526
|
-
cfg,
|
|
1527
|
-
accountId
|
|
1528
|
-
}).defaultTo
|
|
1229
|
+
config: clickClackConfigAdapter,
|
|
1230
|
+
setup: clickClackSetupAdapter,
|
|
1231
|
+
setupWizard: clickClackSetupWizard,
|
|
1232
|
+
secrets: {
|
|
1233
|
+
secretTargetRegistryEntries,
|
|
1234
|
+
collectRuntimeConfigAssignments
|
|
1529
1235
|
},
|
|
1530
1236
|
messaging: {
|
|
1531
1237
|
targetPrefixes: ["clickclack", "cc"],
|
|
@@ -1627,4 +1333,4 @@ const clickClackPlugin = createChatChannelPlugin({
|
|
|
1627
1333
|
}
|
|
1628
1334
|
});
|
|
1629
1335
|
//#endregion
|
|
1630
|
-
export {
|
|
1336
|
+
export { setClickClackRuntime as a, getClickClackRuntime as i, buildClickClackTarget as n, parseClickClackTarget as r, clickClackPlugin as t };
|