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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/docs/plugins.md +924 -0
- package/examples/channels.test.ts +13 -0
- package/examples/channels.ts +27 -0
- package/examples/dashboard.test.ts +11 -0
- package/examples/dashboard.ts +9 -0
- package/examples/events.test.ts +26 -0
- package/examples/events.ts +25 -0
- package/examples/guide.test.ts +52 -0
- package/examples/holds.test.ts +16 -0
- package/examples/holds.ts +30 -0
- package/examples/http.test.ts +12 -0
- package/examples/http.ts +17 -0
- package/examples/interactions.test.ts +25 -0
- package/examples/interactions.ts +28 -0
- package/examples/migrations.test.ts +31 -0
- package/examples/migrations.ts +42 -0
- package/examples/packages.test.ts +9 -0
- package/examples/packages.ts +9 -0
- package/examples/preflight.test.ts +11 -0
- package/examples/preflight.ts +19 -0
- package/examples/prompt.test.ts +25 -0
- package/examples/prompt.ts +20 -0
- package/examples/providers.test.ts +10 -0
- package/examples/providers.ts +17 -0
- package/examples/seeds.test.ts +12 -0
- package/examples/seeds.ts +18 -0
- package/examples/selection.test.ts +13 -0
- package/examples/selection.ts +14 -0
- package/examples/services.test.ts +20 -0
- package/examples/services.ts +30 -0
- package/examples/session-tools.test.ts +24 -0
- package/examples/session-tools.ts +34 -0
- package/examples/tools.test.ts +18 -0
- package/examples/tools.ts +28 -0
- package/package.json +55 -0
- package/src/cli/add-plugin.test.ts +107 -0
- package/src/cli/add-plugin.ts +75 -0
- package/src/cli/checks/basic.test.ts +276 -0
- package/src/cli/checks/bun.ts +26 -0
- package/src/cli/checks/configuration.ts +28 -0
- package/src/cli/checks/database.test.ts +117 -0
- package/src/cli/checks/database.ts +100 -0
- package/src/cli/checks/discord.test.ts +200 -0
- package/src/cli/checks/discord.ts +218 -0
- package/src/cli/checks/environment.ts +27 -0
- package/src/cli/checks/model.ts +28 -0
- package/src/cli/checks/public-url.ts +40 -0
- package/src/cli/cli.test.ts +122 -0
- package/src/cli/cli.ts +166 -0
- package/src/cli/config-edit.test.ts +130 -0
- package/src/cli/config-edit.ts +199 -0
- package/src/cli/discord-api.ts +96 -0
- package/src/cli/doctor.test.ts +184 -0
- package/src/cli/doctor.ts +92 -0
- package/src/cli/http.ts +23 -0
- package/src/cli/init.test.ts +127 -0
- package/src/cli/init.ts +82 -0
- package/src/cli/main.ts +13 -0
- package/src/cli/project.ts +123 -0
- package/src/cli/report.ts +68 -0
- package/src/cli/roundtable.mjs +22 -0
- package/src/cli/runtime.ts +53 -0
- package/src/cli/size.test.ts +9 -0
- package/src/cli/start.ts +33 -0
- package/src/cli/templates.test.ts +129 -0
- package/src/cli/templates.ts +119 -0
- package/src/cli/testing/fixtures.ts +106 -0
- package/src/core/agents/agent-claim.test.ts +142 -0
- package/src/core/agents/agent-claim.ts +170 -0
- package/src/core/agents/agent-dashboard.test.ts +120 -0
- package/src/core/agents/agent-dashboard.ts +185 -0
- package/src/core/agents/agent-guild.test.ts +124 -0
- package/src/core/agents/agent-messages.ts +189 -0
- package/src/core/agents/agent-ports.ts +103 -0
- package/src/core/agents/agent-prompt.ts +163 -0
- package/src/core/agents/agent-rules.ts +106 -0
- package/src/core/agents/agent-schema.ts +115 -0
- package/src/core/agents/agent-settings.ts +49 -0
- package/src/core/agents/agent-store.test.ts +205 -0
- package/src/core/agents/agent-store.ts +384 -0
- package/src/core/agents/agent-team.ts +445 -0
- package/src/core/agents/agent-tools.ts +386 -0
- package/src/core/agents/avatar-studio.test.ts +88 -0
- package/src/core/agents/avatar-studio.ts +147 -0
- package/src/core/agents/group-messages.ts +99 -0
- package/src/core/agents/group-round.test.ts +143 -0
- package/src/core/agents/group-round.ts +117 -0
- package/src/core/agents/group-turns.ts +132 -0
- package/src/core/agents/owner-identity.test.ts +172 -0
- package/src/core/agents/team-editing.ts +205 -0
- package/src/core/agents/team-keys.ts +40 -0
- package/src/core/agents/team-layout.ts +135 -0
- package/src/core/agents/team-lifecycle.ts +188 -0
- package/src/core/agents/team-options.ts +52 -0
- package/src/core/agents/team-status.ts +80 -0
- package/src/core/agents/team-text.ts +60 -0
- package/src/core/agents/team-turn-types.ts +107 -0
- package/src/core/agents/team-turns.test.ts +278 -0
- package/src/core/agents/team-turns.ts +323 -0
- package/src/core/assets/neutral.png +0 -0
- package/src/core/assets/prompts/shared-guest.md +11 -0
- package/src/core/assets/prompts/shared.md +11 -0
- package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
- package/src/core/attachments/attachment-dir.ts +15 -0
- package/src/core/attachments/attachment-fetcher.ts +83 -0
- package/src/core/attachments/attachments.test.ts +85 -0
- package/src/core/attachments/image-prep.ts +50 -0
- package/src/core/attachments/prompt-block.ts +37 -0
- package/src/core/attachments/turn-attachments.ts +79 -0
- package/src/core/boundary.test.ts +45 -0
- package/src/core/builtin/agent-server.ts +275 -0
- package/src/core/builtin/discord.ts +95 -0
- package/src/core/builtin/modules.test.ts +127 -0
- package/src/core/builtin/modules.ts +176 -0
- package/src/core/builtin/seeds.ts +10 -0
- package/src/core/builtin/stores.ts +42 -0
- package/src/core/config/config.test.ts +135 -0
- package/src/core/config/config.ts +285 -0
- package/src/core/config/schema.ts +181 -0
- package/src/core/contract/channels.ts +152 -0
- package/src/core/contract/discord.ts +33 -0
- package/src/core/contract/providers.ts +67 -0
- package/src/core/db/guild-scope.ts +53 -0
- package/src/core/db/migrations.test.ts +237 -0
- package/src/core/db/migrations.ts +37 -0
- package/src/core/define-roundtable.test.ts +134 -0
- package/src/core/define-roundtable.ts +195 -0
- package/src/core/define.test.ts +144 -0
- package/src/core/define.ts +154 -0
- package/src/core/discord/agent-commands.test.ts +58 -0
- package/src/core/discord/agent-commands.ts +385 -0
- package/src/core/discord/agent-discord.test.ts +70 -0
- package/src/core/discord/agent-discord.ts +367 -0
- package/src/core/discord/channel-executor.ts +404 -0
- package/src/core/discord/channel-operations.ts +329 -0
- package/src/core/discord/discord-surface.ts +431 -0
- package/src/core/discord/dispatch-thread-host.test.ts +114 -0
- package/src/core/discord/dispatch-thread-host.ts +72 -0
- package/src/core/discord/dispatch-threads.test.ts +94 -0
- package/src/core/discord/dispatch-threads.ts +180 -0
- package/src/core/discord/interaction-module.ts +1 -0
- package/src/core/discord/owner-cards.test.ts +435 -0
- package/src/core/discord/owner-cards.ts +463 -0
- package/src/core/discord/owner-command.ts +126 -0
- package/src/core/discord/owner-discord-access.ts +183 -0
- package/src/core/discord/owner-discord-threads.ts +160 -0
- package/src/core/discord/owner-discord.test.ts +325 -0
- package/src/core/discord/owner-discord.ts +282 -0
- package/src/core/discord/owner-panel.ts +103 -0
- package/src/core/discord/schedule-commands.ts +149 -0
- package/src/core/discord/stop-button.ts +20 -0
- package/src/core/domain/attachment.ts +39 -0
- package/src/core/domain/conversation.ts +42 -0
- package/src/core/domain/errors.ts +31 -0
- package/src/core/domain/expression.ts +21 -0
- package/src/core/domain/owner-prompts.ts +45 -0
- package/src/core/domain/ports.ts +91 -0
- package/src/core/domain/profile.ts +32 -0
- package/src/core/drain.test.ts +46 -0
- package/src/core/drain.ts +33 -0
- package/src/core/errors.ts +29 -0
- package/src/core/events.test.ts +81 -0
- package/src/core/events.ts +55 -0
- package/src/core/holds.test.ts +54 -0
- package/src/core/holds.ts +46 -0
- package/src/core/host.test.ts +536 -0
- package/src/core/host.ts +297 -0
- package/src/core/http/listeners.test.ts +164 -0
- package/src/core/http/listeners.ts +151 -0
- package/src/core/i18n/agent-panel.ts +96 -0
- package/src/core/i18n/cards.ts +64 -0
- package/src/core/i18n/channels.ts +28 -0
- package/src/core/i18n/conversation.ts +49 -0
- package/src/core/i18n/dashboard.ts +72 -0
- package/src/core/i18n/discord.ts +23 -0
- package/src/core/i18n/en.ts +27 -0
- package/src/core/i18n/i18n.test.ts +139 -0
- package/src/core/i18n/index.ts +58 -0
- package/src/core/i18n/owner.ts +35 -0
- package/src/core/i18n/schedules.ts +92 -0
- package/src/core/i18n/time-zones.ts +27 -0
- package/src/core/i18n/types.ts +7 -0
- package/src/core/i18n/zh-tw.ts +26 -0
- package/src/core/identity.test.ts +18 -0
- package/src/core/identity.ts +31 -0
- package/src/core/judging/confirmation-judge.ts +68 -0
- package/src/core/judging/effort-judge.test.ts +112 -0
- package/src/core/judging/effort-judge.ts +158 -0
- package/src/core/judging/model-judge.test.ts +126 -0
- package/src/core/judging/model-judge.ts +193 -0
- package/src/core/log.ts +45 -0
- package/src/core/models.ts +52 -0
- package/src/core/modules/background/background-turns.ts +78 -0
- package/src/core/modules/delegation/delegate.ts +54 -0
- package/src/core/modules/delegation/delegator.test.ts +170 -0
- package/src/core/modules/delegation/delegator.ts +191 -0
- package/src/core/modules/delegation/sol-worker.ts +78 -0
- package/src/core/modules/discord-admin/discord-admin.ts +288 -0
- package/src/core/modules/host-shell/shell-policy.ts +386 -0
- package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
- package/src/core/modules/memory/owner-memory-store.ts +228 -0
- package/src/core/modules/memory/owner-memory.ts +153 -0
- package/src/core/modules/notify/notify.ts +26 -0
- package/src/core/modules/schedules/recurrence.ts +206 -0
- package/src/core/modules/schedules/schedule-store.ts +213 -0
- package/src/core/modules/schedules/schedule-tools.ts +196 -0
- package/src/core/modules/schedules/schedule.test.ts +358 -0
- package/src/core/modules/schedules/scheduler.ts +121 -0
- package/src/core/modules/schedules/schedules.ts +87 -0
- package/src/core/modules/skills/repo-name.ts +11 -0
- package/src/core/modules/skills/skill-kind.test.ts +55 -0
- package/src/core/modules/skills/skill-link.ts +69 -0
- package/src/core/modules/skills/skill-listing.ts +128 -0
- package/src/core/modules/skills/skill-registry.test.ts +376 -0
- package/src/core/modules/skills/skill-registry.ts +323 -0
- package/src/core/modules/skills/skill-rules.ts +120 -0
- package/src/core/modules/skills/skill-store.ts +285 -0
- package/src/core/modules/skills/skill-tools.ts +207 -0
- package/src/core/ops/error-reporter.test.ts +283 -0
- package/src/core/ops/error-reporter.ts +271 -0
- package/src/core/plugin.ts +200 -0
- package/src/core/presentation/card-cadence.ts +41 -0
- package/src/core/presentation/headline.ts +34 -0
- package/src/core/presentation/presentation.test.ts +166 -0
- package/src/core/presentation/quiet-links.ts +36 -0
- package/src/core/presentation/reply-splitter.ts +110 -0
- package/src/core/presentation/thinking-line.ts +24 -0
- package/src/core/public-entry.test.ts +59 -0
- package/src/core/registry/contributions.test.ts +215 -0
- package/src/core/registry/contributions.ts +249 -0
- package/src/core/registry/interactions.test.ts +87 -0
- package/src/core/registry/interactions.ts +55 -0
- package/src/core/registry/providers.test.ts +103 -0
- package/src/core/registry/providers.ts +55 -0
- package/src/core/routing/channel-queue.test.ts +31 -0
- package/src/core/routing/channel-queue.ts +44 -0
- package/src/core/routing/channel-router.test.ts +326 -0
- package/src/core/routing/channel-router.ts +157 -0
- package/src/core/routing/conversation-kind.ts +14 -0
- package/src/core/routing/forward-join.ts +65 -0
- package/src/core/routing/message-text.ts +16 -0
- package/src/core/routing/settle-turn.test.ts +21 -0
- package/src/core/routing/settle-turn.ts +26 -0
- package/src/core/runtime/compaction-tiers.test.ts +227 -0
- package/src/core/runtime/compaction-tiers.ts +203 -0
- package/src/core/runtime/conversation-sessions.ts +162 -0
- package/src/core/runtime/extensions/agent-prompt.ts +16 -0
- package/src/core/runtime/extensions/ask-user.test.ts +114 -0
- package/src/core/runtime/extensions/ask-user.ts +98 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
- package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
- package/src/core/runtime/mcp.ts +23 -0
- package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
- package/src/core/runtime/pending-confirmation-store.ts +75 -0
- package/src/core/runtime/pi-agent-runtime.ts +477 -0
- package/src/core/runtime/prompt-slot.ts +81 -0
- package/src/core/runtime/runtime-types.ts +150 -0
- package/src/core/runtime/session-archive.test.ts +20 -0
- package/src/core/runtime/session-archive.ts +17 -0
- package/src/core/runtime/session-factory.ts +291 -0
- package/src/core/runtime/steerable-run.test.ts +264 -0
- package/src/core/runtime/steerable-run.ts +109 -0
- package/src/core/runtime/text-tools.test.ts +110 -0
- package/src/core/runtime/text-tools.ts +66 -0
- package/src/core/runtime/turn-answer.test.ts +72 -0
- package/src/core/runtime/turn-answer.ts +79 -0
- package/src/core/runtime/worker-task.test.ts +82 -0
- package/src/core/runtime/worker-task.ts +65 -0
- package/src/core/services.test.ts +28 -0
- package/src/core/services.ts +107 -0
- package/src/core/sessions.test.ts +85 -0
- package/src/core/sessions.ts +149 -0
- package/src/core/shared/attachment-reader.ts +91 -0
- package/src/core/shared/delegate-tool.ts +15 -0
- package/src/core/shared/mcp-adapter.ts +58 -0
- package/src/core/shared/package-dir.ts +9 -0
- package/src/core/shared/profile-tools.ts +16 -0
- package/src/core/shared/read-attachment-tool.ts +43 -0
- package/src/core/shared/schedule-tools.ts +118 -0
- package/src/core/shared/session-messages.ts +50 -0
- package/src/core/shared/tool-result.ts +9 -0
- package/src/core/shared/unix-server.ts +18 -0
- package/src/core/size.test.ts +9 -0
- package/src/core/speakers.test.ts +78 -0
- package/src/core/speakers.ts +143 -0
- package/src/core/testing/database.ts +52 -0
- package/src/core/testing/file-size.ts +34 -0
- package/src/core/testing/locale.ts +8 -0
- package/src/core/testing/modules.ts +97 -0
- package/src/core/testing/owner.ts +15 -0
- package/src/core/testing/thread-host.ts +60 -0
- package/src/core/time.test.ts +166 -0
- package/src/core/time.ts +67 -0
- package/src/core/tool-tiers.test.ts +117 -0
- package/src/core/tool-tiers.ts +127 -0
- package/src/entries.test.ts +112 -0
- package/src/index.ts +28 -0
- package/src/testing.test.ts +156 -0
- package/src/testing.ts +182 -0
- package/templates/.env.example +22 -0
- package/templates/README.md +11 -0
- package/templates/_gitignore +3 -0
- package/templates/agents.ts +12 -0
- package/templates/biome.json.tmpl +7 -0
- package/templates/docker-compose.yml +16 -0
- package/templates/package.json.tmpl +24 -0
- package/templates/persona/shared.md +2 -0
- package/templates/plugin/plugin.test.ts.tmpl +13 -0
- package/templates/plugin/plugin.ts +18 -0
- package/templates/roundtable.config.ts +24 -0
- package/templates/tsconfig.json.tmpl +16 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AttachmentBuilder,
|
|
3
|
+
ChannelType,
|
|
4
|
+
type Client,
|
|
5
|
+
ContainerBuilder,
|
|
6
|
+
discordSort,
|
|
7
|
+
type Guild,
|
|
8
|
+
type Message,
|
|
9
|
+
MessageFlags,
|
|
10
|
+
type NonThreadGuildBasedChannel,
|
|
11
|
+
SeparatorBuilder,
|
|
12
|
+
SeparatorSpacingSize,
|
|
13
|
+
type TextChannel,
|
|
14
|
+
TextDisplayBuilder,
|
|
15
|
+
type Webhook,
|
|
16
|
+
} from "discord.js";
|
|
17
|
+
import type {
|
|
18
|
+
AgentCategory,
|
|
19
|
+
AgentChannels,
|
|
20
|
+
AgentPost,
|
|
21
|
+
CategoryLayout,
|
|
22
|
+
ChannelMessage,
|
|
23
|
+
DashboardBoard,
|
|
24
|
+
} from "../agents/agent-ports.ts";
|
|
25
|
+
import { AgentError } from "../domain/errors.ts";
|
|
26
|
+
import { messages } from "../i18n/index.ts";
|
|
27
|
+
import type { Logger } from "../log.ts";
|
|
28
|
+
|
|
29
|
+
const DASHBOARD_NAME = "dashboard";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The agent server's Discord side: one webhook of the assistant's per channel, through which every
|
|
33
|
+
* agent speaks under its own name and avatar, and new channels under an `Agents` or `Groups`
|
|
34
|
+
* category.
|
|
35
|
+
*/
|
|
36
|
+
export class DiscordAgentChannels implements AgentChannels {
|
|
37
|
+
readonly #client: Client;
|
|
38
|
+
readonly #guildId: string;
|
|
39
|
+
readonly #webhooks = new Map<string, Promise<Webhook>>();
|
|
40
|
+
|
|
41
|
+
constructor(client: Client, guildId: string) {
|
|
42
|
+
this.#client = client;
|
|
43
|
+
this.#guildId = guildId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async post(channelId: string, post: AgentPost): Promise<void> {
|
|
47
|
+
const webhook = await this.#webhook(channelId);
|
|
48
|
+
const as = {
|
|
49
|
+
username: post.name,
|
|
50
|
+
avatarURL: post.avatarUrl,
|
|
51
|
+
allowedMentions: { parse: ["users" as const] },
|
|
52
|
+
...(post.threadId ? { threadId: post.threadId } : {}),
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
if (post.thinking) await webhook.send({ ...as, content: post.thinking });
|
|
56
|
+
for (const chunk of post.chunks)
|
|
57
|
+
await webhook.send({ ...as, content: chunk });
|
|
58
|
+
for (const file of post.files ?? [])
|
|
59
|
+
await webhook.send({
|
|
60
|
+
...as,
|
|
61
|
+
files: [
|
|
62
|
+
new AttachmentBuilder(Buffer.from(file.data), { name: file.name }),
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// A webhook deleted by hand is found or made again on the next post.
|
|
67
|
+
this.#webhooks.delete(channelId);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async createChannel(
|
|
73
|
+
name: string,
|
|
74
|
+
topic: string,
|
|
75
|
+
category: AgentCategory,
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
const guild = await this.#client.guilds.fetch(this.#guildId);
|
|
78
|
+
const parent = await this.#category(guild, category);
|
|
79
|
+
const channel = await guild.channels.create({
|
|
80
|
+
name,
|
|
81
|
+
type: ChannelType.GuildText,
|
|
82
|
+
parent: parent.id,
|
|
83
|
+
topic: topic.slice(0, 1024),
|
|
84
|
+
});
|
|
85
|
+
return channel.id;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async placeIn(channelId: string, category: AgentCategory): Promise<boolean> {
|
|
89
|
+
const text = await this.#text(channelId);
|
|
90
|
+
const parent = await this.#category(text.guild, category);
|
|
91
|
+
if (text.parentId === parent.id) return false;
|
|
92
|
+
await text.setParent(parent.id, { lockPermissions: false });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async layout(): Promise<CategoryLayout[]> {
|
|
97
|
+
const guild = await this.#client.guilds.fetch(this.#guildId);
|
|
98
|
+
const channels = (await guild.channels.fetch()).filter(
|
|
99
|
+
(c): c is NonThreadGuildBasedChannel => c !== null,
|
|
100
|
+
);
|
|
101
|
+
const categories = channels.filter(
|
|
102
|
+
(c) => c.type === ChannelType.GuildCategory,
|
|
103
|
+
);
|
|
104
|
+
return [...discordSort(categories).values()].map((category) => ({
|
|
105
|
+
id: category.id,
|
|
106
|
+
name: category.name,
|
|
107
|
+
channelIds: [
|
|
108
|
+
...discordSort(
|
|
109
|
+
channels.filter((c) => c.parentId === category.id),
|
|
110
|
+
).keys(),
|
|
111
|
+
],
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async arrange(layout: CategoryLayout[], remove: string[]): Promise<void> {
|
|
116
|
+
const guild = await this.#client.guilds.fetch(this.#guildId);
|
|
117
|
+
const current = await guild.channels.fetch();
|
|
118
|
+
const positions: { channel: string; position: number }[] = [];
|
|
119
|
+
for (const [index, category] of layout.entries()) {
|
|
120
|
+
const id =
|
|
121
|
+
category.id ??
|
|
122
|
+
(
|
|
123
|
+
await guild.channels.create({
|
|
124
|
+
name: category.name,
|
|
125
|
+
type: ChannelType.GuildCategory,
|
|
126
|
+
})
|
|
127
|
+
).id;
|
|
128
|
+
positions.push({ channel: id, position: index });
|
|
129
|
+
for (const [position, channelId] of category.channelIds.entries()) {
|
|
130
|
+
// Discord's bulk position edit takes at most one parent change, so moves go one by one.
|
|
131
|
+
const channel = current.get(channelId);
|
|
132
|
+
if (channel && channel.parentId !== id)
|
|
133
|
+
await channel.setParent(id, { lockPermissions: false });
|
|
134
|
+
positions.push({ channel: channelId, position });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await guild.channels.setPositions(positions);
|
|
138
|
+
if (remove.length === 0) return;
|
|
139
|
+
// Fetch the parents again before judging a category empty.
|
|
140
|
+
const channels = await guild.channels.fetch();
|
|
141
|
+
for (const id of remove) {
|
|
142
|
+
const category = channels.get(id);
|
|
143
|
+
if (
|
|
144
|
+
category?.type === ChannelType.GuildCategory &&
|
|
145
|
+
!channels.some((c) => c?.parentId === id)
|
|
146
|
+
)
|
|
147
|
+
await category.delete(messages().auditArranged);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async setTopic(channelId: string, topic: string): Promise<void> {
|
|
152
|
+
const text = await this.#text(channelId);
|
|
153
|
+
// Discord allows two topic edits per channel in ten minutes; skip the ones that change nothing.
|
|
154
|
+
if (text.topic !== topic.slice(0, 1024))
|
|
155
|
+
await text.setTopic(topic.slice(0, 1024));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async removeWebhook(channelId: string): Promise<void> {
|
|
159
|
+
this.#webhooks.delete(channelId);
|
|
160
|
+
const webhook = await this.#find(await this.#text(channelId));
|
|
161
|
+
await webhook?.delete(messages().auditArchived);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async exists(channelId: string): Promise<boolean> {
|
|
165
|
+
const channel = await this.#client.channels
|
|
166
|
+
.fetch(channelId)
|
|
167
|
+
.catch(() => null);
|
|
168
|
+
return channel !== null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async read(
|
|
172
|
+
channelId: string,
|
|
173
|
+
options: { limit: number; around?: string },
|
|
174
|
+
): Promise<ChannelMessage[]> {
|
|
175
|
+
const channel = await this.#client.channels
|
|
176
|
+
.fetch(channelId)
|
|
177
|
+
.catch(() => null);
|
|
178
|
+
if (
|
|
179
|
+
!channel ||
|
|
180
|
+
channel.isDMBased() ||
|
|
181
|
+
channel.guildId !== this.#guildId ||
|
|
182
|
+
!channel.isTextBased()
|
|
183
|
+
)
|
|
184
|
+
throw new AgentError(
|
|
185
|
+
`<#${channelId}> is not a text channel or thread of the agent server.`,
|
|
186
|
+
);
|
|
187
|
+
const fetched = await channel.messages.fetch(
|
|
188
|
+
options.around
|
|
189
|
+
? { limit: options.limit, around: options.around }
|
|
190
|
+
: { limit: options.limit },
|
|
191
|
+
);
|
|
192
|
+
return [...fetched.values()]
|
|
193
|
+
.sort((a, b) => a.createdTimestamp - b.createdTimestamp)
|
|
194
|
+
.map(channelMessage);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The category, made when missing. */
|
|
198
|
+
async #category(guild: Guild, name: AgentCategory) {
|
|
199
|
+
const channels = await guild.channels.fetch();
|
|
200
|
+
return (
|
|
201
|
+
channels.find(
|
|
202
|
+
(c) => c?.type === ChannelType.GuildCategory && c.name === name,
|
|
203
|
+
) ??
|
|
204
|
+
(await guild.channels.create({ name, type: ChannelType.GuildCategory }))
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async #text(channelId: string): Promise<TextChannel> {
|
|
209
|
+
const channel = await this.#client.channels.fetch(channelId);
|
|
210
|
+
if (channel?.type !== ChannelType.GuildText)
|
|
211
|
+
throw new AgentError(`channel ${channelId} is not a server text channel`);
|
|
212
|
+
return channel as TextChannel;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#webhook(channelId: string): Promise<Webhook> {
|
|
216
|
+
let pending = this.#webhooks.get(channelId);
|
|
217
|
+
if (!pending) {
|
|
218
|
+
pending = this.#findOrCreate(channelId);
|
|
219
|
+
pending.catch(() => this.#webhooks.delete(channelId));
|
|
220
|
+
this.#webhooks.set(channelId, pending);
|
|
221
|
+
}
|
|
222
|
+
return pending;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async #findOrCreate(channelId: string): Promise<Webhook> {
|
|
226
|
+
const text = await this.#text(channelId);
|
|
227
|
+
return (
|
|
228
|
+
(await this.#find(text)) ??
|
|
229
|
+
(await text.createWebhook({
|
|
230
|
+
name: messages().webhookName,
|
|
231
|
+
reason: messages().auditWebhook,
|
|
232
|
+
}))
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async #find(text: TextChannel): Promise<Webhook | undefined> {
|
|
237
|
+
const applicationId = this.#client.application?.id;
|
|
238
|
+
return (await text.fetchWebhooks()).find(
|
|
239
|
+
(w) =>
|
|
240
|
+
w.name === messages().webhookName && w.applicationId === applicationId,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function channelMessage(message: Message): ChannelMessage {
|
|
246
|
+
const forwarded = message.messageSnapshots.first()?.content;
|
|
247
|
+
return {
|
|
248
|
+
id: message.id,
|
|
249
|
+
author:
|
|
250
|
+
message.member?.displayName ??
|
|
251
|
+
message.author.globalName ??
|
|
252
|
+
message.author.username,
|
|
253
|
+
at: message.createdAt,
|
|
254
|
+
text: [message.content, forwarded ? `(forwarded) ${forwarded}` : ""]
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.join("\n"),
|
|
257
|
+
attachments: [...message.attachments.values()].map((a) => a.name),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `#dashboard` at the top of the agent server and the assistant's one message in it, found again after a
|
|
263
|
+
* restart and made again when either is deleted. The message is pinned when the assistant has the Pin
|
|
264
|
+
* Messages permission; it is found by its author either way.
|
|
265
|
+
*/
|
|
266
|
+
export class DiscordDashboard implements DashboardBoard {
|
|
267
|
+
readonly #client: Client;
|
|
268
|
+
readonly #guildId: string;
|
|
269
|
+
readonly #logger: Logger;
|
|
270
|
+
#message: Message | undefined;
|
|
271
|
+
/** Whether pinning has been tried since startup, so a missing permission is reported once. */
|
|
272
|
+
#pinTried = false;
|
|
273
|
+
|
|
274
|
+
constructor(client: Client, guildId: string, logger: Logger) {
|
|
275
|
+
this.#client = client;
|
|
276
|
+
this.#guildId = guildId;
|
|
277
|
+
this.#logger = logger;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async show(sections: string[]): Promise<void> {
|
|
281
|
+
const container = new ContainerBuilder();
|
|
282
|
+
sections.forEach((section, index) => {
|
|
283
|
+
if (index > 0)
|
|
284
|
+
container.addSeparatorComponents(
|
|
285
|
+
new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small),
|
|
286
|
+
);
|
|
287
|
+
container.addTextDisplayComponents(
|
|
288
|
+
new TextDisplayBuilder().setContent(section),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
const body = {
|
|
292
|
+
components: [container],
|
|
293
|
+
allowedMentions: { parse: [] },
|
|
294
|
+
};
|
|
295
|
+
const message = this.#message ?? (await this.#find());
|
|
296
|
+
if (message) {
|
|
297
|
+
try {
|
|
298
|
+
this.#message = await message.edit(body);
|
|
299
|
+
await this.#pin(this.#message);
|
|
300
|
+
return;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
this.#message = undefined;
|
|
303
|
+
// A message deleted by hand is made again; anything else is the caller's to log.
|
|
304
|
+
if (!(await this.#gone(message))) throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const channel = await this.#channel();
|
|
308
|
+
const posted = await channel.send({
|
|
309
|
+
...body,
|
|
310
|
+
flags: MessageFlags.IsComponentsV2,
|
|
311
|
+
});
|
|
312
|
+
this.#message = posted;
|
|
313
|
+
await this.#pin(posted);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Pins the dashboard message once per startup, when it is not pinned yet. */
|
|
317
|
+
async #pin(message: Message): Promise<void> {
|
|
318
|
+
if (message.pinned || this.#pinTried) return;
|
|
319
|
+
this.#pinTried = true;
|
|
320
|
+
await message
|
|
321
|
+
.pin()
|
|
322
|
+
.catch((error: unknown) =>
|
|
323
|
+
this.#logger.warn(
|
|
324
|
+
{ err: error },
|
|
325
|
+
"dashboard message not pinned; grant the assistant Pin Messages in the agent server",
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The assistant's latest message in the dashboard channel, if both exist. */
|
|
331
|
+
async #find(): Promise<Message | undefined> {
|
|
332
|
+
const channel = await this.#channel();
|
|
333
|
+
const botId = this.#client.user?.id;
|
|
334
|
+
const recent = await channel.messages.fetch({ limit: 50 });
|
|
335
|
+
return recent.find((message) => message.author.id === botId);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async #gone(message: Message): Promise<boolean> {
|
|
339
|
+
return message.channel.messages
|
|
340
|
+
.fetch(message.id)
|
|
341
|
+
.then(() => false)
|
|
342
|
+
.catch(() => true);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async #channel(): Promise<TextChannel> {
|
|
346
|
+
const guild = await this.#client.guilds.fetch(this.#guildId);
|
|
347
|
+
const channels = await guild.channels.fetch();
|
|
348
|
+
const found = channels.find(
|
|
349
|
+
(c) =>
|
|
350
|
+
c?.type === ChannelType.GuildText &&
|
|
351
|
+
c.name === DASHBOARD_NAME &&
|
|
352
|
+
c.parentId === null,
|
|
353
|
+
);
|
|
354
|
+
if (found) {
|
|
355
|
+
const text = found as TextChannel;
|
|
356
|
+
const topic = messages().dashTopic;
|
|
357
|
+
if (text.topic !== topic) await text.setTopic(topic);
|
|
358
|
+
return text;
|
|
359
|
+
}
|
|
360
|
+
return guild.channels.create({
|
|
361
|
+
name: DASHBOARD_NAME,
|
|
362
|
+
type: ChannelType.GuildText,
|
|
363
|
+
position: 0,
|
|
364
|
+
topic: messages().dashTopic,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|