@openclaw/clickclack 2026.7.2-beta.3 → 2026.7.2-beta.4

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,4 +1,4 @@
1
- import { a as createClickClackClient, m as resolveClickClackAccount } from "./setup-surface-CNjp76Xx.js";
2
- import { a as setClickClackRuntime, r as parseClickClackTarget } from "./channel-CEYthZ9N.js";
3
- import "./api.js";
4
- export { createClickClackClient, parseClickClackTarget, resolveClickClackAccount, setClickClackRuntime };
1
+ import { d as createClickClackClient, x as resolveClickClackAccount } from "./setup-surface-Baf26N-D.js";
2
+ import { T as setClickClackRuntime, r as parseClickClackTarget } from "./channel-BDq0KWDB.js";
3
+ import { t as registerClickClackDiscussions } from "./runtime-api-CV1cOqFO.js";
4
+ export { createClickClackClient, parseClickClackTarget, registerClickClackDiscussions, resolveClickClackAccount, setClickClackRuntime };
@@ -1,5 +1,6 @@
1
+ import { a as buildClickClackSetupClaimUrl, o as isClickClackSetupLoopbackHost, s as requireClickClackSetupApiBaseUrl } from "./setup-surface-Baf26N-D.js";
1
2
  import { createProviderOperationDeadline, readProviderJsonResponse, readResponseTextLimited, resolveProviderOperationTimeoutMs } from "openclaw/plugin-sdk/provider-http";
2
- import { fetchWithSsrFGuard, isPrivateOrLoopbackHost, resolvePinnedHostnameWithPolicy, ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
3
+ import { fetchWithSsrFGuard, resolvePinnedHostnameWithPolicy, ssrfPolicyFromHttpBaseUrlAllowedHostname } from "openclaw/plugin-sdk/ssrf-runtime";
3
4
  import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
4
5
  //#region extensions/clickclack/src/setup-claim.ts
5
6
  const CLICKCLACK_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
@@ -20,7 +21,7 @@ function requireString(record, key, label) {
20
21
  if (typeof value !== "string" || !value.trim()) throw new Error(`ClickClack setup code claim returned invalid ${label}.${key}`);
21
22
  return value;
22
23
  }
23
- function parseClickClackSetupCodeClaim(value) {
24
+ function parseClickClackSetupCodeClaim(value, expectedClaimUrl) {
24
25
  const claim = requireRecord(value, "response");
25
26
  const bot = requireRecord(claim.bot, "bot");
26
27
  const workspace = requireRecord(claim.workspace, "workspace");
@@ -31,7 +32,23 @@ function parseClickClackSetupCodeClaim(value) {
31
32
  if (defaultTo !== void 0 && typeof defaultTo !== "string") throw new Error("ClickClack setup code claim returned invalid defaults.defaultTo");
32
33
  if (allowFrom !== void 0 && (!Array.isArray(allowFrom) || !allowFrom.every((entry) => typeof entry === "string"))) throw new Error("ClickClack setup code claim returned invalid defaults.allowFrom");
33
34
  if (agentActivity !== void 0 && typeof agentActivity !== "boolean") throw new Error("ClickClack setup code claim returned invalid defaults.agentActivity");
35
+ const contractVersion = claim.contract_version;
36
+ const apiBaseUrlValue = claim.api_base_url;
37
+ const hasContractMetadata = contractVersion !== void 0 || apiBaseUrlValue !== void 0;
38
+ if (expectedClaimUrl && !hasContractMetadata) throw new Error("ClickClack setup code claim returned a legacy response for an exact endpoint");
39
+ let contract = {};
40
+ if (hasContractMetadata) {
41
+ if (contractVersion !== 1 || typeof apiBaseUrlValue !== "string") throw new Error("ClickClack setup code claim returned invalid v1 contract metadata");
42
+ const apiBaseUrl = requireClickClackSetupApiBaseUrl(apiBaseUrlValue, "setup code claim response.api_base_url");
43
+ const canonicalClaimUrl = buildClickClackSetupClaimUrl(apiBaseUrl);
44
+ if (expectedClaimUrl && expectedClaimUrl !== canonicalClaimUrl) throw new Error("ClickClack setup code claim returned an API base that does not match the claim URL");
45
+ contract = {
46
+ contract_version: 1,
47
+ api_base_url: apiBaseUrl
48
+ };
49
+ }
34
50
  return {
51
+ ...contract,
35
52
  token: requireString(claim, "token", "response"),
36
53
  bot: {
37
54
  id: requireString(bot, "id", "bot"),
@@ -53,30 +70,35 @@ function parseClickClackSetupCodeClaim(value) {
53
70
  }
54
71
  /** Claims a one-time setup code without sending any existing bot credential. */
55
72
  async function claimClickClackSetupCode(params) {
56
- const baseUrl = params.baseUrl.replace(/\/+$/, "");
57
- const parsedBaseUrl = new URL(baseUrl);
73
+ let parsedClaimUrl;
74
+ try {
75
+ parsedClaimUrl = new URL(params.claimUrl);
76
+ } catch {
77
+ throw new Error("ClickClack setup code claim URL must be a valid HTTP(S) endpoint.");
78
+ }
79
+ if (parsedClaimUrl.protocol !== "http:" && parsedClaimUrl.protocol !== "https:" || parsedClaimUrl.username || parsedClaimUrl.password || parsedClaimUrl.search || parsedClaimUrl.hash || !parsedClaimUrl.pathname.endsWith("/api/bot-setup-codes/claim")) throw new Error("ClickClack setup code claim URL must be a valid HTTP(S) endpoint.");
58
80
  const deadline = createProviderOperationDeadline({
59
81
  label: "ClickClack setup code claim",
60
82
  timeoutMs: CLICKCLACK_SETUP_CODE_CLAIM_TIMEOUT_MS
61
83
  });
62
84
  let pinnedHttpTarget;
63
- if (parsedBaseUrl.protocol === "http:") {
85
+ if (parsedClaimUrl.protocol === "http:") {
64
86
  const resolveTimeoutMs = resolveProviderOperationTimeoutMs({
65
87
  deadline,
66
88
  defaultTimeoutMs: CLICKCLACK_SETUP_CODE_CLAIM_TIMEOUT_MS
67
89
  });
68
- const pinned = await withTimeout(resolvePinnedHostnameWithPolicy(parsedBaseUrl.hostname, {
90
+ const pinned = await withTimeout(resolvePinnedHostnameWithPolicy(parsedClaimUrl.hostname, {
69
91
  lookupFn: params.lookupFn,
70
92
  policy: { dangerouslyAllowPrivateNetwork: true }
71
93
  }), resolveTimeoutMs, { message: `ClickClack setup code claim timed out after ${CLICKCLACK_SETUP_CODE_CLAIM_TIMEOUT_MS}ms` });
72
- if (!pinned.addresses.every((address) => isPrivateOrLoopbackHost(address))) throw new Error("ClickClack setup codes require HTTPS unless the server is on a private or loopback network.");
94
+ if (!pinned.addresses.every((address) => isClickClackSetupLoopbackHost(address))) throw new Error("ClickClack setup codes require HTTPS unless the server is on loopback.");
73
95
  pinnedHttpTarget = {
74
96
  hostname: pinned.hostname,
75
97
  addresses: pinned.addresses
76
98
  };
77
99
  }
78
100
  const { response, release } = await fetchWithSsrFGuard({
79
- url: `${baseUrl}/api/bot-setup-codes/claim`,
101
+ url: params.claimUrl,
80
102
  fetchImpl: params.fetch,
81
103
  init: {
82
104
  method: "POST",
@@ -91,8 +113,8 @@ async function claimClickClackSetupCode(params) {
91
113
  deadline,
92
114
  defaultTimeoutMs: CLICKCLACK_SETUP_CODE_CLAIM_TIMEOUT_MS
93
115
  }),
94
- requireHttps: parsedBaseUrl.protocol === "https:",
95
- policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(baseUrl),
116
+ requireHttps: parsedClaimUrl.protocol === "https:",
117
+ policy: ssrfPolicyFromHttpBaseUrlAllowedHostname(params.claimUrl),
96
118
  lookupFn: params.lookupFn,
97
119
  ...pinnedHttpTarget ? { dispatcherPolicy: {
98
120
  mode: "direct",
@@ -105,7 +127,7 @@ async function claimClickClackSetupCode(params) {
105
127
  const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
106
128
  throw new ClickClackSetupCodeClaimError(response.status, detail);
107
129
  }
108
- return parseClickClackSetupCodeClaim(await readProviderJsonResponse(response, "ClickClack setup code claim", { maxBytes: CLICKCLACK_SETUP_CODE_CLAIM_JSON_LIMIT_BYTES }));
130
+ return parseClickClackSetupCodeClaim(await readProviderJsonResponse(response, "ClickClack setup code claim", { maxBytes: CLICKCLACK_SETUP_CODE_CLAIM_JSON_LIMIT_BYTES }), params.expectedClaimUrl);
109
131
  } finally {
110
132
  await release();
111
133
  }
@@ -1,4 +1,4 @@
1
- import { l as clickClackConfigAdapter, n as clickClackSetupAdapter, s as clickClackConfigSchema, t as clickClackSetupWizard, u as clickClackMeta } from "./setup-surface-CNjp76Xx.js";
1
+ import { _ as clickClackMeta, g as clickClackConfigAdapter, m as clickClackConfigSchema, n as clickClackSetupAdapter, r as clickClackSetupContract, t as clickClackSetupWizard } from "./setup-surface-Baf26N-D.js";
2
2
  //#region extensions/clickclack/src/channel.setup.ts
3
3
  const clickClackSetupPlugin = {
4
4
  id: "clickclack",
@@ -12,6 +12,7 @@ const clickClackSetupPlugin = {
12
12
  configSchema: clickClackConfigSchema,
13
13
  config: clickClackConfigAdapter,
14
14
  setup: clickClackSetupAdapter,
15
+ setupContract: clickClackSetupContract,
15
16
  setupWizard: clickClackSetupWizard
16
17
  };
17
18
  //#endregion
@@ -11,10 +11,13 @@ import { getChatChannelMeta } from "openclaw/plugin-sdk/channel-plugin-common";
11
11
  import { buildChannelConfigSchema, buildMultiAccountChannelSchema } from "openclaw/plugin-sdk/channel-config-schema";
12
12
  import { z } from "zod";
13
13
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
14
+ import { redactToolPayloadText } from "openclaw/plugin-sdk/logging-core";
14
15
  import { readProviderJsonResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
15
16
  import { WebSocket } from "ws";
17
+ import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
16
18
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, applyAccountNameToChannelSection, applySetupAccountConfigPatch, baseUrlTextInput, createSetupTranslator, createStandardChannelSetupStatus, defineTokenCredential, formatDocsLink, hasConfiguredSecretInput, migrateBaseNameToDefaultAccount, moveSingleAccountChannelSectionToDefaultAccount, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
17
19
  import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup-runtime";
20
+ import net from "node:net";
18
21
  //#region \0rolldown/runtime.js
19
22
  var __defProp = Object.defineProperty;
20
23
  var __exportAll = (all, no_symbols) => {
@@ -35,6 +38,7 @@ var __exportAll = (all, no_symbols) => {
35
38
  const DEFAULT_RECONNECT_MS = 1500;
36
39
  const MIN_RECONNECT_MS = 100;
37
40
  const MAX_RECONNECT_MS = 6e4;
41
+ const DEFAULT_DISCUSSIONS_SECTION = "Sessions";
38
42
  const { listAccountIds: listClickClackAccountIds, resolveDefaultAccountId: resolveDefaultClickClackAccountId } = createAccountListHelpers("clickclack", {
39
43
  normalizeAccountId,
40
44
  hasImplicitDefaultAccount: (cfg) => {
@@ -49,6 +53,7 @@ function resolveClickClackAccountConfig(cfg, accountId) {
49
53
  accounts: channel?.accounts,
50
54
  accountId,
51
55
  omitKeys: ["defaultAccount"],
56
+ nestedObjectKeys: ["discussions"],
52
57
  normalizeAccountId
53
58
  });
54
59
  const account = resolveNormalizedAccountEntry(channel?.accounts, accountId, normalizeAccountId);
@@ -108,6 +113,9 @@ function resolveClickClackAccount(params) {
108
113
  env: params.env
109
114
  });
110
115
  const workspace = merged.workspace?.trim() ?? "";
116
+ const discussionsWorkspace = merged.discussions?.workspace?.trim() || workspace;
117
+ const controlUrlBase = merged.discussions?.controlUrlBase?.trim();
118
+ const apiEndpoint = merged.apiBaseUrl?.trim().replace(/\/$/, "") || baseUrl;
111
119
  return {
112
120
  accountId,
113
121
  enabled,
@@ -121,7 +129,6 @@ function resolveClickClackAccount(params) {
121
129
  replyMode: merged.replyMode === "model" ? "model" : "agent",
122
130
  model: normalizeOptionalString(merged.model),
123
131
  systemPrompt: normalizeOptionalString(merged.systemPrompt),
124
- timeoutSeconds: merged.timeoutSeconds,
125
132
  toolsAllow: merged.toolsAllow,
126
133
  defaultTo: merged.defaultTo?.trim() || "channel:general",
127
134
  allowFrom: merged.allowFrom ?? ["*"],
@@ -131,10 +138,17 @@ function resolveClickClackAccount(params) {
131
138
  }),
132
139
  agentActivity: merged.agentActivity === true,
133
140
  commandMenu: merged.commandMenu !== false,
141
+ discussions: {
142
+ enabled: merged.discussions?.enabled === true,
143
+ workspace: discussionsWorkspace,
144
+ ...controlUrlBase ? { controlUrlBase } : {},
145
+ section: merged.discussions?.section?.trim() || DEFAULT_DISCUSSIONS_SECTION
146
+ },
134
147
  config: {
135
148
  ...merged,
136
149
  allowFrom: merged.allowFrom ?? ["*"]
137
- }
150
+ },
151
+ apiEndpoint
138
152
  };
139
153
  }
140
154
  /**
@@ -177,6 +191,7 @@ const ClickClackAccountConfigSchema = z.object({
177
191
  name: z.string().optional(),
178
192
  enabled: z.boolean().optional(),
179
193
  baseUrl: z.string().url().optional(),
194
+ apiBaseUrl: z.string().url().optional(),
180
195
  token: buildSecretInputSchema().optional(),
181
196
  tokenFile: z.string().optional(),
182
197
  workspace: z.string().optional(),
@@ -185,13 +200,18 @@ const ClickClackAccountConfigSchema = z.object({
185
200
  replyMode: z.enum(["agent", "model"]).optional(),
186
201
  model: z.string().optional(),
187
202
  systemPrompt: z.string().optional(),
188
- timeoutSeconds: z.number().int().min(1).max(3600).optional(),
189
203
  toolsAllow: z.array(z.string()).optional(),
190
204
  defaultTo: z.string().optional(),
191
205
  allowFrom: z.array(z.string()).optional(),
192
206
  reconnectMs: z.number().int().min(100).max(6e4).optional(),
193
207
  agentActivity: z.boolean().optional(),
194
- commandMenu: z.boolean().optional()
208
+ commandMenu: z.boolean().optional(),
209
+ discussions: z.object({
210
+ enabled: z.boolean().optional(),
211
+ workspace: z.string().optional(),
212
+ controlUrlBase: z.string().url().optional(),
213
+ section: z.string().optional()
214
+ }).strict().optional()
195
215
  }).strict();
196
216
  /**
197
217
  * Config schema exported to core so `openclaw doctor` and config validation
@@ -222,6 +242,15 @@ const CLICKCLACK_CORRELATION_ID_PATTERN = /^[A-Za-z0-9._:-]+$/u;
222
242
  const CLICKCLACK_CORRELATION_ID_HEADER = "X-Correlation-ID";
223
243
  const CLICKCLACK_INBOUND_JSON_LIMIT_BYTES = 16 * 1024 * 1024;
224
244
  const CLICKCLACK_WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 3e4;
245
+ const CLICKCLACK_MESSAGE_PAGE_LIMIT = 200;
246
+ const CLICKCLACK_DISCUSSION_ROOT_PAGE_LIMIT = 8;
247
+ const CLICKCLACK_DISCUSSION_THREAD_REQUEST_LIMIT = 24;
248
+ function compareMessages(left, right) {
249
+ return left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id);
250
+ }
251
+ function keepLatestMessages(messages, limit) {
252
+ return messages.toSorted(compareMessages).slice(-limit);
253
+ }
225
254
  var ClickClackHttpError = class extends Error {
226
255
  constructor(status, detail, headers) {
227
256
  super(`ClickClack ${status}: ${detail}`);
@@ -229,6 +258,12 @@ var ClickClackHttpError = class extends Error {
229
258
  this.headers = headers;
230
259
  }
231
260
  };
261
+ /** Matches the workspace/name uniqueness error returned by current ClickClack servers. */
262
+ function isClickClackChannelNameConflict(error) {
263
+ if (!(error instanceof ClickClackHttpError) || error.status !== 400 && error.status !== 409) return false;
264
+ const message = error.message.toLowerCase();
265
+ return (message.includes("unique") || message.includes("duplicate")) && message.includes("channel") && /workspace.*name|name.*workspace/u.test(message);
266
+ }
232
267
  /** Accepts the same bounded request-correlation shape as the ClickClack API. */
233
268
  function normalizeClickClackCorrelationId(value) {
234
269
  if (typeof value !== "string") return;
@@ -258,7 +293,7 @@ function createClickClackClient(options) {
258
293
  });
259
294
  if (!response.ok) {
260
295
  const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
261
- throw new ClickClackHttpError(response.status, detail, new Headers(response.headers));
296
+ throw new ClickClackHttpError(response.status, redactToolPayloadText(detail), new Headers(response.headers));
262
297
  }
263
298
  return await readProviderJsonResponse(response, "ClickClack response", { maxBytes: CLICKCLACK_INBOUND_JSON_LIMIT_BYTES });
264
299
  }
@@ -289,9 +324,63 @@ function createClickClackClient(options) {
289
324
  channels: async (workspaceId) => {
290
325
  return (await request(`/api/workspaces/${encodeURIComponent(workspaceId)}/channels`)).channels;
291
326
  },
327
+ createChannel: async (workspaceId, channel) => {
328
+ return (await request(`/api/workspaces/${encodeURIComponent(workspaceId)}/channels`, {
329
+ method: "POST",
330
+ body: JSON.stringify(channel)
331
+ })).channel;
332
+ },
333
+ updateChannel: async (channelId, patch) => {
334
+ return (await request(`/api/channels/${encodeURIComponent(channelId)}`, {
335
+ method: "PATCH",
336
+ body: JSON.stringify(patch)
337
+ })).channel;
338
+ },
292
339
  channelMessages: async (channelId, afterSeq, limit = 20) => {
293
340
  return (await request(`/api/channels/${encodeURIComponent(channelId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
294
341
  },
342
+ latestChannelMessages: async (channelId, limit = 30) => {
343
+ const boundedLimit = Math.max(1, Math.min(CLICKCLACK_MESSAGE_PAGE_LIMIT, limit));
344
+ let beforeSeq;
345
+ let latest = [];
346
+ let rootPageCount = 0;
347
+ let threadRequestCount = 0;
348
+ let truncated = false;
349
+ while (true) {
350
+ rootPageCount += 1;
351
+ const query = new URLSearchParams({ limit: String(CLICKCLACK_MESSAGE_PAGE_LIMIT) });
352
+ if (beforeSeq !== void 0) query.set("before_seq", String(beforeSeq));
353
+ const page = await request(`/api/channels/${encodeURIComponent(channelId)}/messages?${query.toString()}`);
354
+ for (const root of page.messages) {
355
+ latest = keepLatestMessages([...latest, root], boundedLimit);
356
+ const lastReplyAt = root.thread_state?.last_reply_at;
357
+ const cutoff = latest.length === boundedLimit ? latest[0]?.created_at : void 0;
358
+ if (!root.thread_state?.reply_count || lastReplyAt !== void 0 && cutoff !== void 0 && lastReplyAt < cutoff) continue;
359
+ if (threadRequestCount >= CLICKCLACK_DISCUSSION_THREAD_REQUEST_LIMIT) {
360
+ truncated = true;
361
+ continue;
362
+ }
363
+ threadRequestCount += 1;
364
+ const threadQuery = new URLSearchParams({ limit: String(CLICKCLACK_MESSAGE_PAGE_LIMIT) });
365
+ const thread = await request(`/api/messages/${encodeURIComponent(root.id)}/thread?${threadQuery.toString()}`);
366
+ if (thread.replies.length < root.thread_state.reply_count) {
367
+ truncated = true;
368
+ continue;
369
+ }
370
+ latest = keepLatestMessages([...latest, ...thread.replies], boundedLimit);
371
+ }
372
+ if (!page.has_older) return {
373
+ messages: latest,
374
+ truncated
375
+ };
376
+ if (rootPageCount >= CLICKCLACK_DISCUSSION_ROOT_PAGE_LIMIT) return {
377
+ messages: latest,
378
+ truncated: true
379
+ };
380
+ if (page.messages.length === 0 || !Number.isSafeInteger(page.oldest_seq) || page.oldest_seq < 0 || page.oldest_seq === beforeSeq) throw new Error("ClickClack message pagination did not advance");
381
+ beforeSeq = page.oldest_seq;
382
+ }
383
+ },
295
384
  directMessages: async (conversationId, afterSeq, limit = 20) => {
296
385
  return (await request(`/api/dms/${encodeURIComponent(conversationId)}/messages?after_seq=${afterSeq}&limit=${limit}`)).messages;
297
386
  },
@@ -447,6 +536,59 @@ async function resolveChannelId(client, workspaceId, channel) {
447
536
  return found.id;
448
537
  }
449
538
  //#endregion
539
+ //#region extensions/clickclack/src/setup-contract.ts
540
+ const CLICKCLACK_SETUP_CODE_CLAIM_PATH = "/api/bot-setup-codes/claim";
541
+ const LOOPBACK_ADDRESSES = new net.BlockList();
542
+ LOOPBACK_ADDRESSES.addSubnet("127.0.0.0", 8, "ipv4");
543
+ LOOPBACK_ADDRESSES.addAddress("::1", "ipv6");
544
+ const BASE_PATH_SEGMENT = /^[A-Za-z0-9._~-]+$/u;
545
+ function isClickClackSetupLoopbackHost(hostname) {
546
+ const normalized = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
547
+ if (normalized === "localhost") return true;
548
+ const family = net.isIP(normalized);
549
+ return family !== 0 && LOOPBACK_ADDRESSES.check(normalized, family === 4 ? "ipv4" : "ipv6");
550
+ }
551
+ function requireClickClackSetupApiBaseUrl(value, label) {
552
+ if (!value || value !== value.trim()) throw new Error(`ClickClack ${label} is invalid`);
553
+ let parsed;
554
+ try {
555
+ parsed = new URL(value);
556
+ } catch {
557
+ throw new Error(`ClickClack ${label} is invalid`);
558
+ }
559
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || !parsed.hostname || parsed.hostname.endsWith(".") || parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error(`ClickClack ${label} is invalid`);
560
+ if (parsed.protocol === "http:" && !isClickClackSetupLoopbackHost(parsed.hostname)) throw new Error(`ClickClack ${label} must use HTTPS unless it is on loopback`);
561
+ const pathname = parsed.pathname;
562
+ let basePath = "";
563
+ if (pathname !== "/") {
564
+ if (pathname.endsWith("/") || pathname.includes("//") || pathname.includes("\\") || pathname.slice(1).split("/").some((segment) => !BASE_PATH_SEGMENT.test(segment))) throw new Error(`ClickClack ${label} is invalid`);
565
+ basePath = pathname;
566
+ }
567
+ const canonical = parsed.origin + basePath;
568
+ if (value !== canonical) throw new Error(`ClickClack ${label} is not canonical`);
569
+ return canonical;
570
+ }
571
+ function requireClickClackSetupClaimUrl(value) {
572
+ let parsed;
573
+ try {
574
+ parsed = new URL(value);
575
+ } catch {
576
+ throw new Error("ClickClack setup URL has an invalid claim endpoint.");
577
+ }
578
+ if (parsed.username || parsed.password || parsed.search || parsed.hash || !parsed.pathname.endsWith("/api/bot-setup-codes/claim")) throw new Error("ClickClack setup URL has an invalid claim endpoint.");
579
+ const basePath = parsed.pathname.slice(0, -26);
580
+ const apiBaseUrl = requireClickClackSetupApiBaseUrl(parsed.origin + basePath, "setup URL API base");
581
+ const claimUrl = apiBaseUrl + CLICKCLACK_SETUP_CODE_CLAIM_PATH;
582
+ if (value !== claimUrl) throw new Error("ClickClack setup URL has a non-canonical claim endpoint.");
583
+ return {
584
+ claimUrl,
585
+ apiBaseUrl
586
+ };
587
+ }
588
+ function buildClickClackSetupClaimUrl(baseUrl) {
589
+ return baseUrl.replace(/\/+$/u, "") + CLICKCLACK_SETUP_CODE_CLAIM_PATH;
590
+ }
591
+ //#endregion
450
592
  //#region extensions/clickclack/src/setup-core.ts
451
593
  const channel$1 = "clickclack";
452
594
  const SETUP_CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -489,19 +631,30 @@ function parseClickClackSetupCodeInput(params) {
489
631
  }
490
632
  if (setupUrl.protocol !== "http:" && setupUrl.protocol !== "https:") throw new Error("ClickClack setup codes require an HTTP(S) URL.");
491
633
  if (setupUrl.username || setupUrl.password) throw new Error("ClickClack setup URLs must not include credentials.");
634
+ if (setupUrl.search) throw new Error("ClickClack setup URLs must not include a query.");
492
635
  code = setupUrl.hash.slice(1);
493
636
  if (!code) throw new Error("ClickClack setup URL is missing its #CODE fragment.");
494
637
  setupUrl.hash = "";
495
- setupUrl.search = "";
496
- baseUrl = requireClickClackSetupCodeBaseUrl(setupUrl.toString());
638
+ let exactClaimUrl;
639
+ if (setupUrl.pathname.endsWith("/api/bot-setup-codes/claim")) {
640
+ const exactEndpoint = requireClickClackSetupClaimUrl(setupUrl.toString());
641
+ baseUrl = exactEndpoint.apiBaseUrl;
642
+ exactClaimUrl = exactEndpoint.claimUrl;
643
+ } else baseUrl = requireClickClackSetupCodeBaseUrl(setupUrl.toString());
497
644
  if (params.baseUrl) {
498
645
  if (requireClickClackSetupCodeBaseUrl(params.baseUrl) !== baseUrl) throw new Error("ClickClack --base-url does not match the server in the setup-code URL.");
499
646
  }
500
- } else {
501
- code = code.startsWith("#") ? code.slice(1) : code;
502
- if (!params.baseUrl) throw new Error("A bare ClickClack setup code requires --base-url.");
503
- baseUrl = requireClickClackSetupCodeBaseUrl(params.baseUrl);
647
+ const normalizedCode = normalizeClickClackSetupCode(code);
648
+ if (!normalizedCode) throw new Error("ClickClack setup code must contain 12 valid base32 characters.");
649
+ return {
650
+ code: normalizedCode,
651
+ baseUrl,
652
+ ...exactClaimUrl ? { exactClaimUrl } : {}
653
+ };
504
654
  }
655
+ code = code.startsWith("#") ? code.slice(1) : code;
656
+ if (!params.baseUrl) throw new Error("A bare ClickClack setup code requires --base-url.");
657
+ baseUrl = requireClickClackSetupCodeBaseUrl(params.baseUrl);
505
658
  const normalizedCode = normalizeClickClackSetupCode(code);
506
659
  if (!normalizedCode) throw new Error("ClickClack setup code must contain 12 valid base32 characters.");
507
660
  return {
@@ -522,7 +675,8 @@ function applyClickClackSetupConfigPatch(params) {
522
675
  const namedConfig = applyAccountNameToChannelSection({
523
676
  cfg: accountId === DEFAULT_ACCOUNT_ID ? params.cfg : moveSingleAccountChannelSectionToDefaultAccount({
524
677
  cfg: params.cfg,
525
- channelKey: channel$1
678
+ channelKey: channel$1,
679
+ setupSurface: clickClackSetupAdapter
526
680
  }),
527
681
  channelKey: channel$1,
528
682
  accountId,
@@ -585,21 +739,32 @@ function applyClickClackCredentialConfig(params) {
585
739
  }
586
740
  const clickClackSetupAdapter = {
587
741
  resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
588
- prepareAccountConfigInput: async ({ input }) => {
589
- if (!input.code?.trim()) return input;
590
- if (input.token?.trim() || input.tokenFile?.trim() || input.useEnv) throw new Error(SETUP_CODE_CONFLICT_ERROR);
591
- const setup = parseClickClackSetupCodeInput({
592
- code: input.code,
593
- baseUrl: input.baseUrl
742
+ prepareAccountConfigInput: async ({ cfg, accountId, input }) => {
743
+ const setupInput = input;
744
+ if (!setupInput.code?.trim()) return setupInput;
745
+ if (setupInput.token?.trim() || setupInput.tokenFile?.trim() || setupInput.useEnv) throw new Error(SETUP_CODE_CONFLICT_ERROR);
746
+ let setup = parseClickClackSetupCodeInput({
747
+ code: setupInput.code,
748
+ baseUrl: setupInput.baseUrl
594
749
  });
750
+ const privateApiBaseUrl = normalizeClickClackBaseUrl(resolveClickClackAccountConfig(cfg, accountId).apiBaseUrl);
751
+ const claimUrl = setup.exactClaimUrl && !privateApiBaseUrl ? setup.exactClaimUrl : buildClickClackSetupClaimUrl(privateApiBaseUrl ?? setup.baseUrl);
595
752
  let claim;
596
753
  try {
597
- const { claimClickClackSetupCode } = await import("./setup-claim-BUWvZjtu.js");
598
- claim = await claimClickClackSetupCode(setup);
754
+ const { claimClickClackSetupCode } = await import("./setup-claim-CJIUfZS7.js");
755
+ claim = await claimClickClackSetupCode({
756
+ claimUrl,
757
+ code: setup.code,
758
+ ...setup.exactClaimUrl ? { expectedClaimUrl: setup.exactClaimUrl } : {}
759
+ });
599
760
  } catch (error) {
600
761
  throw formatClickClackSetupCodeClaimError(error);
601
762
  }
602
- const { code: _code, tokenFile: _tokenFile, useEnv: _useEnv, ...remainingInput } = input;
763
+ setup = {
764
+ ...setup,
765
+ baseUrl: claim.api_base_url ?? setup.baseUrl
766
+ };
767
+ const { code: _code, tokenFile: _tokenFile, useEnv: _useEnv, ...remainingInput } = setupInput;
603
768
  return {
604
769
  ...remainingInput,
605
770
  baseUrl: setup.baseUrl,
@@ -633,40 +798,42 @@ const clickClackSetupAdapter = {
633
798
  }
634
799
  ],
635
800
  validate: ({ cfg, accountId, input }) => {
636
- const baseUrl = normalizeClickClackBaseUrl(input.baseUrl);
637
- if (input.baseUrl && !baseUrl) return INVALID_BASE_URL_ERROR;
638
- if (!input.useEnv) return null;
801
+ const setupInput = input;
802
+ const baseUrl = normalizeClickClackBaseUrl(setupInput.baseUrl);
803
+ if (setupInput.baseUrl && !baseUrl) return INVALID_BASE_URL_ERROR;
804
+ if (!setupInput.useEnv) return null;
639
805
  const existing = resolveClickClackAccountConfig(cfg, accountId);
640
806
  const existingBaseUrl = normalizeClickClackBaseUrl(existing.baseUrl);
641
807
  if (!baseUrl && existing.baseUrl?.trim() && !existingBaseUrl) return INVALID_BASE_URL_ERROR;
642
808
  if (!baseUrl && !existingBaseUrl) return REQUIRED_INPUT_ERROR;
643
- if (!input.workspace?.trim() && !existing.workspace?.trim()) return REQUIRED_INPUT_ERROR;
809
+ if (!setupInput.workspace?.trim() && !existing.workspace?.trim()) return REQUIRED_INPUT_ERROR;
644
810
  return null;
645
811
  }
646
812
  }),
647
813
  applyAccountConfig: ({ cfg, accountId, input }) => {
648
- const existing = input.useEnv ? resolveClickClackAccountConfig(cfg, accountId) : void 0;
649
- const baseUrl = normalizeClickClackBaseUrl(input.baseUrl ?? existing?.baseUrl);
650
- const workspace = input.workspace?.trim() || existing?.workspace?.trim();
651
- const tokenFile = input.tokenFile?.trim();
652
- const token = input.token?.trim();
814
+ const setupInput = input;
815
+ const existing = setupInput.useEnv ? resolveClickClackAccountConfig(cfg, accountId) : void 0;
816
+ const baseUrl = normalizeClickClackBaseUrl(setupInput.baseUrl ?? existing?.baseUrl);
817
+ const workspace = setupInput.workspace?.trim() || existing?.workspace?.trim();
818
+ const tokenFile = setupInput.tokenFile?.trim();
819
+ const token = setupInput.token?.trim();
653
820
  return applyClickClackCredentialConfig({
654
821
  cfg: applyClickClackSetupConfigPatch({
655
822
  cfg,
656
823
  accountId,
657
- name: input.name,
824
+ name: setupInput.name,
658
825
  patch: {
659
826
  ...baseUrl ? { baseUrl } : {},
660
827
  ...workspace ? { workspace } : {},
661
- ...input.defaultTo?.trim() ? { defaultTo: input.defaultTo.trim() } : {},
662
- ...input.allowFrom ? { allowFrom: [...input.allowFrom] } : {},
663
- ...input.agentActivity !== void 0 ? { agentActivity: input.agentActivity } : {}
828
+ ...setupInput.defaultTo?.trim() ? { defaultTo: setupInput.defaultTo.trim() } : {},
829
+ ...setupInput.allowFrom ? { allowFrom: [...setupInput.allowFrom] } : {},
830
+ ...setupInput.agentActivity !== void 0 ? { agentActivity: setupInput.agentActivity } : {}
664
831
  }
665
832
  }),
666
833
  accountId,
667
834
  token,
668
835
  tokenFile,
669
- useEnv: input.useEnv
836
+ useEnv: setupInput.useEnv
670
837
  });
671
838
  },
672
839
  afterAccountConfigWritten: async ({ cfg, accountId, runtime }) => {
@@ -678,6 +845,77 @@ const clickClackSetupAdapter = {
678
845
  });
679
846
  }
680
847
  };
848
+ const clickClackSetupContract = defineChannelSetupContract({
849
+ fields: {
850
+ code: {
851
+ kind: "string",
852
+ sensitive: true,
853
+ cli: {
854
+ flags: "--code <code>",
855
+ description: "ClickClack one-time setup code or setup URL"
856
+ }
857
+ },
858
+ token: {
859
+ kind: "string",
860
+ sensitive: true,
861
+ cli: {
862
+ flags: "--token <token>",
863
+ description: "ClickClack bot token"
864
+ }
865
+ },
866
+ tokenFile: {
867
+ kind: "string",
868
+ sensitive: true,
869
+ cli: {
870
+ flags: "--token-file <path>",
871
+ description: "ClickClack bot token file"
872
+ }
873
+ },
874
+ baseUrl: {
875
+ kind: "string",
876
+ cli: {
877
+ flags: "--base-url <url>",
878
+ description: "ClickClack API base URL"
879
+ }
880
+ },
881
+ workspace: {
882
+ kind: "string",
883
+ cli: {
884
+ flags: "--workspace <workspace>",
885
+ description: "ClickClack workspace id, slug, or name"
886
+ }
887
+ },
888
+ defaultTo: {
889
+ kind: "string",
890
+ cli: {
891
+ flags: "--default-to <target>",
892
+ description: "Default ClickClack target"
893
+ }
894
+ },
895
+ allowFrom: {
896
+ kind: "string-list",
897
+ cli: {
898
+ flags: "--allow-from <ids>",
899
+ description: "Allowed ClickClack senders"
900
+ }
901
+ },
902
+ agentActivity: {
903
+ kind: "boolean",
904
+ cli: {
905
+ flags: "--agent-activity",
906
+ description: "Enable ClickClack agent activity"
907
+ }
908
+ },
909
+ useEnv: {
910
+ kind: "boolean",
911
+ cli: {
912
+ flags: "--use-env",
913
+ description: "Use CLICKCLACK_BOT_TOKEN"
914
+ }
915
+ }
916
+ },
917
+ legacyAdapter: clickClackSetupAdapter
918
+ });
681
919
  //#endregion
682
920
  //#region extensions/clickclack/src/setup-verify.ts
683
921
  var setup_verify_exports = /* @__PURE__ */ __exportAll({
@@ -708,7 +946,7 @@ async function checkClickClackSetupConnection(params) {
708
946
  if (usesUnavailableImplicitEnvToken(account, token)) return { status: "skipped-env-token" };
709
947
  if (!account.baseUrl || !account.workspace || !token) return { status: "skipped-unconfigured" };
710
948
  const client = createClickClackClient({
711
- baseUrl: account.baseUrl,
949
+ baseUrl: account.apiEndpoint,
712
950
  token
713
951
  });
714
952
  const me = await client.me();
@@ -908,4 +1146,4 @@ const clickClackSetupWizard = {
908
1146
  disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
909
1147
  };
910
1148
  //#endregion
911
- export { createClickClackClient as a, CLICKCLACK_CHANNEL_ID as c, DEFAULT_ACCOUNT_ID$2 as d, listClickClackAccountIds as f, resolveDefaultClickClackAccountId as h, resolveWorkspaceId as i, clickClackConfigAdapter as l, resolveClickClackAccount as m, clickClackSetupAdapter as n, normalizeClickClackCorrelationId as o, listEnabledClickClackAccounts as p, resolveChannelId as r, clickClackConfigSchema as s, clickClackSetupWizard as t, clickClackMeta as u };
1149
+ export { resolveDefaultClickClackAccountId as S, clickClackMeta as _, buildClickClackSetupClaimUrl as a, listEnabledClickClackAccounts as b, resolveChannelId as c, createClickClackClient as d, isClickClackChannelNameConflict as f, clickClackConfigAdapter as g, CLICKCLACK_CHANNEL_ID as h, CLICKCLACK_SETUP_CODE_CLAIM_PATH as i, resolveWorkspaceId as l, clickClackConfigSchema as m, clickClackSetupAdapter as n, isClickClackSetupLoopbackHost as o, normalizeClickClackCorrelationId as p, clickClackSetupContract as r, requireClickClackSetupApiBaseUrl as s, clickClackSetupWizard as t, ClickClackHttpError as u, DEFAULT_ACCOUNT_ID$2 as v, resolveClickClackAccount as x, listClickClackAccountIds as y };