@kodelyth/discord 2026.5.39 → 2026.6.1

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.
Files changed (126) hide show
  1. package/dist/account-inspect-Dqw-enky.js +81 -0
  2. package/dist/account-inspect-api.js +10 -0
  3. package/dist/accounts-B7OBFePq.js +224 -0
  4. package/dist/action-runtime-api.js +2 -0
  5. package/dist/agent-components.runtime-DVY_1VB4.js +4 -0
  6. package/dist/allow-list-B0s7evD7.js +354 -0
  7. package/dist/api-CXAcv9nZ.js +130 -0
  8. package/dist/api.js +23 -0
  9. package/dist/approval-handler.runtime-B9xUAF3n.js +426 -0
  10. package/dist/audit-DoiK49WO.js +24 -0
  11. package/dist/audit-core-BGrq3G7r.js +105 -0
  12. package/dist/channel-U_aeoFwW.js +795 -0
  13. package/dist/channel-actions-BxEBnEuv.js +173 -0
  14. package/dist/channel-actions.runtime-CPtpH-yl.js +263 -0
  15. package/dist/channel-api-BfjklLby.js +21 -0
  16. package/dist/channel-config-api.js +2 -0
  17. package/dist/channel-plugin-api.js +2 -0
  18. package/dist/channel.setup-BUSC0apv.js +337 -0
  19. package/dist/components-luonoe13.js +909 -0
  20. package/dist/config-api-DSYGqaLQ.js +2 -0
  21. package/dist/config-schema-DIqJBGwC.js +357 -0
  22. package/dist/configured-state.js +6 -0
  23. package/dist/contract-api.js +8 -0
  24. package/dist/conversation-identity-DXAm0_Mk.js +270 -0
  25. package/dist/directory-config-CYbuMmPS.js +49 -0
  26. package/dist/directory-contract-api.js +2 -0
  27. package/dist/directory-live-DX4dLRpJ.js +159 -0
  28. package/dist/doctor-bbKSvGVD.js +244 -0
  29. package/dist/doctor-contract-Btjt6NJD.js +383 -0
  30. package/dist/doctor-contract-api.js +2 -0
  31. package/dist/gateway-registry-BKSpa4GB.js +74 -0
  32. package/dist/handle-action.guild-admin-B5BArS2n.js +286 -0
  33. package/dist/inbound-context-WAOqhGlT.js +48 -0
  34. package/dist/inbound-event-delivery-C-1Ji3WP.js +65 -0
  35. package/dist/index.js +26 -0
  36. package/dist/manager.runtime-DXHynKE4.js +2356 -0
  37. package/dist/message-handler-mXzc3tA_.js +381 -0
  38. package/dist/message-handler.preflight-BPD1a347.js +1113 -0
  39. package/dist/message-handler.process-GUa3aV8z.js +1438 -0
  40. package/dist/message-utils-dUbem16p.js +549 -0
  41. package/dist/outbound-adapter-C18OAc1y.js +536 -0
  42. package/dist/pluralkit-D1Q2x0w5.js +22 -0
  43. package/dist/preflight-audio-CZtpWcIm.js +72 -0
  44. package/dist/preflight-audio.runtime-Brx_0_xW.js +7 -0
  45. package/dist/preview-streaming-D_slNIiO.js +8 -0
  46. package/dist/probe-D--Ca4JF.js +139 -0
  47. package/dist/probe.runtime-DQBchZzv.js +2 -0
  48. package/dist/provider-B2-31CIT.js +9565 -0
  49. package/dist/provider-session.runtime-BwzzSsrH.js +6 -0
  50. package/dist/provider.runtime-CP3oHLls.js +2 -0
  51. package/dist/resolve-allowlist-common-CqxPLcJO.js +34 -0
  52. package/dist/resolve-channels-0LX4pUbB.js +265 -0
  53. package/dist/resolve-users-CztOv0Qs.js +120 -0
  54. package/dist/runtime-DUaw66V_.js +1073 -0
  55. package/dist/runtime-api.actions.js +3 -0
  56. package/dist/runtime-api.js +30 -0
  57. package/dist/runtime-api.lookup.js +7 -0
  58. package/dist/runtime-api.monitor-CvVKvEXW.js +5 -0
  59. package/dist/runtime-api.monitor.js +8 -0
  60. package/dist/runtime-api.send.js +6 -0
  61. package/dist/runtime-api.threads.js +6 -0
  62. package/dist/runtime-fC6f4UF2.js +8 -0
  63. package/dist/runtime-setter-api.js +2 -0
  64. package/dist/secret-config-contract-B6WW5V88.js +115 -0
  65. package/dist/secret-contract-api.js +2 -0
  66. package/dist/security-audit-CnyIQKz6.js +120 -0
  67. package/dist/security-audit-contract-api.js +2 -0
  68. package/dist/security-audit.runtime-CQSkjNLu.js +2 -0
  69. package/dist/security-contract-DLvYOgLM.js +26 -0
  70. package/dist/security-contract-api.js +2 -0
  71. package/dist/security-doctor-DepqtNCI.js +18 -0
  72. package/dist/send-DCtPCHGk.js +881 -0
  73. package/dist/send.components-Bcgxvm52.js +474 -0
  74. package/dist/send.outbound-S9t0UuHc.js +330 -0
  75. package/dist/send.receipt-CDn3GBWC.js +3119 -0
  76. package/dist/send.shared-D4iBnAmn.js +669 -0
  77. package/dist/sender-identity-CxCe3_1a.js +43 -0
  78. package/dist/session-contract-Dwhw3RTY.js +6 -0
  79. package/dist/session-key-api.js +2 -0
  80. package/dist/session-key-normalization-CP8dPUid.js +23 -0
  81. package/dist/setup-entry.js +11 -0
  82. package/dist/setup-plugin-api.js +2 -0
  83. package/dist/shared-AIlvuZXt.js +171 -0
  84. package/dist/subagent-hooks-8bK-mgiU.js +120 -0
  85. package/dist/subagent-hooks-api.js +22 -0
  86. package/dist/system-events-Ba1TklaL.js +34 -0
  87. package/dist/target-resolver-BrtFQtoK.js +82 -0
  88. package/dist/targets-DWLLZE2l.js +3 -0
  89. package/dist/test-api.js +45 -0
  90. package/dist/thread-binding-api.js +4 -0
  91. package/dist/thread-bindings-9aKRmZv0.js +255 -0
  92. package/dist/thread-bindings.discord-api-ssGH5wc2.js +244 -0
  93. package/dist/thread-bindings.manager-0YBHGemk.js +534 -0
  94. package/dist/thread-bindings.session-updates-DJZGIwaU.js +54 -0
  95. package/dist/thread-bindings.state-eTFl-PqJ.js +318 -0
  96. package/dist/timeouts-CEwuGaWT.js +52 -0
  97. package/dist/timeouts.js +2 -0
  98. package/dist/typing-BmJKRpCS.js +14 -0
  99. package/package.json +19 -7
  100. package/account-inspect-api.js +0 -7
  101. package/action-runtime-api.js +0 -7
  102. package/api.js +0 -7
  103. package/channel-config-api.js +0 -7
  104. package/channel-plugin-api.js +0 -7
  105. package/configured-state.js +0 -7
  106. package/contract-api.js +0 -7
  107. package/directory-contract-api.js +0 -7
  108. package/doctor-contract-api.js +0 -7
  109. package/index.js +0 -7
  110. package/runtime-api.actions.js +0 -7
  111. package/runtime-api.js +0 -7
  112. package/runtime-api.lookup.js +0 -7
  113. package/runtime-api.monitor.js +0 -7
  114. package/runtime-api.send.js +0 -7
  115. package/runtime-api.threads.js +0 -7
  116. package/runtime-setter-api.js +0 -7
  117. package/secret-contract-api.js +0 -7
  118. package/security-audit-contract-api.js +0 -7
  119. package/security-contract-api.js +0 -7
  120. package/session-key-api.js +0 -7
  121. package/setup-entry.js +0 -7
  122. package/setup-plugin-api.js +0 -7
  123. package/subagent-hooks-api.js +0 -7
  124. package/test-api.js +0 -7
  125. package/thread-binding-api.js +0 -7
  126. package/timeouts.js +0 -7
@@ -0,0 +1,49 @@
1
+ import { Wt as __exportAll } from "./send.receipt-CDn3GBWC.js";
2
+ import { a as mergeDiscordAccountConfig, c as resolveDiscordAccountAllowFrom, o as resolveDefaultDiscordAccountId } from "./accounts-B7OBFePq.js";
3
+ import { normalizeAccountId } from "klaw/plugin-sdk/account-id";
4
+ import { createResolvedDirectoryEntriesLister } from "klaw/plugin-sdk/directory-config-runtime";
5
+ //#region extensions/discord/src/directory-config.ts
6
+ var directory_config_exports = /* @__PURE__ */ __exportAll({
7
+ listDiscordDirectoryGroupsFromConfig: () => listDiscordDirectoryGroupsFromConfig,
8
+ listDiscordDirectoryPeersFromConfig: () => listDiscordDirectoryPeersFromConfig
9
+ });
10
+ function resolveDiscordDirectoryConfigAccount(cfg, accountId) {
11
+ const resolvedAccountId = normalizeAccountId(accountId ?? resolveDefaultDiscordAccountId(cfg));
12
+ const config = mergeDiscordAccountConfig(cfg, resolvedAccountId);
13
+ return {
14
+ accountId: resolvedAccountId,
15
+ config,
16
+ allowFrom: resolveDiscordAccountAllowFrom({
17
+ cfg,
18
+ accountId: resolvedAccountId
19
+ }) ?? [],
20
+ dm: config.dm
21
+ };
22
+ }
23
+ const listDiscordDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister({
24
+ kind: "user",
25
+ resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
26
+ resolveSources: (account) => {
27
+ const guildUsers = Object.values(account.config.guilds ?? {}).flatMap((guild) => (guild.users ?? []).concat(Object.values(guild.channels ?? {}).flatMap((channel) => channel.users ?? [])));
28
+ return [
29
+ account.allowFrom,
30
+ Object.keys(account.config.dms ?? {}),
31
+ guildUsers
32
+ ];
33
+ },
34
+ normalizeId: (raw) => {
35
+ const cleaned = (raw.match(/^<@!?(\d+)>$/)?.[1] ?? raw).replace(/^(discord|user):/i, "").trim();
36
+ return /^\d+$/.test(cleaned) ? `user:${cleaned}` : null;
37
+ }
38
+ });
39
+ const listDiscordDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister({
40
+ kind: "group",
41
+ resolveAccount: (cfg, accountId) => resolveDiscordDirectoryConfigAccount(cfg, accountId),
42
+ resolveSources: (account) => Object.values(account.config.guilds ?? {}).map((guild) => Object.keys(guild.channels ?? {})),
43
+ normalizeId: (raw) => {
44
+ const cleaned = (raw.match(/^<#(\d+)>$/)?.[1] ?? raw).replace(/^(discord|channel|group):/i, "").trim();
45
+ return /^\d+$/.test(cleaned) ? `channel:${cleaned}` : null;
46
+ }
47
+ });
48
+ //#endregion
49
+ export { listDiscordDirectoryGroupsFromConfig as n, listDiscordDirectoryPeersFromConfig as r, directory_config_exports as t };
@@ -0,0 +1,2 @@
1
+ import { n as listDiscordDirectoryGroupsFromConfig, r as listDiscordDirectoryPeersFromConfig } from "./directory-config-CYbuMmPS.js";
2
+ export { listDiscordDirectoryGroupsFromConfig, listDiscordDirectoryPeersFromConfig };
@@ -0,0 +1,159 @@
1
+ import { Wt as __exportAll } from "./send.receipt-CDn3GBWC.js";
2
+ import { p as normalizeDiscordToken, s as resolveDiscordAccount } from "./accounts-B7OBFePq.js";
3
+ import { n as fetchDiscord } from "./api-CXAcv9nZ.js";
4
+ import { a as normalizeDiscordSlug } from "./allow-list-B0s7evD7.js";
5
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeOptionalStringifiedId } from "klaw/plugin-sdk/string-coerce-runtime";
6
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "klaw/plugin-sdk/routing";
7
+ //#region extensions/discord/src/directory-cache.ts
8
+ const DISCORD_DIRECTORY_CACHE_MAX_ENTRIES = 4e3;
9
+ const DISCORD_DISCRIMINATOR_SUFFIX = /#\d{4}$/;
10
+ const DIRECTORY_HANDLE_CACHE = /* @__PURE__ */ new Map();
11
+ function normalizeAccountCacheKey(accountId) {
12
+ return normalizeAccountId(accountId ?? DEFAULT_ACCOUNT_ID) || DEFAULT_ACCOUNT_ID;
13
+ }
14
+ function normalizeSnowflake(value) {
15
+ const text = normalizeOptionalStringifiedId(value) ?? "";
16
+ if (!/^\d+$/.test(text)) return null;
17
+ return text;
18
+ }
19
+ function normalizeHandleKey(raw) {
20
+ let handle = normalizeOptionalString(raw) ?? "";
21
+ if (!handle) return null;
22
+ if (handle.startsWith("@")) handle = normalizeOptionalString(handle.slice(1)) ?? "";
23
+ if (!handle || /\s/.test(handle)) return null;
24
+ return normalizeLowercaseStringOrEmpty(handle);
25
+ }
26
+ function ensureAccountCache(accountId) {
27
+ const cacheKey = normalizeAccountCacheKey(accountId);
28
+ const existing = DIRECTORY_HANDLE_CACHE.get(cacheKey);
29
+ if (existing) return existing;
30
+ const created = /* @__PURE__ */ new Map();
31
+ DIRECTORY_HANDLE_CACHE.set(cacheKey, created);
32
+ return created;
33
+ }
34
+ function setCacheEntry(cache, key, userId) {
35
+ if (cache.has(key)) cache.delete(key);
36
+ cache.set(key, userId);
37
+ if (cache.size <= DISCORD_DIRECTORY_CACHE_MAX_ENTRIES) return;
38
+ const oldest = cache.keys().next();
39
+ if (!oldest.done) cache.delete(oldest.value);
40
+ }
41
+ function rememberDiscordDirectoryUser(params) {
42
+ const userId = normalizeSnowflake(params.userId);
43
+ if (!userId) return;
44
+ const cache = ensureAccountCache(params.accountId);
45
+ for (const candidate of params.handles) {
46
+ if (typeof candidate !== "string") continue;
47
+ const handle = normalizeHandleKey(candidate);
48
+ if (!handle) continue;
49
+ setCacheEntry(cache, handle, userId);
50
+ const withoutDiscriminator = handle.replace(DISCORD_DISCRIMINATOR_SUFFIX, "");
51
+ if (withoutDiscriminator && withoutDiscriminator !== handle) setCacheEntry(cache, withoutDiscriminator, userId);
52
+ }
53
+ }
54
+ function resolveDiscordDirectoryUserId(params) {
55
+ const cache = DIRECTORY_HANDLE_CACHE.get(normalizeAccountCacheKey(params.accountId));
56
+ if (!cache) return;
57
+ const handle = normalizeHandleKey(params.handle);
58
+ if (!handle) return;
59
+ const direct = cache.get(handle);
60
+ if (direct) return direct;
61
+ const withoutDiscriminator = handle.replace(DISCORD_DISCRIMINATOR_SUFFIX, "");
62
+ if (!withoutDiscriminator || withoutDiscriminator === handle) return;
63
+ return cache.get(withoutDiscriminator);
64
+ }
65
+ //#endregion
66
+ //#region extensions/discord/src/directory-live.ts
67
+ var directory_live_exports = /* @__PURE__ */ __exportAll({
68
+ listDiscordDirectoryGroupsLive: () => listDiscordDirectoryGroupsLive,
69
+ listDiscordDirectoryPeersLive: () => listDiscordDirectoryPeersLive
70
+ });
71
+ function normalizeQuery(value) {
72
+ return normalizeOptionalLowercaseString(value) ?? "";
73
+ }
74
+ function buildUserRank(user) {
75
+ return user.bot ? 0 : 1;
76
+ }
77
+ function resolveDiscordDirectoryAccess(params) {
78
+ const account = resolveDiscordAccount({
79
+ cfg: params.cfg,
80
+ accountId: params.accountId
81
+ });
82
+ const token = normalizeDiscordToken(account.token, "channels.discord.token");
83
+ if (!token) return null;
84
+ return {
85
+ token,
86
+ query: normalizeQuery(params.query),
87
+ accountId: account.accountId
88
+ };
89
+ }
90
+ async function listDiscordGuilds(token) {
91
+ return (await fetchDiscord("/users/@me/guilds", token)).filter((guild) => guild.id && guild.name);
92
+ }
93
+ async function listDiscordDirectoryGroupsLive(params) {
94
+ const access = resolveDiscordDirectoryAccess(params);
95
+ if (!access) return [];
96
+ const { token, query } = access;
97
+ const guilds = await listDiscordGuilds(token);
98
+ const rows = [];
99
+ for (const guild of guilds) {
100
+ const channels = await fetchDiscord(`/guilds/${guild.id}/channels`, token);
101
+ for (const channel of channels) {
102
+ const name = channel.name?.trim();
103
+ if (!name) continue;
104
+ if (query && !normalizeDiscordSlug(name).includes(normalizeDiscordSlug(query))) continue;
105
+ rows.push({
106
+ kind: "group",
107
+ id: `channel:${channel.id}`,
108
+ name,
109
+ handle: `#${name}`,
110
+ raw: channel
111
+ });
112
+ if (typeof params.limit === "number" && params.limit > 0 && rows.length >= params.limit) return rows;
113
+ }
114
+ }
115
+ return rows;
116
+ }
117
+ async function listDiscordDirectoryPeersLive(params) {
118
+ const access = resolveDiscordDirectoryAccess(params);
119
+ if (!access) return [];
120
+ const { token, query, accountId } = access;
121
+ if (!query) return [];
122
+ const guilds = await listDiscordGuilds(token);
123
+ const rows = [];
124
+ const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 25;
125
+ for (const guild of guilds) {
126
+ const paramsObj = new URLSearchParams({
127
+ query,
128
+ limit: String(Math.min(limit, 100))
129
+ });
130
+ const members = await fetchDiscord(`/guilds/${guild.id}/members/search?${paramsObj.toString()}`, token);
131
+ for (const member of members) {
132
+ const user = member.user;
133
+ if (!user?.id) continue;
134
+ rememberDiscordDirectoryUser({
135
+ accountId,
136
+ userId: user.id,
137
+ handles: [
138
+ user.username,
139
+ user.global_name,
140
+ member.nick,
141
+ user.username ? `@${user.username}` : null
142
+ ]
143
+ });
144
+ const name = member.nick?.trim() || user.global_name?.trim() || user.username?.trim();
145
+ rows.push({
146
+ kind: "user",
147
+ id: `user:${user.id}`,
148
+ name: name || void 0,
149
+ handle: user.username ? `@${user.username}` : void 0,
150
+ rank: buildUserRank(user),
151
+ raw: member
152
+ });
153
+ if (rows.length >= limit) return rows;
154
+ }
155
+ }
156
+ return rows;
157
+ }
158
+ //#endregion
159
+ export { resolveDiscordDirectoryUserId as a, rememberDiscordDirectoryUser as i, listDiscordDirectoryGroupsLive as n, listDiscordDirectoryPeersLive as r, directory_live_exports as t };
@@ -0,0 +1,244 @@
1
+ import { o as resolveDefaultDiscordAccountId } from "./accounts-B7OBFePq.js";
2
+ import { t as inspectDiscordAccount } from "./account-inspect-Dqw-enky.js";
3
+ import { r as DISCORD_LEGACY_CONFIG_RULES } from "./shared-AIlvuZXt.js";
4
+ import { n as normalizeCompatibilityConfig } from "./doctor-contract-Btjt6NJD.js";
5
+ import { t as isDiscordMutableAllowEntry } from "./security-doctor-DepqtNCI.js";
6
+ import { normalizeOptionalString } from "klaw/plugin-sdk/string-coerce-runtime";
7
+ import { collectProviderDangerousNameMatchingScopes } from "klaw/plugin-sdk/runtime-doctor";
8
+ //#region extensions/discord/src/doctor.ts
9
+ function asObjectRecord$1(value) {
10
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
11
+ }
12
+ function sanitizeForLog(value) {
13
+ return value.replace(/\p{Cc}+/gu, " ").trim();
14
+ }
15
+ function collectDiscordAccountScopes(cfg) {
16
+ const scopes = [];
17
+ const discord = asObjectRecord$1(cfg.channels?.discord);
18
+ if (!discord) return scopes;
19
+ scopes.push({
20
+ prefix: "channels.discord",
21
+ account: discord
22
+ });
23
+ const accounts = asObjectRecord$1(discord.accounts);
24
+ if (!accounts) return scopes;
25
+ for (const key of Object.keys(accounts)) {
26
+ const account = asObjectRecord$1(accounts[key]);
27
+ if (account) scopes.push({
28
+ prefix: `channels.discord.accounts.${key}`,
29
+ account
30
+ });
31
+ }
32
+ return scopes;
33
+ }
34
+ function collectDiscordIdLists(prefix, account) {
35
+ const refs = [{
36
+ pathLabel: `${prefix}.allowFrom`,
37
+ holder: account,
38
+ key: "allowFrom"
39
+ }];
40
+ const dm = asObjectRecord$1(account.dm);
41
+ if (dm) {
42
+ refs.push({
43
+ pathLabel: `${prefix}.dm.allowFrom`,
44
+ holder: dm,
45
+ key: "allowFrom"
46
+ });
47
+ refs.push({
48
+ pathLabel: `${prefix}.dm.groupChannels`,
49
+ holder: dm,
50
+ key: "groupChannels"
51
+ });
52
+ }
53
+ const execApprovals = asObjectRecord$1(account.execApprovals);
54
+ if (execApprovals) refs.push({
55
+ pathLabel: `${prefix}.execApprovals.approvers`,
56
+ holder: execApprovals,
57
+ key: "approvers"
58
+ });
59
+ const guilds = asObjectRecord$1(account.guilds);
60
+ if (!guilds) return refs;
61
+ for (const guildId of Object.keys(guilds)) {
62
+ const guild = asObjectRecord$1(guilds[guildId]);
63
+ if (!guild) continue;
64
+ refs.push({
65
+ pathLabel: `${prefix}.guilds.${guildId}.users`,
66
+ holder: guild,
67
+ key: "users"
68
+ });
69
+ refs.push({
70
+ pathLabel: `${prefix}.guilds.${guildId}.roles`,
71
+ holder: guild,
72
+ key: "roles"
73
+ });
74
+ const channels = asObjectRecord$1(guild.channels);
75
+ if (!channels) continue;
76
+ for (const channelId of Object.keys(channels)) {
77
+ const channel = asObjectRecord$1(channels[channelId]);
78
+ if (!channel) continue;
79
+ refs.push({
80
+ pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.users`,
81
+ holder: channel,
82
+ key: "users"
83
+ });
84
+ refs.push({
85
+ pathLabel: `${prefix}.guilds.${guildId}.channels.${channelId}.roles`,
86
+ holder: channel,
87
+ key: "roles"
88
+ });
89
+ }
90
+ }
91
+ return refs;
92
+ }
93
+ function scanDiscordNumericIdEntries(cfg) {
94
+ const hits = [];
95
+ const scanList = (pathLabel, list) => {
96
+ if (!Array.isArray(list)) return;
97
+ for (const [index, entry] of list.entries()) {
98
+ if (typeof entry !== "number") continue;
99
+ hits.push({
100
+ path: `${pathLabel}[${index}]`,
101
+ entry,
102
+ safe: Number.isSafeInteger(entry) && entry >= 0
103
+ });
104
+ }
105
+ };
106
+ for (const scope of collectDiscordAccountScopes(cfg)) for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) scanList(ref.pathLabel, ref.holder[ref.key]);
107
+ return hits;
108
+ }
109
+ function collectDiscordNumericIdWarnings(params) {
110
+ if (params.hits.length === 0) return [];
111
+ const hitsByListPath = /* @__PURE__ */ new Map();
112
+ for (const hit of params.hits) {
113
+ const listPath = hit.path.replace(/\[\d+\]$/, "");
114
+ const existing = hitsByListPath.get(listPath);
115
+ if (existing) existing.push(hit);
116
+ else hitsByListPath.set(listPath, [hit]);
117
+ }
118
+ const repairableHits = [];
119
+ const blockedHits = [];
120
+ for (const hits of hitsByListPath.values()) if (hits.some((hit) => !hit.safe)) blockedHits.push(...hits);
121
+ else repairableHits.push(...hits);
122
+ const lines = [];
123
+ if (repairableHits.length > 0) {
124
+ const sample = repairableHits[0];
125
+ lines.push(`- Discord allowlists contain ${repairableHits.length} numeric ${repairableHits.length === 1 ? "entry" : "entries"} (e.g. ${sanitizeForLog(sample.path)}=${sanitizeForLog(String(sample.entry))}).`, `- Discord IDs must be strings; run "${params.doctorFixCommand}" to convert numeric IDs to quoted strings.`);
126
+ }
127
+ if (blockedHits.length > 0) {
128
+ const sample = blockedHits[0];
129
+ lines.push(`- Discord allowlists contain ${blockedHits.length} numeric ${blockedHits.length === 1 ? "entry" : "entries"} in lists that cannot be auto-repaired (e.g. ${sanitizeForLog(sample.path)}).`, `- These lists include invalid or precision-losing numeric IDs; manually quote the original values in your config file, then rerun "${params.doctorFixCommand}".`);
130
+ }
131
+ return lines;
132
+ }
133
+ function maybeRepairDiscordNumericIds(cfg, doctorFixCommand) {
134
+ const hits = scanDiscordNumericIdEntries(cfg);
135
+ if (hits.length === 0) return {
136
+ config: cfg,
137
+ changes: []
138
+ };
139
+ const next = structuredClone(cfg);
140
+ const changes = [];
141
+ const repairList = (pathLabel, holder, key) => {
142
+ const raw = holder[key];
143
+ if (!Array.isArray(raw)) return;
144
+ if (raw.some((entry) => typeof entry === "number" && (!Number.isSafeInteger(entry) || entry < 0))) return;
145
+ let converted = 0;
146
+ holder[key] = raw.map((entry) => {
147
+ if (typeof entry === "number") {
148
+ converted += 1;
149
+ return String(entry);
150
+ }
151
+ return entry;
152
+ });
153
+ if (converted > 0) changes.push(`- ${sanitizeForLog(pathLabel)}: converted ${converted} numeric ${converted === 1 ? "ID" : "IDs"} to strings`);
154
+ };
155
+ for (const scope of collectDiscordAccountScopes(next)) for (const ref of collectDiscordIdLists(scope.prefix, scope.account)) repairList(ref.pathLabel, ref.holder, ref.key);
156
+ if (changes.length === 0) return {
157
+ config: cfg,
158
+ changes: [],
159
+ warnings: collectDiscordNumericIdWarnings({
160
+ hits,
161
+ doctorFixCommand
162
+ })
163
+ };
164
+ return {
165
+ config: next,
166
+ changes,
167
+ warnings: collectDiscordNumericIdWarnings({
168
+ hits: hits.filter((hit) => !hit.safe),
169
+ doctorFixCommand
170
+ })
171
+ };
172
+ }
173
+ function collectDiscordMissingEnvTokenWarnings(params) {
174
+ if (resolveDefaultDiscordAccountId(params.cfg) !== "default") return [];
175
+ const account = inspectDiscordAccount({
176
+ cfg: params.cfg,
177
+ accountId: "default",
178
+ envToken: params.env?.DISCORD_BOT_TOKEN ?? ""
179
+ });
180
+ if (!account.enabled || account.tokenStatus !== "missing" || account.tokenSource !== "none") return [];
181
+ return ["- channels.discord: default account has no available bot token, and DISCORD_BOT_TOKEN is absent in this doctor environment. After migration, verify DISCORD_BOT_TOKEN is present in the state-dir .env or configure channels.discord.token / channels.discord.accounts.default.token as a SecretRef."];
182
+ }
183
+ function collectDiscordMutableAllowlistWarnings(cfg) {
184
+ const hits = [];
185
+ const addHits = (pathLabel, list) => {
186
+ if (!Array.isArray(list)) return;
187
+ for (const entry of list) {
188
+ const text = normalizeOptionalString(String(entry)) ?? "";
189
+ if (!text || text === "*" || !isDiscordMutableAllowEntry(text)) continue;
190
+ hits.push({
191
+ path: pathLabel,
192
+ entry: text
193
+ });
194
+ }
195
+ };
196
+ for (const scope of collectProviderDangerousNameMatchingScopes(cfg, "discord")) {
197
+ if (scope.dangerousNameMatchingEnabled) continue;
198
+ addHits(`${scope.prefix}.allowFrom`, scope.account.allowFrom);
199
+ const dm = asObjectRecord$1(scope.account.dm);
200
+ if (dm) addHits(`${scope.prefix}.dm.allowFrom`, dm.allowFrom);
201
+ const guilds = asObjectRecord$1(scope.account.guilds);
202
+ if (!guilds) continue;
203
+ for (const [guildId, guildRaw] of Object.entries(guilds)) {
204
+ const guild = asObjectRecord$1(guildRaw);
205
+ if (!guild) continue;
206
+ addHits(`${scope.prefix}.guilds.${guildId}.users`, guild.users);
207
+ const channels = asObjectRecord$1(guild.channels);
208
+ if (!channels) continue;
209
+ for (const [channelId, channelRaw] of Object.entries(channels)) {
210
+ const channel = asObjectRecord$1(channelRaw);
211
+ if (channel) addHits(`${scope.prefix}.guilds.${guildId}.channels.${channelId}.users`, channel.users);
212
+ }
213
+ }
214
+ }
215
+ if (hits.length === 0) return [];
216
+ const exampleLines = hits.slice(0, 8).map((hit) => `- ${sanitizeForLog(hit.path)}: ${sanitizeForLog(hit.entry)}`);
217
+ const remaining = hits.length > 8 ? `- +${hits.length - 8} more mutable allowlist entries.` : null;
218
+ return [
219
+ `- Found ${hits.length} mutable allowlist ${hits.length === 1 ? "entry" : "entries"} across discord while name matching is disabled by default.`,
220
+ ...exampleLines,
221
+ ...remaining ? [remaining] : [],
222
+ `- Option A (break-glass): enable channels.discord.dangerousNameMatching=true for the affected scope.`,
223
+ `- Option B (recommended): resolve names to stable Discord IDs and rewrite the allowlist entries.`
224
+ ];
225
+ }
226
+ const discordDoctor = {
227
+ dmAllowFromMode: "topOnly",
228
+ groupModel: "route",
229
+ groupAllowFromFallbackToAllowFrom: false,
230
+ warnOnEmptyGroupSenderAllowlist: false,
231
+ legacyConfigRules: DISCORD_LEGACY_CONFIG_RULES,
232
+ normalizeCompatibilityConfig,
233
+ collectPreviewWarnings: ({ cfg, doctorFixCommand, env }) => [...collectDiscordMissingEnvTokenWarnings({
234
+ cfg,
235
+ env
236
+ }), ...collectDiscordNumericIdWarnings({
237
+ hits: scanDiscordNumericIdEntries(cfg),
238
+ doctorFixCommand
239
+ })],
240
+ collectMutableAllowlistWarnings: ({ cfg }) => collectDiscordMutableAllowlistWarnings(cfg),
241
+ repairConfig: ({ cfg, doctorFixCommand }) => maybeRepairDiscordNumericIds(cfg, doctorFixCommand)
242
+ };
243
+ //#endregion
244
+ export { collectDiscordMissingEnvTokenWarnings, collectDiscordNumericIdWarnings, discordDoctor, maybeRepairDiscordNumericIds, scanDiscordNumericIdEntries };