@poolzin/pool-bot 2026.2.0 → 2026.2.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 (230) hide show
  1. package/dist/agents/bash-tools.exec.js +76 -25
  2. package/dist/agents/cli-runner/helpers.js +9 -11
  3. package/dist/agents/identity.js +47 -7
  4. package/dist/agents/memory-search.js +25 -8
  5. package/dist/agents/model-selection.js +21 -0
  6. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  7. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  8. package/dist/agents/pi-embedded-helpers.js +1 -1
  9. package/dist/agents/pi-embedded-runner/compact.js +1 -0
  10. package/dist/agents/pi-embedded-runner/model.js +61 -2
  11. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  12. package/dist/agents/pi-embedded-runner/run.js +199 -46
  13. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  14. package/dist/agents/pi-embedded-subscribe.js +118 -29
  15. package/dist/agents/pi-tools.js +10 -5
  16. package/dist/agents/poolbot-tools.js +15 -10
  17. package/dist/agents/sandbox-paths.js +31 -0
  18. package/dist/agents/session-tool-result-guard.js +94 -15
  19. package/dist/agents/shell-utils.js +51 -0
  20. package/dist/agents/skills/bundled-context.js +23 -0
  21. package/dist/agents/skills/bundled-dir.js +41 -7
  22. package/dist/agents/skills-install.js +60 -23
  23. package/dist/agents/subagent-announce.js +79 -34
  24. package/dist/agents/tool-policy.conformance.js +14 -0
  25. package/dist/agents/tool-policy.js +24 -0
  26. package/dist/agents/tools/cron-tool.js +166 -19
  27. package/dist/agents/tools/discord-actions-presence.js +78 -0
  28. package/dist/agents/tools/message-tool.js +56 -2
  29. package/dist/agents/tools/sessions-history-tool.js +69 -1
  30. package/dist/agents/tools/web-search.js +211 -42
  31. package/dist/agents/usage.js +23 -1
  32. package/dist/agents/workspace-run.js +67 -0
  33. package/dist/agents/workspace-templates.js +44 -0
  34. package/dist/auto-reply/command-auth.js +121 -6
  35. package/dist/auto-reply/envelope.js +50 -72
  36. package/dist/auto-reply/reply/commands-compact.js +1 -0
  37. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  38. package/dist/auto-reply/reply/commands-context.js +1 -0
  39. package/dist/auto-reply/reply/commands-models.js +107 -60
  40. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  41. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  42. package/dist/auto-reply/reply/inbound-context.js +5 -1
  43. package/dist/auto-reply/reply/model-selection.js +3 -3
  44. package/dist/auto-reply/thinking.js +88 -43
  45. package/dist/browser/bridge-server.js +13 -0
  46. package/dist/browser/cdp.helpers.js +38 -24
  47. package/dist/browser/client-fetch.js +50 -7
  48. package/dist/browser/config.js +1 -10
  49. package/dist/browser/extension-relay.js +101 -40
  50. package/dist/browser/pw-ai.js +1 -1
  51. package/dist/browser/pw-session.js +143 -8
  52. package/dist/browser/pw-tools-core.interactions.js +125 -27
  53. package/dist/browser/pw-tools-core.responses.js +1 -1
  54. package/dist/browser/pw-tools-core.state.js +1 -1
  55. package/dist/browser/routes/agent.act.js +86 -41
  56. package/dist/browser/routes/dispatcher.js +4 -4
  57. package/dist/browser/screenshot.js +1 -1
  58. package/dist/browser/server.js +13 -0
  59. package/dist/build-info.json +3 -3
  60. package/dist/channels/reply-prefix.js +8 -1
  61. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  62. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  63. package/dist/cli/cron-cli/shared.js +56 -41
  64. package/dist/cli/dns-cli.js +26 -14
  65. package/dist/cli/gateway-cli/register.js +37 -19
  66. package/dist/cli/memory-cli.js +5 -5
  67. package/dist/cli/parse-bytes.js +37 -0
  68. package/dist/cli/update-cli.js +173 -52
  69. package/dist/commands/agent.js +1 -0
  70. package/dist/commands/doctor-config-flow.js +61 -5
  71. package/dist/commands/doctor-state-migrations.js +1 -1
  72. package/dist/commands/health.js +1 -1
  73. package/dist/commands/model-allowlist.js +29 -0
  74. package/dist/commands/model-picker.js +2 -1
  75. package/dist/commands/models/list.status-command.js +43 -23
  76. package/dist/commands/models/shared.js +15 -0
  77. package/dist/commands/onboard-custom.js +384 -0
  78. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  79. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  80. package/dist/commands/onboard-skills.js +63 -38
  81. package/dist/commands/openai-model-default.js +41 -0
  82. package/dist/config/defaults.js +3 -2
  83. package/dist/config/paths.js +136 -35
  84. package/dist/config/plugin-auto-enable.js +21 -5
  85. package/dist/config/redact-snapshot.js +153 -0
  86. package/dist/config/schema.field-metadata.js +590 -0
  87. package/dist/config/schema.js +2 -2
  88. package/dist/config/sessions/store.js +291 -23
  89. package/dist/config/zod-schema.agent-defaults.js +3 -0
  90. package/dist/config/zod-schema.agent-runtime.js +13 -2
  91. package/dist/config/zod-schema.providers-core.js +142 -0
  92. package/dist/config/zod-schema.session.js +3 -0
  93. package/dist/cron/delivery.js +57 -0
  94. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  95. package/dist/cron/isolated-agent/helpers.js +22 -5
  96. package/dist/cron/isolated-agent/run.js +171 -63
  97. package/dist/cron/isolated-agent/session.js +2 -0
  98. package/dist/cron/normalize.js +356 -28
  99. package/dist/cron/parse.js +10 -5
  100. package/dist/cron/run-log.js +35 -10
  101. package/dist/cron/schedule.js +41 -6
  102. package/dist/cron/service/jobs.js +208 -35
  103. package/dist/cron/service/ops.js +72 -16
  104. package/dist/cron/service/state.js +2 -0
  105. package/dist/cron/service/store.js +386 -14
  106. package/dist/cron/service/timer.js +390 -147
  107. package/dist/cron/session-reaper.js +86 -0
  108. package/dist/cron/store.js +23 -8
  109. package/dist/cron/validate-timestamp.js +43 -0
  110. package/dist/discord/monitor/agent-components.js +438 -0
  111. package/dist/discord/monitor/allow-list.js +28 -5
  112. package/dist/discord/monitor/gateway-registry.js +29 -0
  113. package/dist/discord/monitor/native-command.js +44 -23
  114. package/dist/discord/monitor/sender-identity.js +45 -0
  115. package/dist/discord/pluralkit.js +27 -0
  116. package/dist/discord/send.outbound.js +92 -5
  117. package/dist/discord/send.shared.js +60 -23
  118. package/dist/discord/targets.js +84 -1
  119. package/dist/entry.js +15 -9
  120. package/dist/extensionAPI.js +8 -0
  121. package/dist/gateway/control-ui.js +8 -1
  122. package/dist/gateway/hooks-mapping.js +3 -0
  123. package/dist/gateway/hooks.js +65 -0
  124. package/dist/gateway/net.js +96 -31
  125. package/dist/gateway/node-command-policy.js +50 -15
  126. package/dist/gateway/origin-check.js +56 -0
  127. package/dist/gateway/protocol/client-info.js +9 -0
  128. package/dist/gateway/protocol/index.js +9 -2
  129. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  130. package/dist/gateway/protocol/schema/cron.js +22 -10
  131. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  132. package/dist/gateway/protocol/schema/sessions.js +12 -0
  133. package/dist/gateway/server/hooks.js +1 -1
  134. package/dist/gateway/server-broadcast.js +26 -9
  135. package/dist/gateway/server-chat.js +112 -23
  136. package/dist/gateway/server-discovery-runtime.js +10 -2
  137. package/dist/gateway/server-http.js +109 -11
  138. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  139. package/dist/gateway/server-methods/agents.js +321 -2
  140. package/dist/gateway/server-methods/usage.js +559 -16
  141. package/dist/gateway/server-runtime-state.js +22 -8
  142. package/dist/gateway/server-startup-memory.js +16 -0
  143. package/dist/gateway/server.impl.js +5 -1
  144. package/dist/gateway/session-utils.fs.js +23 -25
  145. package/dist/gateway/session-utils.js +20 -10
  146. package/dist/gateway/sessions-patch.js +7 -22
  147. package/dist/gateway/test-helpers.server.js +35 -2
  148. package/dist/imessage/constants.js +2 -0
  149. package/dist/imessage/monitor/deliver.js +4 -1
  150. package/dist/imessage/monitor/monitor-provider.js +51 -1
  151. package/dist/infra/bonjour-discovery.js +131 -70
  152. package/dist/infra/control-ui-assets.js +134 -12
  153. package/dist/infra/errors.js +12 -0
  154. package/dist/infra/exec-approvals.js +266 -57
  155. package/dist/infra/format-time/format-datetime.js +79 -0
  156. package/dist/infra/format-time/format-duration.js +81 -0
  157. package/dist/infra/format-time/format-relative.js +80 -0
  158. package/dist/infra/heartbeat-runner.js +140 -49
  159. package/dist/infra/home-dir.js +54 -0
  160. package/dist/infra/net/fetch-guard.js +122 -0
  161. package/dist/infra/net/ssrf.js +65 -29
  162. package/dist/infra/outbound/abort.js +14 -0
  163. package/dist/infra/outbound/message-action-runner.js +77 -13
  164. package/dist/infra/outbound/outbound-session.js +143 -37
  165. package/dist/infra/poolbot-root.js +43 -1
  166. package/dist/infra/session-cost-usage.js +631 -41
  167. package/dist/infra/state-migrations.js +317 -47
  168. package/dist/infra/update-global.js +35 -0
  169. package/dist/infra/update-runner.js +149 -43
  170. package/dist/infra/warning-filter.js +65 -0
  171. package/dist/infra/widearea-dns.js +30 -9
  172. package/dist/logging/redact-identifier.js +12 -0
  173. package/dist/media/fetch.js +81 -58
  174. package/dist/media-understanding/apply.js +403 -3
  175. package/dist/media-understanding/attachments.js +38 -27
  176. package/dist/media-understanding/defaults.js +16 -0
  177. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  178. package/dist/media-understanding/providers/google/audio.js +24 -17
  179. package/dist/media-understanding/providers/google/video.js +24 -17
  180. package/dist/media-understanding/providers/image.js +2 -2
  181. package/dist/media-understanding/providers/index.js +4 -1
  182. package/dist/media-understanding/providers/openai/audio.js +22 -14
  183. package/dist/media-understanding/providers/shared.js +16 -11
  184. package/dist/media-understanding/providers/zai/index.js +6 -0
  185. package/dist/media-understanding/runner.js +158 -90
  186. package/dist/memory/batch-voyage.js +277 -0
  187. package/dist/memory/embeddings-voyage.js +75 -0
  188. package/dist/memory/embeddings.js +28 -16
  189. package/dist/memory/internal.js +101 -18
  190. package/dist/memory/manager.js +154 -48
  191. package/dist/memory/search-manager.js +173 -0
  192. package/dist/memory/session-files.js +9 -3
  193. package/dist/node-host/runner.js +34 -24
  194. package/dist/node-host/with-timeout.js +27 -0
  195. package/dist/plugins/commands.js +5 -1
  196. package/dist/plugins/config-state.js +86 -7
  197. package/dist/plugins/source-display.js +51 -0
  198. package/dist/process/exec.js +20 -2
  199. package/dist/routing/resolve-route.js +12 -0
  200. package/dist/routing/session-key.js +15 -0
  201. package/dist/runtime.js +2 -0
  202. package/dist/security/audit-extra.async.js +601 -0
  203. package/dist/security/audit-extra.js +2 -830
  204. package/dist/security/audit-extra.sync.js +505 -0
  205. package/dist/security/channel-metadata.js +34 -0
  206. package/dist/security/external-content.js +88 -6
  207. package/dist/security/skill-scanner.js +330 -0
  208. package/dist/sessions/session-key-utils.js +7 -0
  209. package/dist/signal/monitor/event-handler.js +80 -1
  210. package/dist/slack/monitor/media.js +85 -15
  211. package/dist/tailscale/detect.js +1 -2
  212. package/dist/telegram/bot/helpers.js +109 -28
  213. package/dist/telegram/bot-handlers.js +144 -3
  214. package/dist/telegram/bot-message-context.js +37 -10
  215. package/dist/telegram/bot-message-dispatch.js +48 -15
  216. package/dist/telegram/bot-native-commands.js +86 -29
  217. package/dist/telegram/bot.js +30 -29
  218. package/dist/telegram/model-buttons.js +163 -0
  219. package/dist/telegram/monitor.js +110 -85
  220. package/dist/telegram/send.js +129 -47
  221. package/dist/terminal/restore.js +45 -0
  222. package/dist/test-helpers/state-dir-env.js +16 -0
  223. package/dist/tts/tts.js +12 -6
  224. package/dist/tui/tui-session-actions.js +166 -54
  225. package/dist/utils/fetch-timeout.js +20 -0
  226. package/dist/utils/normalize-secret-input.js +19 -0
  227. package/dist/utils/transcript-tools.js +58 -0
  228. package/dist/utils.js +45 -14
  229. package/dist/version.js +42 -5
  230. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { Button, ChannelType, Command, Row, } from "@buape/carbon";
2
2
  import { ApplicationCommandOptionType, ButtonStyle } from "discord-api-types/v10";
3
- import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
3
+ import { resolveHumanDelayConfig } from "../../agents/identity.js";
4
4
  import { resolveChunkMode, resolveTextChunkLimit } from "../../auto-reply/chunk.js";
5
5
  import { buildCommandTextFromArgs, findCommandByNativeName, listChatCommands, parseCommandArgs, resolveCommandArgChoices, resolveCommandArgMenu, serializeCommandArgs, } from "../../auto-reply/commands-registry.js";
6
6
  import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
@@ -11,8 +11,10 @@ import { resolveAgentRoute } from "../../routing/resolve-route.js";
11
11
  import { loadWebMedia } from "../../web/media.js";
12
12
  import { chunkDiscordTextWithMode } from "../chunk.js";
13
13
  import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
14
- import { allowListMatches, isDiscordGroupAllowedByPolicy, normalizeDiscordAllowList, normalizeDiscordSlug, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, resolveDiscordUserAllowed, } from "./allow-list.js";
15
- import { formatDiscordUserTag } from "./format.js";
14
+ import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
15
+ import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
16
+ import { allowListMatches, isDiscordGroupAllowedByPolicy, normalizeDiscordAllowList, normalizeDiscordSlug, resolveDiscordChannelConfigWithFallback, resolveDiscordGuildEntry, resolveDiscordOwnerAllowFrom, resolveDiscordUserAllowed, } from "./allow-list.js";
17
+ import { resolveDiscordSenderIdentity } from "./sender-identity.js";
16
18
  import { resolveDiscordChannelInfo } from "./message-utils.js";
17
19
  import { resolveDiscordThreadParentInfo } from "./threading.js";
18
20
  function buildDiscordCommandOptions(params) {
@@ -357,6 +359,7 @@ async function dispatchDiscordCommandInteraction(params) {
357
359
  const user = interaction.user;
358
360
  if (!user)
359
361
  return;
362
+ const sender = resolveDiscordSenderIdentity({ author: user, pluralkitInfo: null });
360
363
  const channel = interaction.channel;
361
364
  const channelType = channel?.type;
362
365
  const isDirectMessage = channelType === ChannelType.DM;
@@ -370,12 +373,13 @@ async function dispatchDiscordCommandInteraction(params) {
370
373
  const ownerAllowList = normalizeDiscordAllowList(discordConfig?.dm?.allowFrom ?? [], [
371
374
  "discord:",
372
375
  "user:",
376
+ "pk:",
373
377
  ]);
374
378
  const ownerOk = ownerAllowList && user
375
379
  ? allowListMatches(ownerAllowList, {
376
- id: user.id,
377
- name: user.username,
378
- tag: formatDiscordUserTag(user),
380
+ id: sender.id,
381
+ name: sender.name,
382
+ tag: sender.tag,
379
383
  })
380
384
  : false;
381
385
  const guildInfo = resolveDiscordGuildEntry({
@@ -447,12 +451,12 @@ async function dispatchDiscordCommandInteraction(params) {
447
451
  if (dmPolicy !== "open") {
448
452
  const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
449
453
  const effectiveAllowFrom = [...(discordConfig?.dm?.allowFrom ?? []), ...storeAllowFrom];
450
- const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
454
+ const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]);
451
455
  const permitted = allowList
452
456
  ? allowListMatches(allowList, {
453
- id: user.id,
454
- name: user.username,
455
- tag: formatDiscordUserTag(user),
457
+ id: sender.id,
458
+ name: sender.name,
459
+ tag: sender.tag,
456
460
  })
457
461
  : false;
458
462
  if (!permitted) {
@@ -462,8 +466,8 @@ async function dispatchDiscordCommandInteraction(params) {
462
466
  channel: "discord",
463
467
  id: user.id,
464
468
  meta: {
465
- tag: formatDiscordUserTag(user),
466
- name: user.username ?? undefined,
469
+ tag: sender.tag,
470
+ name: sender.name ?? undefined,
467
471
  },
468
472
  });
469
473
  if (created) {
@@ -488,9 +492,9 @@ async function dispatchDiscordCommandInteraction(params) {
488
492
  const userOk = hasUserAllowlist
489
493
  ? resolveDiscordUserAllowed({
490
494
  allowList: channelUsers,
491
- userId: user.id,
492
- userName: user.username,
493
- userTag: formatDiscordUserTag(user),
495
+ userId: sender.id,
496
+ userName: sender.name,
497
+ userTag: sender.tag,
494
498
  })
495
499
  : false;
496
500
  const authorizers = useAccessGroups
@@ -555,10 +559,17 @@ async function dispatchDiscordCommandInteraction(params) {
555
559
  kind: isDirectMessage ? "dm" : isGroupDm ? "group" : "channel",
556
560
  id: isDirectMessage ? user.id : channelId,
557
561
  },
562
+ parentPeer: threadParentId ? { kind: "channel", id: threadParentId } : undefined,
558
563
  });
559
564
  const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
565
+ const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
566
+ channelConfig,
567
+ guildInfo,
568
+ sender: { id: sender.id, name: sender.name, tag: sender.tag },
569
+ });
560
570
  const ctxPayload = finalizeInboundContext({
561
571
  Body: prompt,
572
+ BodyForAgent: prompt,
562
573
  RawBody: prompt,
563
574
  CommandBody: prompt,
564
575
  CommandArgs: commandArgs,
@@ -576,19 +587,25 @@ async function dispatchDiscordCommandInteraction(params) {
576
587
  GroupSubject: isGuild ? interaction.guild?.name : undefined,
577
588
  GroupSystemPrompt: isGuild
578
589
  ? (() => {
579
- const channelTopic = channel && "topic" in channel ? (channel.topic ?? undefined) : undefined;
580
- const channelDescription = channelTopic?.trim();
581
- const systemPromptParts = [
582
- channelDescription ? `Channel topic: ${channelDescription}` : null,
583
- channelConfig?.systemPrompt?.trim() || null,
584
- ].filter((entry) => Boolean(entry));
590
+ const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter((entry) => Boolean(entry));
585
591
  return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
586
592
  })()
587
593
  : undefined,
594
+ UntrustedContext: isGuild
595
+ ? (() => {
596
+ const channelTopic = channel && "topic" in channel ? (channel.topic ?? undefined) : undefined;
597
+ const untrustedChannelMetadata = buildUntrustedChannelMetadata({
598
+ source: "discord",
599
+ label: "Discord channel topic",
600
+ entries: [channelTopic],
601
+ });
602
+ return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
603
+ })()
604
+ : undefined,
588
605
  SenderName: user.globalName ?? user.username,
589
606
  SenderId: user.id,
590
607
  SenderUsername: user.username,
591
- SenderTag: formatDiscordUserTag(user),
608
+ SenderTag: sender.tag,
592
609
  Provider: "discord",
593
610
  Surface: "discord",
594
611
  WasMentioned: true,
@@ -596,13 +613,16 @@ async function dispatchDiscordCommandInteraction(params) {
596
613
  Timestamp: Date.now(),
597
614
  CommandAuthorized: commandAuthorized,
598
615
  CommandSource: "native",
616
+ OwnerAllowFrom: ownerAllowFrom,
599
617
  });
618
+ const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
600
619
  let didReply = false;
601
620
  await dispatchReplyWithDispatcher({
602
621
  ctx: ctxPayload,
603
622
  cfg,
604
623
  dispatcherOptions: {
605
- responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
624
+ responsePrefix: prefixContext.responsePrefix,
625
+ responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
606
626
  humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
607
627
  deliver: async (payload) => {
608
628
  try {
@@ -635,6 +655,7 @@ async function dispatchDiscordCommandInteraction(params) {
635
655
  disableBlockStreaming: typeof discordConfig?.blockStreaming === "boolean"
636
656
  ? !discordConfig.blockStreaming
637
657
  : undefined,
658
+ onModelSelected: prefixContext.onModelSelected,
638
659
  },
639
660
  });
640
661
  }
@@ -0,0 +1,45 @@
1
+ import { formatDiscordUserTag } from "./format.js";
2
+ export function resolveDiscordWebhookId(message) {
3
+ const candidate = message.webhookId ?? message.webhook_id;
4
+ return typeof candidate === "string" && candidate.trim() ? candidate.trim() : null;
5
+ }
6
+ export function resolveDiscordSenderIdentity(params) {
7
+ const pkInfo = params.pluralkitInfo ?? null;
8
+ const pkMember = pkInfo?.member ?? undefined;
9
+ const pkSystem = pkInfo?.system ?? undefined;
10
+ const memberId = pkMember?.id?.trim();
11
+ const memberNameRaw = pkMember?.display_name ?? pkMember?.name ?? "";
12
+ const memberName = memberNameRaw?.trim();
13
+ if (memberId && memberName) {
14
+ const systemName = pkSystem?.name?.trim();
15
+ const label = systemName ? `${memberName} (PK:${systemName})` : `${memberName} (PK)`;
16
+ return {
17
+ id: memberId,
18
+ name: memberName,
19
+ tag: pkMember?.name?.trim() || undefined,
20
+ label,
21
+ isPluralKit: true,
22
+ pluralkit: {
23
+ memberId,
24
+ memberName,
25
+ systemId: pkSystem?.id?.trim() || undefined,
26
+ systemName,
27
+ },
28
+ };
29
+ }
30
+ const senderTag = formatDiscordUserTag(params.author);
31
+ const senderDisplay = params.member?.nickname ?? params.author.globalName ?? params.author.username;
32
+ const senderLabel = senderDisplay && senderTag && senderDisplay !== senderTag
33
+ ? `${senderDisplay} (${senderTag})`
34
+ : (senderDisplay ?? senderTag ?? params.author.id);
35
+ return {
36
+ id: params.author.id,
37
+ name: params.author.username ?? undefined,
38
+ tag: senderTag,
39
+ label: senderLabel,
40
+ isPluralKit: false,
41
+ };
42
+ }
43
+ export function resolveDiscordSenderLabel(params) {
44
+ return resolveDiscordSenderIdentity(params).label;
45
+ }
@@ -0,0 +1,27 @@
1
+ import { resolveFetch } from "../infra/fetch.js";
2
+ const PLURALKIT_API_BASE = "https://api.pluralkit.me/v2";
3
+ export async function fetchPluralKitMessageInfo(params) {
4
+ if (!params.config?.enabled) {
5
+ return null;
6
+ }
7
+ const fetchImpl = resolveFetch(params.fetcher);
8
+ if (!fetchImpl) {
9
+ return null;
10
+ }
11
+ const headers = {};
12
+ if (params.config.token?.trim()) {
13
+ headers.Authorization = params.config.token.trim();
14
+ }
15
+ const res = await fetchImpl(`${PLURALKIT_API_BASE}/messages/${params.messageId}`, {
16
+ headers,
17
+ });
18
+ if (res.status === 404) {
19
+ return null;
20
+ }
21
+ if (!res.ok) {
22
+ const text = await res.text().catch(() => "");
23
+ const detail = text.trim() ? `: ${text.trim()}` : "";
24
+ throw new Error(`PluralKit API failed (${res.status})${detail}`);
25
+ }
26
+ return (await res.json());
27
+ }
@@ -1,11 +1,25 @@
1
- import { Routes } from "discord-api-types/v10";
1
+ import { ChannelType, Routes } from "discord-api-types/v10";
2
2
  import { resolveChunkMode } from "../auto-reply/chunk.js";
3
3
  import { loadConfig } from "../config/config.js";
4
4
  import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
5
5
  import { recordChannelActivity } from "../infra/channel-activity.js";
6
6
  import { convertMarkdownTables } from "../markdown/tables.js";
7
7
  import { resolveDiscordAccount } from "./accounts.js";
8
- import { buildDiscordSendError, createDiscordClient, normalizeDiscordPollInput, normalizeStickerIds, parseRecipient, resolveChannelId, sendDiscordMedia, sendDiscordText, } from "./send.shared.js";
8
+ import { buildDiscordSendError, buildDiscordTextChunks, createDiscordClient, normalizeDiscordPollInput, normalizeStickerIds, parseAndResolveRecipient, resolveChannelId, sendDiscordMedia, sendDiscordText, } from "./send.shared.js";
9
+ /** Discord thread names are capped at 100 characters. */
10
+ const DISCORD_THREAD_NAME_LIMIT = 100;
11
+ /** Derive a thread title from the first non-empty line of the message text. */
12
+ function deriveForumThreadName(text) {
13
+ const firstLine = text
14
+ .split("\n")
15
+ .find((l) => l.trim())
16
+ ?.trim() ?? "";
17
+ return firstLine.slice(0, DISCORD_THREAD_NAME_LIMIT) || new Date().toISOString().slice(0, 16);
18
+ }
19
+ /** Forum/Media channels cannot receive regular messages; detect them here. */
20
+ function isForumLikeType(channelType) {
21
+ return channelType === ChannelType.GuildForum || channelType === ChannelType.GuildMedia;
22
+ }
9
23
  export async function sendMessageDiscord(to, text, opts = {}) {
10
24
  const cfg = loadConfig();
11
25
  const accountInfo = resolveDiscordAccount({
@@ -20,8 +34,81 @@ export async function sendMessageDiscord(to, text, opts = {}) {
20
34
  const chunkMode = resolveChunkMode(cfg, "discord", accountInfo.accountId);
21
35
  const textWithTables = convertMarkdownTables(text ?? "", tableMode);
22
36
  const { token, rest, request } = createDiscordClient(opts, cfg);
23
- const recipient = parseRecipient(to);
37
+ const recipient = await parseAndResolveRecipient(to, opts.accountId);
24
38
  const { channelId } = await resolveChannelId(rest, recipient, request);
39
+ // Forum/Media channels reject POST /messages; auto-create a thread post instead.
40
+ let channelType;
41
+ try {
42
+ const channel = (await rest.get(Routes.channel(channelId)));
43
+ channelType = channel?.type;
44
+ }
45
+ catch {
46
+ // If we can't fetch the channel, fall through to the normal send path.
47
+ }
48
+ if (isForumLikeType(channelType)) {
49
+ const threadName = deriveForumThreadName(textWithTables);
50
+ const chunks = buildDiscordTextChunks(textWithTables, {
51
+ maxLinesPerMessage: accountInfo.config.maxLinesPerMessage,
52
+ chunkMode,
53
+ });
54
+ const starterContent = chunks[0]?.trim() ? chunks[0] : threadName;
55
+ const starterEmbeds = opts.embeds?.length ? opts.embeds : undefined;
56
+ let threadRes;
57
+ try {
58
+ threadRes = (await request(() => rest.post(Routes.threads(channelId), {
59
+ body: {
60
+ name: threadName,
61
+ message: {
62
+ content: starterContent,
63
+ ...(starterEmbeds ? { embeds: starterEmbeds } : {}),
64
+ },
65
+ },
66
+ }), "forum-thread"));
67
+ }
68
+ catch (err) {
69
+ throw await buildDiscordSendError(err, {
70
+ channelId,
71
+ rest,
72
+ token,
73
+ hasMedia: Boolean(opts.mediaUrl),
74
+ });
75
+ }
76
+ const threadId = threadRes.id;
77
+ const messageId = threadRes.message?.id ?? threadId;
78
+ const resultChannelId = threadRes.message?.channel_id ?? threadId;
79
+ const remainingChunks = chunks.slice(1);
80
+ try {
81
+ if (opts.mediaUrl) {
82
+ const [mediaCaption, ...afterMediaChunks] = remainingChunks;
83
+ await sendDiscordMedia(rest, threadId, mediaCaption ?? "", opts.mediaUrl, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, chunkMode);
84
+ for (const chunk of afterMediaChunks) {
85
+ await sendDiscordText(rest, threadId, chunk, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, chunkMode);
86
+ }
87
+ }
88
+ else {
89
+ for (const chunk of remainingChunks) {
90
+ await sendDiscordText(rest, threadId, chunk, undefined, request, accountInfo.config.maxLinesPerMessage, undefined, chunkMode);
91
+ }
92
+ }
93
+ }
94
+ catch (err) {
95
+ throw await buildDiscordSendError(err, {
96
+ channelId: threadId,
97
+ rest,
98
+ token,
99
+ hasMedia: Boolean(opts.mediaUrl),
100
+ });
101
+ }
102
+ recordChannelActivity({
103
+ channel: "discord",
104
+ accountId: accountInfo.accountId,
105
+ direction: "outbound",
106
+ });
107
+ return {
108
+ messageId: messageId ? String(messageId) : "unknown",
109
+ channelId: String(resultChannelId ?? channelId),
110
+ };
111
+ }
25
112
  let result;
26
113
  try {
27
114
  if (opts.mediaUrl) {
@@ -52,7 +139,7 @@ export async function sendMessageDiscord(to, text, opts = {}) {
52
139
  export async function sendStickerDiscord(to, stickerIds, opts = {}) {
53
140
  const cfg = loadConfig();
54
141
  const { rest, request } = createDiscordClient(opts, cfg);
55
- const recipient = parseRecipient(to);
142
+ const recipient = await parseAndResolveRecipient(to, opts.accountId);
56
143
  const { channelId } = await resolveChannelId(rest, recipient, request);
57
144
  const content = opts.content?.trim();
58
145
  const stickers = normalizeStickerIds(stickerIds);
@@ -70,7 +157,7 @@ export async function sendStickerDiscord(to, stickerIds, opts = {}) {
70
157
  export async function sendPollDiscord(to, poll, opts = {}) {
71
158
  const cfg = loadConfig();
72
159
  const { rest, request } = createDiscordClient(opts, cfg);
73
- const recipient = parseRecipient(to);
160
+ const recipient = await parseAndResolveRecipient(to, opts.accountId);
74
161
  const { channelId } = await resolveChannelId(rest, recipient, request);
75
162
  const content = opts.content?.trim();
76
163
  const payload = normalizeDiscordPollInput(poll);
@@ -9,7 +9,7 @@ import { resolveDiscordAccount } from "./accounts.js";
9
9
  import { chunkDiscordTextWithMode } from "./chunk.js";
10
10
  import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
11
11
  import { DiscordSendError } from "./send.types.js";
12
- import { parseDiscordTarget } from "./targets.js";
12
+ import { parseDiscordTarget, resolveDiscordTarget } from "./targets.js";
13
13
  import { normalizeDiscordToken } from "./token.js";
14
14
  const DISCORD_TEXT_LIMIT = 2000;
15
15
  const DISCORD_MAX_STICKERS = 3;
@@ -19,8 +19,9 @@ const DISCORD_MISSING_PERMISSIONS = 50013;
19
19
  const DISCORD_CANNOT_DM = 50007;
20
20
  function resolveToken(params) {
21
21
  const explicit = normalizeDiscordToken(params.explicit);
22
- if (explicit)
22
+ if (explicit) {
23
23
  return explicit;
24
+ }
24
25
  const fallback = normalizeDiscordToken(params.fallbackToken);
25
26
  if (!fallback) {
26
27
  throw new Error(`Discord bot token missing for account "${params.accountId}" (set discord.accounts.${params.accountId}.token or DISCORD_BOT_TOKEN for default).`);
@@ -68,6 +69,37 @@ function parseRecipient(raw) {
68
69
  }
69
70
  return { kind: target.kind, id: target.id };
70
71
  }
72
+ /**
73
+ * Parse and resolve Discord recipient, including username lookup.
74
+ * This enables sending DMs by username (e.g., "john.doe") by querying
75
+ * the Discord directory to resolve usernames to user IDs.
76
+ *
77
+ * @param raw - The recipient string (username, ID, or known format)
78
+ * @param accountId - Discord account ID to use for directory lookup
79
+ * @returns Parsed DiscordRecipient with resolved user ID if applicable
80
+ */
81
+ export async function parseAndResolveRecipient(raw, accountId) {
82
+ const cfg = loadConfig();
83
+ const accountInfo = resolveDiscordAccount({ cfg, accountId });
84
+ // First try to resolve using directory lookup (handles usernames)
85
+ const trimmed = raw.trim();
86
+ const parseOptions = {
87
+ ambiguousMessage: `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
88
+ };
89
+ const resolved = await resolveDiscordTarget(raw, {
90
+ cfg,
91
+ accountId: accountInfo.accountId,
92
+ }, parseOptions);
93
+ if (resolved) {
94
+ return { kind: resolved.kind, id: resolved.id };
95
+ }
96
+ // Fallback to standard parsing (for channels, etc.)
97
+ const parsed = parseDiscordTarget(raw, parseOptions);
98
+ if (!parsed) {
99
+ throw new Error("Recipient is required for Discord sends");
100
+ }
101
+ return { kind: parsed.kind, id: parsed.id };
102
+ }
71
103
  function normalizeStickerIds(raw) {
72
104
  const ids = raw.map((entry) => entry.trim()).filter(Boolean);
73
105
  if (ids.length === 0) {
@@ -102,29 +134,33 @@ function normalizeDiscordPollInput(input) {
102
134
  };
103
135
  }
104
136
  function getDiscordErrorCode(err) {
105
- if (!err || typeof err !== "object")
137
+ if (!err || typeof err !== "object") {
106
138
  return undefined;
139
+ }
107
140
  const candidate = "code" in err && err.code !== undefined
108
141
  ? err.code
109
142
  : "rawError" in err && err.rawError && typeof err.rawError === "object"
110
143
  ? err.rawError.code
111
144
  : undefined;
112
- if (typeof candidate === "number")
145
+ if (typeof candidate === "number") {
113
146
  return candidate;
147
+ }
114
148
  if (typeof candidate === "string" && /^\d+$/.test(candidate)) {
115
149
  return Number(candidate);
116
150
  }
117
151
  return undefined;
118
152
  }
119
153
  async function buildDiscordSendError(err, ctx) {
120
- if (err instanceof DiscordSendError)
154
+ if (err instanceof DiscordSendError) {
121
155
  return err;
156
+ }
122
157
  const code = getDiscordErrorCode(err);
123
158
  if (code === DISCORD_CANNOT_DM) {
124
159
  return new DiscordSendError("discord dm failed: user blocks dms or privacy settings disallow it", { kind: "dm-blocked" });
125
160
  }
126
- if (code !== DISCORD_MISSING_PERMISSIONS)
161
+ if (code !== DISCORD_MISSING_PERMISSIONS) {
127
162
  return err;
163
+ }
128
164
  let missing = [];
129
165
  try {
130
166
  const permissions = await fetchChannelPermissionsDiscord(ctx.channelId, {
@@ -165,18 +201,26 @@ async function resolveChannelId(rest, recipient, request) {
165
201
  }
166
202
  return { channelId: dmChannel.id, dm: true };
167
203
  }
204
+ export function buildDiscordTextChunks(text, opts = {}) {
205
+ if (!text) {
206
+ return [];
207
+ }
208
+ const chunks = chunkDiscordTextWithMode(text, {
209
+ maxChars: opts.maxChars ?? DISCORD_TEXT_LIMIT,
210
+ maxLines: opts.maxLinesPerMessage,
211
+ chunkMode: opts.chunkMode,
212
+ });
213
+ if (!chunks.length && text) {
214
+ chunks.push(text);
215
+ }
216
+ return chunks;
217
+ }
168
218
  async function sendDiscordText(rest, channelId, text, replyTo, request, maxLinesPerMessage, embeds, chunkMode) {
169
219
  if (!text.trim()) {
170
220
  throw new Error("Message must be non-empty for Discord sends");
171
221
  }
172
222
  const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
173
- const chunks = chunkDiscordTextWithMode(text, {
174
- maxChars: DISCORD_TEXT_LIMIT,
175
- maxLines: maxLinesPerMessage,
176
- chunkMode,
177
- });
178
- if (!chunks.length && text)
179
- chunks.push(text);
223
+ const chunks = buildDiscordTextChunks(text, { maxLinesPerMessage, chunkMode });
180
224
  if (chunks.length === 1) {
181
225
  const res = (await request(() => rest.post(Routes.channelMessages(channelId), {
182
226
  body: {
@@ -206,15 +250,7 @@ async function sendDiscordText(rest, channelId, text, replyTo, request, maxLines
206
250
  }
207
251
  async function sendDiscordMedia(rest, channelId, text, mediaUrl, replyTo, request, maxLinesPerMessage, embeds, chunkMode) {
208
252
  const media = await loadWebMedia(mediaUrl);
209
- const chunks = text
210
- ? chunkDiscordTextWithMode(text, {
211
- maxChars: DISCORD_TEXT_LIMIT,
212
- maxLines: maxLinesPerMessage,
213
- chunkMode,
214
- })
215
- : [];
216
- if (!chunks.length && text)
217
- chunks.push(text);
253
+ const chunks = text ? buildDiscordTextChunks(text, { maxLinesPerMessage, chunkMode }) : [];
218
254
  const caption = chunks[0] ?? "";
219
255
  const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
220
256
  const res = (await request(() => rest.post(Routes.channelMessages(channelId), {
@@ -231,8 +267,9 @@ async function sendDiscordMedia(rest, channelId, text, mediaUrl, replyTo, reques
231
267
  },
232
268
  }), "media"));
233
269
  for (const chunk of chunks.slice(1)) {
234
- if (!chunk.trim())
270
+ if (!chunk.trim()) {
235
271
  continue;
272
+ }
236
273
  await sendDiscordText(rest, channelId, chunk, undefined, request, maxLinesPerMessage, undefined, chunkMode);
237
274
  }
238
275
  return res;
@@ -1,8 +1,10 @@
1
1
  import { buildMessagingTarget, ensureTargetId, requireTargetKind, } from "../channels/targets.js";
2
+ import { listDiscordDirectoryPeersLive } from "./directory-live.js";
2
3
  export function parseDiscordTarget(raw, options = {}) {
3
4
  const trimmed = raw.trim();
4
- if (!trimmed)
5
+ if (!trimmed) {
5
6
  return undefined;
7
+ }
6
8
  const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
7
9
  if (mentionMatch) {
8
10
  return buildMessagingTarget("user", mentionMatch[1], trimmed);
@@ -41,3 +43,84 @@ export function resolveDiscordChannelId(raw) {
41
43
  const target = parseDiscordTarget(raw, { defaultKind: "channel" });
42
44
  return requireTargetKind({ platform: "Discord", target, kind: "channel" });
43
45
  }
46
+ /**
47
+ * Resolve a Discord username to user ID using the directory lookup.
48
+ * This enables sending DMs by username instead of requiring explicit user IDs.
49
+ *
50
+ * @param raw - The username or raw target string (e.g., "john.doe")
51
+ * @param options - Directory configuration params (cfg, accountId, limit)
52
+ * @param parseOptions - Messaging target parsing options (defaults, ambiguity message)
53
+ * @returns Parsed MessagingTarget with user ID, or undefined if not found
54
+ */
55
+ export async function resolveDiscordTarget(raw, options, parseOptions = {}) {
56
+ const trimmed = raw.trim();
57
+ if (!trimmed) {
58
+ return undefined;
59
+ }
60
+ const likelyUsername = isLikelyUsername(trimmed);
61
+ const shouldLookup = isExplicitUserLookup(trimmed, parseOptions) || likelyUsername;
62
+ // Parse directly if it's already a known format. Use a safe parse so ambiguous
63
+ // numeric targets don't throw when we still want to attempt username lookup.
64
+ const directParse = safeParseDiscordTarget(trimmed, parseOptions);
65
+ if (directParse && directParse.kind !== "channel" && !likelyUsername) {
66
+ return directParse;
67
+ }
68
+ if (!shouldLookup) {
69
+ return directParse ?? parseDiscordTarget(trimmed, parseOptions);
70
+ }
71
+ // Try to resolve as a username via directory lookup
72
+ try {
73
+ const directoryEntries = await listDiscordDirectoryPeersLive({
74
+ ...options,
75
+ query: trimmed,
76
+ limit: 1,
77
+ });
78
+ const match = directoryEntries[0];
79
+ if (match && match.kind === "user") {
80
+ // Extract user ID from the directory entry (format: "user:<id>")
81
+ const userId = match.id.replace(/^user:/, "");
82
+ return buildMessagingTarget("user", userId, trimmed);
83
+ }
84
+ }
85
+ catch {
86
+ // Directory lookup failed - fall through to parse as-is
87
+ // This preserves existing behavior for channel names
88
+ }
89
+ // Fallback to original parsing (for channels, etc.)
90
+ return parseDiscordTarget(trimmed, parseOptions);
91
+ }
92
+ function safeParseDiscordTarget(input, options) {
93
+ try {
94
+ return parseDiscordTarget(input, options);
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ function isExplicitUserLookup(input, options) {
101
+ if (/^<@!?(\d+)>$/.test(input)) {
102
+ return true;
103
+ }
104
+ if (/^(user:|discord:)/.test(input)) {
105
+ return true;
106
+ }
107
+ if (input.startsWith("@")) {
108
+ return true;
109
+ }
110
+ if (/^\d+$/.test(input)) {
111
+ return options.defaultKind === "user";
112
+ }
113
+ return false;
114
+ }
115
+ /**
116
+ * Check if a string looks like a Discord username (not a mention, prefix, or ID).
117
+ * Usernames typically don't start with special characters except underscore.
118
+ */
119
+ function isLikelyUsername(input) {
120
+ // Skip if it's already a known format
121
+ if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) {
122
+ return false;
123
+ }
124
+ // Likely a username if it doesn't match known patterns
125
+ return true;
126
+ }
package/dist/entry.js CHANGED
@@ -3,32 +3,38 @@ import { spawn } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
6
- import { isTruthyEnvValue } from "./infra/env.js";
6
+ import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
7
7
  import { installProcessWarningFilter } from "./infra/warnings.js";
8
8
  import { attachChildProcessBridge } from "./process/child-process-bridge.js";
9
9
  process.title = "poolbot";
10
10
  installProcessWarningFilter();
11
+ normalizeEnv();
11
12
  if (process.argv.includes("--no-color")) {
12
13
  process.env.NO_COLOR = "1";
13
14
  process.env.FORCE_COLOR = "0";
14
15
  }
15
16
  const EXPERIMENTAL_WARNING_FLAG = "--disable-warning=ExperimentalWarning";
16
- function hasExperimentalWarningSuppressed(nodeOptions) {
17
- if (!nodeOptions)
18
- return false;
19
- return nodeOptions.includes(EXPERIMENTAL_WARNING_FLAG) || nodeOptions.includes("--no-warnings");
17
+ function hasExperimentalWarningSuppressed() {
18
+ const nodeOptions = process.env.NODE_OPTIONS ?? "";
19
+ if (nodeOptions.includes(EXPERIMENTAL_WARNING_FLAG) || nodeOptions.includes("--no-warnings"))
20
+ return true;
21
+ for (const arg of process.execArgv) {
22
+ if (arg === EXPERIMENTAL_WARNING_FLAG || arg === "--no-warnings")
23
+ return true;
24
+ }
25
+ return false;
20
26
  }
21
27
  function ensureExperimentalWarningSuppressed() {
22
28
  if (isTruthyEnvValue(process.env.CLAWDBOT_NO_RESPAWN))
23
29
  return false;
24
30
  if (isTruthyEnvValue(process.env.CLAWDBOT_NODE_OPTIONS_READY))
25
31
  return false;
26
- const nodeOptions = process.env.NODE_OPTIONS ?? "";
27
- if (hasExperimentalWarningSuppressed(nodeOptions))
32
+ if (hasExperimentalWarningSuppressed())
28
33
  return false;
34
+ // Respawn guard (and keep recursion bounded if something goes wrong).
29
35
  process.env.CLAWDBOT_NODE_OPTIONS_READY = "1";
30
- process.env.NODE_OPTIONS = `${nodeOptions} ${EXPERIMENTAL_WARNING_FLAG}`.trim();
31
- const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
36
+ // Pass flag as a Node CLI option, not via NODE_OPTIONS (--disable-warning is disallowed in NODE_OPTIONS).
37
+ const child = spawn(process.execPath, [EXPERIMENTAL_WARNING_FLAG, ...process.execArgv, ...process.argv.slice(1)], {
32
38
  stdio: "inherit",
33
39
  env: process.env,
34
40
  });