pi-roundtable 0.1.0

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 (315) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -0
  4. package/docs/plugins.md +924 -0
  5. package/examples/channels.test.ts +13 -0
  6. package/examples/channels.ts +27 -0
  7. package/examples/dashboard.test.ts +11 -0
  8. package/examples/dashboard.ts +9 -0
  9. package/examples/events.test.ts +26 -0
  10. package/examples/events.ts +25 -0
  11. package/examples/guide.test.ts +52 -0
  12. package/examples/holds.test.ts +16 -0
  13. package/examples/holds.ts +30 -0
  14. package/examples/http.test.ts +12 -0
  15. package/examples/http.ts +17 -0
  16. package/examples/interactions.test.ts +25 -0
  17. package/examples/interactions.ts +28 -0
  18. package/examples/migrations.test.ts +31 -0
  19. package/examples/migrations.ts +42 -0
  20. package/examples/packages.test.ts +9 -0
  21. package/examples/packages.ts +9 -0
  22. package/examples/preflight.test.ts +11 -0
  23. package/examples/preflight.ts +19 -0
  24. package/examples/prompt.test.ts +25 -0
  25. package/examples/prompt.ts +20 -0
  26. package/examples/providers.test.ts +10 -0
  27. package/examples/providers.ts +17 -0
  28. package/examples/seeds.test.ts +12 -0
  29. package/examples/seeds.ts +18 -0
  30. package/examples/selection.test.ts +13 -0
  31. package/examples/selection.ts +14 -0
  32. package/examples/services.test.ts +20 -0
  33. package/examples/services.ts +30 -0
  34. package/examples/session-tools.test.ts +24 -0
  35. package/examples/session-tools.ts +34 -0
  36. package/examples/tools.test.ts +18 -0
  37. package/examples/tools.ts +28 -0
  38. package/package.json +55 -0
  39. package/src/cli/add-plugin.test.ts +107 -0
  40. package/src/cli/add-plugin.ts +75 -0
  41. package/src/cli/checks/basic.test.ts +276 -0
  42. package/src/cli/checks/bun.ts +26 -0
  43. package/src/cli/checks/configuration.ts +28 -0
  44. package/src/cli/checks/database.test.ts +117 -0
  45. package/src/cli/checks/database.ts +100 -0
  46. package/src/cli/checks/discord.test.ts +200 -0
  47. package/src/cli/checks/discord.ts +218 -0
  48. package/src/cli/checks/environment.ts +27 -0
  49. package/src/cli/checks/model.ts +28 -0
  50. package/src/cli/checks/public-url.ts +40 -0
  51. package/src/cli/cli.test.ts +122 -0
  52. package/src/cli/cli.ts +166 -0
  53. package/src/cli/config-edit.test.ts +130 -0
  54. package/src/cli/config-edit.ts +199 -0
  55. package/src/cli/discord-api.ts +96 -0
  56. package/src/cli/doctor.test.ts +184 -0
  57. package/src/cli/doctor.ts +92 -0
  58. package/src/cli/http.ts +23 -0
  59. package/src/cli/init.test.ts +127 -0
  60. package/src/cli/init.ts +82 -0
  61. package/src/cli/main.ts +13 -0
  62. package/src/cli/project.ts +123 -0
  63. package/src/cli/report.ts +68 -0
  64. package/src/cli/roundtable.mjs +22 -0
  65. package/src/cli/runtime.ts +53 -0
  66. package/src/cli/size.test.ts +9 -0
  67. package/src/cli/start.ts +33 -0
  68. package/src/cli/templates.test.ts +129 -0
  69. package/src/cli/templates.ts +119 -0
  70. package/src/cli/testing/fixtures.ts +106 -0
  71. package/src/core/agents/agent-claim.test.ts +142 -0
  72. package/src/core/agents/agent-claim.ts +170 -0
  73. package/src/core/agents/agent-dashboard.test.ts +120 -0
  74. package/src/core/agents/agent-dashboard.ts +185 -0
  75. package/src/core/agents/agent-guild.test.ts +124 -0
  76. package/src/core/agents/agent-messages.ts +189 -0
  77. package/src/core/agents/agent-ports.ts +103 -0
  78. package/src/core/agents/agent-prompt.ts +163 -0
  79. package/src/core/agents/agent-rules.ts +106 -0
  80. package/src/core/agents/agent-schema.ts +115 -0
  81. package/src/core/agents/agent-settings.ts +49 -0
  82. package/src/core/agents/agent-store.test.ts +205 -0
  83. package/src/core/agents/agent-store.ts +384 -0
  84. package/src/core/agents/agent-team.ts +445 -0
  85. package/src/core/agents/agent-tools.ts +386 -0
  86. package/src/core/agents/avatar-studio.test.ts +88 -0
  87. package/src/core/agents/avatar-studio.ts +147 -0
  88. package/src/core/agents/group-messages.ts +99 -0
  89. package/src/core/agents/group-round.test.ts +143 -0
  90. package/src/core/agents/group-round.ts +117 -0
  91. package/src/core/agents/group-turns.ts +132 -0
  92. package/src/core/agents/owner-identity.test.ts +172 -0
  93. package/src/core/agents/team-editing.ts +205 -0
  94. package/src/core/agents/team-keys.ts +40 -0
  95. package/src/core/agents/team-layout.ts +135 -0
  96. package/src/core/agents/team-lifecycle.ts +188 -0
  97. package/src/core/agents/team-options.ts +52 -0
  98. package/src/core/agents/team-status.ts +80 -0
  99. package/src/core/agents/team-text.ts +60 -0
  100. package/src/core/agents/team-turn-types.ts +107 -0
  101. package/src/core/agents/team-turns.test.ts +278 -0
  102. package/src/core/agents/team-turns.ts +323 -0
  103. package/src/core/assets/neutral.png +0 -0
  104. package/src/core/assets/prompts/shared-guest.md +11 -0
  105. package/src/core/assets/prompts/shared.md +11 -0
  106. package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
  107. package/src/core/attachments/attachment-dir.ts +15 -0
  108. package/src/core/attachments/attachment-fetcher.ts +83 -0
  109. package/src/core/attachments/attachments.test.ts +85 -0
  110. package/src/core/attachments/image-prep.ts +50 -0
  111. package/src/core/attachments/prompt-block.ts +37 -0
  112. package/src/core/attachments/turn-attachments.ts +79 -0
  113. package/src/core/boundary.test.ts +45 -0
  114. package/src/core/builtin/agent-server.ts +275 -0
  115. package/src/core/builtin/discord.ts +95 -0
  116. package/src/core/builtin/modules.test.ts +127 -0
  117. package/src/core/builtin/modules.ts +176 -0
  118. package/src/core/builtin/seeds.ts +10 -0
  119. package/src/core/builtin/stores.ts +42 -0
  120. package/src/core/config/config.test.ts +135 -0
  121. package/src/core/config/config.ts +285 -0
  122. package/src/core/config/schema.ts +181 -0
  123. package/src/core/contract/channels.ts +152 -0
  124. package/src/core/contract/discord.ts +33 -0
  125. package/src/core/contract/providers.ts +67 -0
  126. package/src/core/db/guild-scope.ts +53 -0
  127. package/src/core/db/migrations.test.ts +237 -0
  128. package/src/core/db/migrations.ts +37 -0
  129. package/src/core/define-roundtable.test.ts +134 -0
  130. package/src/core/define-roundtable.ts +195 -0
  131. package/src/core/define.test.ts +144 -0
  132. package/src/core/define.ts +154 -0
  133. package/src/core/discord/agent-commands.test.ts +58 -0
  134. package/src/core/discord/agent-commands.ts +385 -0
  135. package/src/core/discord/agent-discord.test.ts +70 -0
  136. package/src/core/discord/agent-discord.ts +367 -0
  137. package/src/core/discord/channel-executor.ts +404 -0
  138. package/src/core/discord/channel-operations.ts +329 -0
  139. package/src/core/discord/discord-surface.ts +431 -0
  140. package/src/core/discord/dispatch-thread-host.test.ts +114 -0
  141. package/src/core/discord/dispatch-thread-host.ts +72 -0
  142. package/src/core/discord/dispatch-threads.test.ts +94 -0
  143. package/src/core/discord/dispatch-threads.ts +180 -0
  144. package/src/core/discord/interaction-module.ts +1 -0
  145. package/src/core/discord/owner-cards.test.ts +435 -0
  146. package/src/core/discord/owner-cards.ts +463 -0
  147. package/src/core/discord/owner-command.ts +126 -0
  148. package/src/core/discord/owner-discord-access.ts +183 -0
  149. package/src/core/discord/owner-discord-threads.ts +160 -0
  150. package/src/core/discord/owner-discord.test.ts +325 -0
  151. package/src/core/discord/owner-discord.ts +282 -0
  152. package/src/core/discord/owner-panel.ts +103 -0
  153. package/src/core/discord/schedule-commands.ts +149 -0
  154. package/src/core/discord/stop-button.ts +20 -0
  155. package/src/core/domain/attachment.ts +39 -0
  156. package/src/core/domain/conversation.ts +42 -0
  157. package/src/core/domain/errors.ts +31 -0
  158. package/src/core/domain/expression.ts +21 -0
  159. package/src/core/domain/owner-prompts.ts +45 -0
  160. package/src/core/domain/ports.ts +91 -0
  161. package/src/core/domain/profile.ts +32 -0
  162. package/src/core/drain.test.ts +46 -0
  163. package/src/core/drain.ts +33 -0
  164. package/src/core/errors.ts +29 -0
  165. package/src/core/events.test.ts +81 -0
  166. package/src/core/events.ts +55 -0
  167. package/src/core/holds.test.ts +54 -0
  168. package/src/core/holds.ts +46 -0
  169. package/src/core/host.test.ts +536 -0
  170. package/src/core/host.ts +297 -0
  171. package/src/core/http/listeners.test.ts +164 -0
  172. package/src/core/http/listeners.ts +151 -0
  173. package/src/core/i18n/agent-panel.ts +96 -0
  174. package/src/core/i18n/cards.ts +64 -0
  175. package/src/core/i18n/channels.ts +28 -0
  176. package/src/core/i18n/conversation.ts +49 -0
  177. package/src/core/i18n/dashboard.ts +72 -0
  178. package/src/core/i18n/discord.ts +23 -0
  179. package/src/core/i18n/en.ts +27 -0
  180. package/src/core/i18n/i18n.test.ts +139 -0
  181. package/src/core/i18n/index.ts +58 -0
  182. package/src/core/i18n/owner.ts +35 -0
  183. package/src/core/i18n/schedules.ts +92 -0
  184. package/src/core/i18n/time-zones.ts +27 -0
  185. package/src/core/i18n/types.ts +7 -0
  186. package/src/core/i18n/zh-tw.ts +26 -0
  187. package/src/core/identity.test.ts +18 -0
  188. package/src/core/identity.ts +31 -0
  189. package/src/core/judging/confirmation-judge.ts +68 -0
  190. package/src/core/judging/effort-judge.test.ts +112 -0
  191. package/src/core/judging/effort-judge.ts +158 -0
  192. package/src/core/judging/model-judge.test.ts +126 -0
  193. package/src/core/judging/model-judge.ts +193 -0
  194. package/src/core/log.ts +45 -0
  195. package/src/core/models.ts +52 -0
  196. package/src/core/modules/background/background-turns.ts +78 -0
  197. package/src/core/modules/delegation/delegate.ts +54 -0
  198. package/src/core/modules/delegation/delegator.test.ts +170 -0
  199. package/src/core/modules/delegation/delegator.ts +191 -0
  200. package/src/core/modules/delegation/sol-worker.ts +78 -0
  201. package/src/core/modules/discord-admin/discord-admin.ts +288 -0
  202. package/src/core/modules/host-shell/shell-policy.ts +386 -0
  203. package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
  204. package/src/core/modules/memory/owner-memory-store.ts +228 -0
  205. package/src/core/modules/memory/owner-memory.ts +153 -0
  206. package/src/core/modules/notify/notify.ts +26 -0
  207. package/src/core/modules/schedules/recurrence.ts +206 -0
  208. package/src/core/modules/schedules/schedule-store.ts +213 -0
  209. package/src/core/modules/schedules/schedule-tools.ts +196 -0
  210. package/src/core/modules/schedules/schedule.test.ts +358 -0
  211. package/src/core/modules/schedules/scheduler.ts +121 -0
  212. package/src/core/modules/schedules/schedules.ts +87 -0
  213. package/src/core/modules/skills/repo-name.ts +11 -0
  214. package/src/core/modules/skills/skill-kind.test.ts +55 -0
  215. package/src/core/modules/skills/skill-link.ts +69 -0
  216. package/src/core/modules/skills/skill-listing.ts +128 -0
  217. package/src/core/modules/skills/skill-registry.test.ts +376 -0
  218. package/src/core/modules/skills/skill-registry.ts +323 -0
  219. package/src/core/modules/skills/skill-rules.ts +120 -0
  220. package/src/core/modules/skills/skill-store.ts +285 -0
  221. package/src/core/modules/skills/skill-tools.ts +207 -0
  222. package/src/core/ops/error-reporter.test.ts +283 -0
  223. package/src/core/ops/error-reporter.ts +271 -0
  224. package/src/core/plugin.ts +200 -0
  225. package/src/core/presentation/card-cadence.ts +41 -0
  226. package/src/core/presentation/headline.ts +34 -0
  227. package/src/core/presentation/presentation.test.ts +166 -0
  228. package/src/core/presentation/quiet-links.ts +36 -0
  229. package/src/core/presentation/reply-splitter.ts +110 -0
  230. package/src/core/presentation/thinking-line.ts +24 -0
  231. package/src/core/public-entry.test.ts +59 -0
  232. package/src/core/registry/contributions.test.ts +215 -0
  233. package/src/core/registry/contributions.ts +249 -0
  234. package/src/core/registry/interactions.test.ts +87 -0
  235. package/src/core/registry/interactions.ts +55 -0
  236. package/src/core/registry/providers.test.ts +103 -0
  237. package/src/core/registry/providers.ts +55 -0
  238. package/src/core/routing/channel-queue.test.ts +31 -0
  239. package/src/core/routing/channel-queue.ts +44 -0
  240. package/src/core/routing/channel-router.test.ts +326 -0
  241. package/src/core/routing/channel-router.ts +157 -0
  242. package/src/core/routing/conversation-kind.ts +14 -0
  243. package/src/core/routing/forward-join.ts +65 -0
  244. package/src/core/routing/message-text.ts +16 -0
  245. package/src/core/routing/settle-turn.test.ts +21 -0
  246. package/src/core/routing/settle-turn.ts +26 -0
  247. package/src/core/runtime/compaction-tiers.test.ts +227 -0
  248. package/src/core/runtime/compaction-tiers.ts +203 -0
  249. package/src/core/runtime/conversation-sessions.ts +162 -0
  250. package/src/core/runtime/extensions/agent-prompt.ts +16 -0
  251. package/src/core/runtime/extensions/ask-user.test.ts +114 -0
  252. package/src/core/runtime/extensions/ask-user.ts +98 -0
  253. package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
  254. package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
  255. package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
  256. package/src/core/runtime/mcp.ts +23 -0
  257. package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
  258. package/src/core/runtime/pending-confirmation-store.ts +75 -0
  259. package/src/core/runtime/pi-agent-runtime.ts +477 -0
  260. package/src/core/runtime/prompt-slot.ts +81 -0
  261. package/src/core/runtime/runtime-types.ts +150 -0
  262. package/src/core/runtime/session-archive.test.ts +20 -0
  263. package/src/core/runtime/session-archive.ts +17 -0
  264. package/src/core/runtime/session-factory.ts +291 -0
  265. package/src/core/runtime/steerable-run.test.ts +264 -0
  266. package/src/core/runtime/steerable-run.ts +109 -0
  267. package/src/core/runtime/text-tools.test.ts +110 -0
  268. package/src/core/runtime/text-tools.ts +66 -0
  269. package/src/core/runtime/turn-answer.test.ts +72 -0
  270. package/src/core/runtime/turn-answer.ts +79 -0
  271. package/src/core/runtime/worker-task.test.ts +82 -0
  272. package/src/core/runtime/worker-task.ts +65 -0
  273. package/src/core/services.test.ts +28 -0
  274. package/src/core/services.ts +107 -0
  275. package/src/core/sessions.test.ts +85 -0
  276. package/src/core/sessions.ts +149 -0
  277. package/src/core/shared/attachment-reader.ts +91 -0
  278. package/src/core/shared/delegate-tool.ts +15 -0
  279. package/src/core/shared/mcp-adapter.ts +58 -0
  280. package/src/core/shared/package-dir.ts +9 -0
  281. package/src/core/shared/profile-tools.ts +16 -0
  282. package/src/core/shared/read-attachment-tool.ts +43 -0
  283. package/src/core/shared/schedule-tools.ts +118 -0
  284. package/src/core/shared/session-messages.ts +50 -0
  285. package/src/core/shared/tool-result.ts +9 -0
  286. package/src/core/shared/unix-server.ts +18 -0
  287. package/src/core/size.test.ts +9 -0
  288. package/src/core/speakers.test.ts +78 -0
  289. package/src/core/speakers.ts +143 -0
  290. package/src/core/testing/database.ts +52 -0
  291. package/src/core/testing/file-size.ts +34 -0
  292. package/src/core/testing/locale.ts +8 -0
  293. package/src/core/testing/modules.ts +97 -0
  294. package/src/core/testing/owner.ts +15 -0
  295. package/src/core/testing/thread-host.ts +60 -0
  296. package/src/core/time.test.ts +166 -0
  297. package/src/core/time.ts +67 -0
  298. package/src/core/tool-tiers.test.ts +117 -0
  299. package/src/core/tool-tiers.ts +127 -0
  300. package/src/entries.test.ts +112 -0
  301. package/src/index.ts +28 -0
  302. package/src/testing.test.ts +156 -0
  303. package/src/testing.ts +182 -0
  304. package/templates/.env.example +22 -0
  305. package/templates/README.md +11 -0
  306. package/templates/_gitignore +3 -0
  307. package/templates/agents.ts +12 -0
  308. package/templates/biome.json.tmpl +7 -0
  309. package/templates/docker-compose.yml +16 -0
  310. package/templates/package.json.tmpl +24 -0
  311. package/templates/persona/shared.md +2 -0
  312. package/templates/plugin/plugin.test.ts.tmpl +13 -0
  313. package/templates/plugin/plugin.ts +18 -0
  314. package/templates/roundtable.config.ts +24 -0
  315. package/templates/tsconfig.json.tmpl +16 -0
@@ -0,0 +1,404 @@
1
+ import {
2
+ type AnyThreadChannel,
3
+ type APIMessage,
4
+ ChannelType,
5
+ type Client,
6
+ type Message,
7
+ type MessageCreateOptions,
8
+ type MessageEditOptions,
9
+ type NewsChannel,
10
+ PermissionFlagsBits,
11
+ type PermissionResolvable,
12
+ type TextChannel,
13
+ } from "discord.js";
14
+ import type { ChannelOperation } from "./channel-operations.ts";
15
+ import {
16
+ type ChannelExecutor,
17
+ ChannelToolError,
18
+ } from "./channel-operations.ts";
19
+
20
+ /** Discord permissions the bot (and the owner granting it) needs for each operation. */
21
+ export const OPERATION_PERMISSIONS: Record<ChannelOperation, bigint[]> = {
22
+ read: [
23
+ PermissionFlagsBits.ViewChannel,
24
+ PermissionFlagsBits.ReadMessageHistory,
25
+ ],
26
+ send: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages],
27
+ edit: [
28
+ PermissionFlagsBits.ViewChannel,
29
+ PermissionFlagsBits.ReadMessageHistory,
30
+ PermissionFlagsBits.SendMessages,
31
+ ],
32
+ pin: [
33
+ PermissionFlagsBits.ViewChannel,
34
+ PermissionFlagsBits.ReadMessageHistory,
35
+ PermissionFlagsBits.PinMessages,
36
+ ],
37
+ delete: [
38
+ PermissionFlagsBits.ViewChannel,
39
+ PermissionFlagsBits.ReadMessageHistory,
40
+ PermissionFlagsBits.ManageMessages,
41
+ ],
42
+ channel: [
43
+ PermissionFlagsBits.ViewChannel,
44
+ PermissionFlagsBits.ManageChannels,
45
+ ],
46
+ permissions: [
47
+ PermissionFlagsBits.ViewChannel,
48
+ PermissionFlagsBits.ManageRoles,
49
+ ],
50
+ };
51
+
52
+ export type ManagedChannel = TextChannel | NewsChannel;
53
+ /** What the owner's own agent may also reach: threads of those channels. */
54
+ export type OwnerChannel = ManagedChannel | AnyThreadChannel;
55
+
56
+ /** A guild text or announcement channel, fetched fresh; anything else is refused. */
57
+ export async function fetchManagedChannel(
58
+ client: Client,
59
+ channelId: string,
60
+ ): Promise<ManagedChannel> {
61
+ const channel = await fetchOwnerChannel(client, channelId);
62
+ if (channel.isThread()) throw new ChannelToolError("INVALID_CHANNEL");
63
+ return channel;
64
+ }
65
+
66
+ /** A guild text or announcement channel or a thread, fetched fresh; anything else is refused. */
67
+ export async function fetchOwnerChannel(
68
+ client: Client,
69
+ channelId: string,
70
+ ): Promise<OwnerChannel> {
71
+ const channel = await client.channels
72
+ .fetch(channelId, { force: true })
73
+ .catch(() => null);
74
+ if (
75
+ !channel ||
76
+ (channel.type !== ChannelType.GuildText &&
77
+ channel.type !== ChannelType.GuildAnnouncement &&
78
+ !channel.isThread())
79
+ )
80
+ throw new ChannelToolError("INVALID_CHANNEL");
81
+ return channel;
82
+ }
83
+
84
+ /** The permissions an operation needs in a channel; posting in a thread has its own. */
85
+ export function operationPermissions(
86
+ operation: ChannelOperation,
87
+ channel: OwnerChannel,
88
+ ): bigint[] {
89
+ const needed = OPERATION_PERMISSIONS[operation];
90
+ return channel.isThread()
91
+ ? needed.map((flag) =>
92
+ flag === PermissionFlagsBits.SendMessages
93
+ ? PermissionFlagsBits.SendMessagesInThreads
94
+ : flag,
95
+ )
96
+ : needed;
97
+ }
98
+
99
+ interface ChannelFile {
100
+ filename: string;
101
+ dataBase64: string;
102
+ description?: string;
103
+ }
104
+
105
+ function serializeMessage(message: Message) {
106
+ return {
107
+ id: message.id,
108
+ channelId: message.channelId,
109
+ authorId: message.author.id,
110
+ content: message.content,
111
+ createdAt: message.createdAt.toISOString(),
112
+ editedAt: message.editedAt?.toISOString() ?? null,
113
+ attachments: message.attachments.map((file) => ({
114
+ id: file.id,
115
+ filename: file.name,
116
+ url: file.url,
117
+ contentType: file.contentType,
118
+ size: file.size,
119
+ width: file.width,
120
+ height: file.height,
121
+ description: file.description,
122
+ })),
123
+ embeds: message.embeds.map((embed) => embed.toJSON()),
124
+ };
125
+ }
126
+
127
+ function messagePayload(
128
+ args: Record<string, unknown>,
129
+ ): Pick<MessageCreateOptions, "content" | "allowedMentions" | "files"> {
130
+ const uploads = args.files as ChannelFile[] | undefined;
131
+ return {
132
+ ...(args.content !== undefined ? { content: String(args.content) } : {}),
133
+ allowedMentions: { parse: [] },
134
+ ...(uploads
135
+ ? {
136
+ files: uploads.map((upload) => ({
137
+ attachment: Buffer.from(upload.dataBase64, "base64"),
138
+ name: upload.filename,
139
+ ...(upload.description === undefined
140
+ ? {}
141
+ : { description: upload.description }),
142
+ })),
143
+ }
144
+ : {}),
145
+ };
146
+ }
147
+
148
+ function editPayload(
149
+ message: Message,
150
+ botId: string | undefined,
151
+ args: Record<string, unknown>,
152
+ ): MessageEditOptions {
153
+ if (!botId || message.author.id !== botId)
154
+ throw new ChannelToolError("INVALID_CHANNEL_MESSAGE_AUTHOR");
155
+ const keep = args.keepAttachmentIds as string[] | undefined;
156
+ if (
157
+ keep?.some((id) => !message.attachments.has(id)) ||
158
+ (keep && new Set(keep).size !== keep.length)
159
+ )
160
+ throw new ChannelToolError("INVALID_CHANNEL_ATTACHMENT_ID");
161
+ const retained = keep ?? [...message.attachments.keys()];
162
+ const added = (args.files as ChannelFile[] | undefined)?.length ?? 0;
163
+ if (retained.length + added > 10)
164
+ throw new ChannelToolError("INVALID_CHANNEL_ATTACHMENT_COUNT");
165
+ return {
166
+ ...messagePayload(args),
167
+ ...(keep === undefined ? {} : { attachments: keep.map((id) => ({ id })) }),
168
+ };
169
+ }
170
+
171
+ interface SearchResponse {
172
+ code?: number;
173
+ retry_after?: number;
174
+ doing_deep_historical_index?: boolean;
175
+ total_results?: number;
176
+ messages?: APIMessage[][];
177
+ }
178
+
179
+ /** Snowflake bound for a date: the first ID Discord could assign at that time. */
180
+ const snowflakeAt = (date: string): bigint =>
181
+ (BigInt(Date.parse(date)) - 1420070400000n) << 22n;
182
+
183
+ async function searchMessages(
184
+ client: Client,
185
+ channel: OwnerChannel,
186
+ args: Record<string, unknown>,
187
+ ) {
188
+ const offset = Number(args.offset);
189
+ const limit = Number(args.limit);
190
+ const query = new URLSearchParams({
191
+ channel_id: channel.id,
192
+ limit: String(limit),
193
+ offset: String(offset),
194
+ sort_by: args.sort === "relevance" ? "relevance" : "timestamp",
195
+ sort_order: args.sort === "oldest" ? "asc" : "desc",
196
+ include_nsfw: String(
197
+ channel.isThread() ? (channel.parent?.nsfw ?? false) : channel.nsfw,
198
+ ),
199
+ });
200
+ if (args.query !== undefined) query.set("content", String(args.query));
201
+ if (args.authorId !== undefined)
202
+ query.set("author_id", String(args.authorId));
203
+ if (args.has !== undefined) query.set("has", String(args.has));
204
+ if (args.afterDate !== undefined) {
205
+ const bound = snowflakeAt(String(args.afterDate));
206
+ query.set("min_id", (bound > 0n ? bound - 1n : bound).toString());
207
+ }
208
+ if (args.beforeDate !== undefined)
209
+ query.set("max_id", snowflakeAt(String(args.beforeDate)).toString());
210
+ const response = (await client.rest.get(
211
+ `/guilds/${channel.guildId}/messages/search`,
212
+ { query },
213
+ )) as SearchResponse;
214
+ const base = { offset, limit, totalIsApproximate: true as const };
215
+ if (response.code === 110000)
216
+ return {
217
+ status: "indexing",
218
+ messages: [],
219
+ indexing: true,
220
+ retryAfterSeconds: Math.max(1, response.retry_after ?? 1),
221
+ pagination: {
222
+ ...base,
223
+ totalResults: null,
224
+ hasMore: null,
225
+ nextOffset: null,
226
+ limitReached: false,
227
+ },
228
+ };
229
+ const total = response.total_results;
230
+ if (
231
+ !Array.isArray(response.messages) ||
232
+ typeof total !== "number" ||
233
+ !Number.isInteger(total) ||
234
+ total < 0
235
+ )
236
+ throw new Error("unexpected search response");
237
+ const messages = response.messages.flat();
238
+ // Never pass on another channel's messages, even if Discord's filter changes.
239
+ if (messages.some((m) => m.channel_id !== channel.id))
240
+ throw new Error("search returned another channel");
241
+ const hasMore = offset + limit < total;
242
+ const limitReached = hasMore && offset + limit > 9975;
243
+ return {
244
+ status: "ready",
245
+ messages: messages.map((m) => ({
246
+ id: m.id,
247
+ channelId: m.channel_id,
248
+ authorId: m.author.id,
249
+ content: m.content,
250
+ createdAt: m.timestamp,
251
+ editedAt: m.edited_timestamp,
252
+ attachments: m.attachments.map((f) => ({
253
+ id: f.id,
254
+ filename: f.filename,
255
+ url: f.url,
256
+ size: f.size,
257
+ contentType: f.content_type ?? null,
258
+ width: f.width ?? null,
259
+ height: f.height ?? null,
260
+ description: f.description ?? null,
261
+ })),
262
+ embeds: m.embeds,
263
+ })),
264
+ indexing: response.doing_deep_historical_index ?? false,
265
+ retryAfterSeconds: null,
266
+ pagination: {
267
+ ...base,
268
+ totalResults: total,
269
+ hasMore,
270
+ nextOffset: hasMore && !limitReached ? offset + limit : null,
271
+ limitReached,
272
+ },
273
+ };
274
+ }
275
+
276
+ async function editPermissions(
277
+ channel: ManagedChannel,
278
+ tool: string,
279
+ args: Record<string, unknown>,
280
+ ) {
281
+ const targetId = String(args.targetId);
282
+ if (tool === "discord_delete_channel_permissions") {
283
+ await channel.permissionOverwrites.delete(targetId);
284
+ return { channelId: channel.id, targetId };
285
+ }
286
+ const allow = (args.allow ?? []) as string[];
287
+ const deny = (args.deny ?? []) as string[];
288
+ for (const permission of [...allow, ...deny]) {
289
+ if (!Object.hasOwn(PermissionFlagsBits, permission))
290
+ throw new ChannelToolError("INVALID_PERMISSION");
291
+ }
292
+ // The bot cannot hand out a permission it does not hold itself.
293
+ const bot = await channel.guild.members.fetchMe({ force: true });
294
+ if (
295
+ !channel
296
+ .permissionsFor(bot)
297
+ .has([...allow, ...deny] as PermissionResolvable[])
298
+ )
299
+ throw new ChannelToolError("BOT_PERMISSION_MISSING");
300
+ await channel.permissionOverwrites.edit(
301
+ targetId,
302
+ Object.fromEntries([
303
+ ...allow.map((p) => [p, true]),
304
+ ...deny.map((p) => [p, false]),
305
+ ]),
306
+ );
307
+ return { channelId: channel.id, targetId };
308
+ }
309
+
310
+ /**
311
+ * Channel operations over the bot's own Discord connection. Threads are reached only for the
312
+ * owner's agent; outside agents' grants name text and announcement channels.
313
+ */
314
+ export function discordChannelExecutor(
315
+ client: Client,
316
+ options: { threads?: boolean } = {},
317
+ ): ChannelExecutor {
318
+ const fetchChannel = (channelId: string): Promise<OwnerChannel> =>
319
+ options.threads
320
+ ? fetchOwnerChannel(client, channelId)
321
+ : fetchManagedChannel(client, channelId);
322
+ return {
323
+ async inspect(channelId, operation) {
324
+ const channel = await fetchManagedChannel(client, channelId);
325
+ const bot = await channel.guild.members.fetchMe({ force: true });
326
+ if (!channel.permissionsFor(bot).has(OPERATION_PERMISSIONS[operation]))
327
+ throw new ChannelToolError("BOT_PERMISSION_MISSING");
328
+ return { guildId: channel.guildId };
329
+ },
330
+
331
+ async names(channelId) {
332
+ try {
333
+ const channel = await fetchManagedChannel(client, channelId);
334
+ return { guildName: channel.guild.name, channelName: channel.name };
335
+ } catch {
336
+ return undefined;
337
+ }
338
+ },
339
+
340
+ async execute(tool, args) {
341
+ const channel = await fetchChannel(String(args.channelId));
342
+ switch (tool) {
343
+ case "discord_get_channel_info":
344
+ return {
345
+ id: channel.id,
346
+ guildId: channel.guildId,
347
+ guildName: channel.guild.name,
348
+ name: channel.name,
349
+ ...(channel.isThread()
350
+ ? {
351
+ thread: true,
352
+ parentId: channel.parentId,
353
+ archived: channel.archived,
354
+ locked: channel.locked,
355
+ }
356
+ : { topic: channel.topic }),
357
+ };
358
+ case "discord_get_messages": {
359
+ const messages = await channel.messages.fetch({
360
+ limit: Number(args.limit),
361
+ before: args.before as string | undefined,
362
+ after: args.after as string | undefined,
363
+ around: args.around as string | undefined,
364
+ });
365
+ return { messages: messages.map(serializeMessage) };
366
+ }
367
+ case "discord_search_messages":
368
+ return searchMessages(client, channel, args);
369
+ case "discord_get_pinned_messages": {
370
+ const pinned = await channel.messages.fetchPins();
371
+ return {
372
+ messages: pinned.items.map((pin) => serializeMessage(pin.message)),
373
+ };
374
+ }
375
+ case "discord_send_message":
376
+ return serializeMessage(await channel.send(messagePayload(args)));
377
+ case "discord_edit_channel": {
378
+ if (channel.isThread())
379
+ throw new ChannelToolError(
380
+ "INVALID_CHANNEL: use discord_edit_thread",
381
+ );
382
+ const { channelId: _id, ...settings } = args;
383
+ await channel.edit(settings);
384
+ return { id: channel.id };
385
+ }
386
+ case "discord_set_channel_permissions":
387
+ case "discord_delete_channel_permissions":
388
+ if (channel.isThread())
389
+ throw new ChannelToolError(
390
+ "INVALID_CHANNEL: a thread takes its parent channel's permissions",
391
+ );
392
+ return editPermissions(channel, tool, args);
393
+ }
394
+ const message = await channel.messages.fetch(String(args.messageId));
395
+ if (tool === "discord_edit_message")
396
+ await message.edit(editPayload(message, client.user?.id, args));
397
+ else if (tool === "discord_delete_message") await message.delete();
398
+ else if (tool === "discord_pin_message") await message.pin();
399
+ else if (tool === "discord_unpin_message") await message.unpin();
400
+ else throw new ChannelToolError("INVALID_CHANNEL_OPERATION");
401
+ return { id: message.id, channelId: channel.id };
402
+ },
403
+ };
404
+ }