@geminixiang/mikan 0.2.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 +324 -0
- package/LICENSE +22 -0
- package/README.md +297 -0
- package/dist/adapter.d.ts +134 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapters/discord/bot.d.ts +63 -0
- package/dist/adapters/discord/bot.d.ts.map +1 -0
- package/dist/adapters/discord/bot.js +577 -0
- package/dist/adapters/discord/bot.js.map +1 -0
- package/dist/adapters/discord/context.d.ts +9 -0
- package/dist/adapters/discord/context.d.ts.map +1 -0
- package/dist/adapters/discord/context.js +245 -0
- package/dist/adapters/discord/context.js.map +1 -0
- package/dist/adapters/discord/index.d.ts +3 -0
- package/dist/adapters/discord/index.d.ts.map +1 -0
- package/dist/adapters/discord/index.js +3 -0
- package/dist/adapters/discord/index.js.map +1 -0
- package/dist/adapters/shared.d.ts +91 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +191 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +139 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -0
- package/dist/adapters/slack/bot.js +1272 -0
- package/dist/adapters/slack/bot.js.map +1 -0
- package/dist/adapters/slack/branch-manager.d.ts +28 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +117 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts +12 -0
- package/dist/adapters/slack/context.d.ts.map +1 -0
- package/dist/adapters/slack/context.js +327 -0
- package/dist/adapters/slack/context.js.map +1 -0
- package/dist/adapters/slack/index.d.ts +3 -0
- package/dist/adapters/slack/index.d.ts.map +1 -0
- package/dist/adapters/slack/index.js +3 -0
- package/dist/adapters/slack/index.js.map +1 -0
- package/dist/adapters/slack/session.d.ts +38 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +66 -0
- package/dist/adapters/slack/session.js.map +1 -0
- package/dist/adapters/slack/tools/attach.d.ts +12 -0
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
- package/dist/adapters/slack/tools/attach.js +40 -0
- package/dist/adapters/slack/tools/attach.js.map +1 -0
- package/dist/adapters/telegram/bot.d.ts +51 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -0
- package/dist/adapters/telegram/bot.js +430 -0
- package/dist/adapters/telegram/bot.js.map +1 -0
- package/dist/adapters/telegram/context.d.ts +9 -0
- package/dist/adapters/telegram/context.d.ts.map +1 -0
- package/dist/adapters/telegram/context.js +190 -0
- package/dist/adapters/telegram/context.js.map +1 -0
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/adapters/telegram/index.d.ts +3 -0
- package/dist/adapters/telegram/index.d.ts.map +1 -0
- package/dist/adapters/telegram/index.js +3 -0
- package/dist/adapters/telegram/index.js.map +1 -0
- package/dist/agent.d.ts +36 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +1147 -0
- package/dist/agent.js.map +1 -0
- package/dist/commands/auto-reply.d.ts +5 -0
- package/dist/commands/auto-reply.d.ts.map +1 -0
- package/dist/commands/auto-reply.js +79 -0
- package/dist/commands/auto-reply.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +18 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +91 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/model.d.ts +14 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +110 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/new.d.ts +5 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +24 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/parse.d.ts +7 -0
- package/dist/commands/parse.d.ts.map +1 -0
- package/dist/commands/parse.js +17 -0
- package/dist/commands/parse.js.map +1 -0
- package/dist/commands/registry.d.ts +4 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +9 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/sandbox.d.ts +10 -0
- package/dist/commands/sandbox.d.ts.map +1 -0
- package/dist/commands/sandbox.js +83 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/session-view.d.ts +5 -0
- package/dist/commands/session-view.d.ts.map +1 -0
- package/dist/commands/session-view.js +62 -0
- package/dist/commands/session-view.js.map +1 -0
- package/dist/commands/types.d.ts +41 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +14 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +370 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +24 -0
- package/dist/context.js.map +1 -0
- package/dist/conversation-history.d.ts +16 -0
- package/dist/conversation-history.d.ts.map +1 -0
- package/dist/conversation-history.js +144 -0
- package/dist/conversation-history.js.map +1 -0
- package/dist/download.d.ts +2 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +89 -0
- package/dist/download.js.map +1 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +12 -0
- package/dist/env.js.map +1 -0
- package/dist/events.d.ts +85 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +483 -0
- package/dist/events.js.map +1 -0
- package/dist/execution-resolver.d.ts +25 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +167 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/file-guards.d.ts +9 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +56 -0
- package/dist/file-guards.js.map +1 -0
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts +2 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +10 -0
- package/dist/instrument.js.map +1 -0
- package/dist/log.d.ts +36 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +206 -0
- package/dist/log.js.map +1 -0
- package/dist/login/index.d.ts +42 -0
- package/dist/login/index.d.ts.map +1 -0
- package/dist/login/index.js +239 -0
- package/dist/login/index.js.map +1 -0
- package/dist/login/portal.d.ts +19 -0
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1544 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/login/session.d.ts +26 -0
- package/dist/login/session.d.ts.map +1 -0
- package/dist/login/session.js +56 -0
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +366 -0
- package/dist/main.js.map +1 -0
- package/dist/provisioner.d.ts +83 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +500 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/runtime/conversation-orchestrator.d.ts +40 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +183 -0
- package/dist/runtime/conversation-orchestrator.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +26 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +221 -0
- package/dist/runtime/session-runtime.js.map +1 -0
- package/dist/sandbox/cloudflare.d.ts +15 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -0
- package/dist/sandbox/cloudflare.js +138 -0
- package/dist/sandbox/cloudflare.js.map +1 -0
- package/dist/sandbox/container.d.ts +16 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +138 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +17 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +212 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +11 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +89 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +22 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +54 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/path-context.d.ts +4 -0
- package/dist/sandbox/path-context.d.ts.map +1 -0
- package/dist/sandbox/path-context.js +20 -0
- package/dist/sandbox/path-context.js.map +1 -0
- package/dist/sandbox/types.d.ts +67 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sentry.d.ts +50 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +257 -0
- package/dist/sentry.js.map +1 -0
- package/dist/session-view/command.d.ts +5 -0
- package/dist/session-view/command.d.ts.map +1 -0
- package/dist/session-view/command.js +7 -0
- package/dist/session-view/command.js.map +1 -0
- package/dist/session-view/portal.d.ts +16 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +1822 -0
- package/dist/session-view/portal.js.map +1 -0
- package/dist/session-view/service.d.ts +34 -0
- package/dist/session-view/service.d.ts.map +1 -0
- package/dist/session-view/service.js +434 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +18 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +36 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/sessions/metadata.d.ts +15 -0
- package/dist/sessions/metadata.d.ts.map +1 -0
- package/dist/sessions/metadata.js +11 -0
- package/dist/sessions/metadata.js.map +1 -0
- package/dist/sessions/policy.d.ts +13 -0
- package/dist/sessions/policy.d.ts.map +1 -0
- package/dist/sessions/policy.js +23 -0
- package/dist/sessions/policy.js.map +1 -0
- package/dist/sessions/store.d.ts +103 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +349 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/store.d.ts +58 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +152 -0
- package/dist/store.js.map +1 -0
- package/dist/tool-diagnostics.d.ts +2 -0
- package/dist/tool-diagnostics.d.ts.map +1 -0
- package/dist/tool-diagnostics.js +7 -0
- package/dist/tool-diagnostics.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +80 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +11 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +133 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/event.d.ts +62 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +138 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +136 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/truncate.d.ts +57 -0
- package/dist/tools/truncate.d.ts.map +1 -0
- package/dist/tools/truncate.js +184 -0
- package/dist/tools/truncate.js.map +1 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +33 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/trigger.d.ts +31 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +98 -0
- package/dist/trigger.js.map +1 -0
- package/dist/ui-copy.d.ts +12 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +36 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +4 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +16 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +72 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +281 -0
- package/dist/vault.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { ApplicationCommandOptionType, Client, Events, GatewayIntentBits, Partials, } from "discord.js";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { basename, join } from "path";
|
|
4
|
+
import * as log from "../../log.js";
|
|
5
|
+
import { resolveChatSessionKey } from "../../sessions/policy.js";
|
|
6
|
+
import { evaluateAutoReplyPolicy } from "../../trigger.js";
|
|
7
|
+
import { formatNothingRunning } from "../../ui-copy.js";
|
|
8
|
+
import { appendBotResponseLog, appendChannelLog, ChannelQueue, resolveOnlyScopedStopTarget, resolveStopTarget, withRetry, } from "../shared.js";
|
|
9
|
+
import { createDiscordAdapters } from "./context.js";
|
|
10
|
+
// discord.js: DiscordAPIError exposes `.status` (HTTP status) and a `.code`.
|
|
11
|
+
// RateLimitError fires when the internal queue gives up. Both should retry.
|
|
12
|
+
function discordIsRateLimited(err) {
|
|
13
|
+
if (err.status === 429)
|
|
14
|
+
return true;
|
|
15
|
+
if (err.httpStatus === 429)
|
|
16
|
+
return true;
|
|
17
|
+
if (err.name === "RateLimitError")
|
|
18
|
+
return true;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const discordRetry = (fn) => withRetry(fn, { isRateLimited: discordIsRateLimited });
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// DiscordBot
|
|
24
|
+
// ============================================================================
|
|
25
|
+
export class DiscordBot {
|
|
26
|
+
constructor(handler, config) {
|
|
27
|
+
this.botUserId = null;
|
|
28
|
+
this.queues = new Map();
|
|
29
|
+
this.startupTime = 0;
|
|
30
|
+
this.channels = new Map();
|
|
31
|
+
this.users = new Map();
|
|
32
|
+
this.handler = handler;
|
|
33
|
+
this.token = config.token;
|
|
34
|
+
this.workingDir = config.workingDir;
|
|
35
|
+
this.client = new Client({
|
|
36
|
+
intents: [
|
|
37
|
+
GatewayIntentBits.Guilds,
|
|
38
|
+
GatewayIntentBits.GuildMessages,
|
|
39
|
+
GatewayIntentBits.MessageContent,
|
|
40
|
+
GatewayIntentBits.DirectMessages,
|
|
41
|
+
],
|
|
42
|
+
partials: [Partials.Channel, Partials.Message],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// ==========================================================================
|
|
46
|
+
// Public API (implements Bot)
|
|
47
|
+
// ==========================================================================
|
|
48
|
+
async start() {
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
this.client.once(Events.ClientReady, async (readyClient) => {
|
|
51
|
+
this.botUserId = readyClient.user.id;
|
|
52
|
+
this.startupTime = Date.now();
|
|
53
|
+
log.logConnected("Discord");
|
|
54
|
+
log.logInfo(`Discord bot started as ${readyClient.user.tag}`);
|
|
55
|
+
this.loadCachedGuildData();
|
|
56
|
+
this.setupEventHandlers();
|
|
57
|
+
try {
|
|
58
|
+
await readyClient.application.commands.set([
|
|
59
|
+
{
|
|
60
|
+
name: "login",
|
|
61
|
+
description: "Store credentials in your private vault",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "session",
|
|
65
|
+
description: "Open the current session in the web viewer",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "new",
|
|
69
|
+
description: "Reset conversation history and start fresh",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "stop",
|
|
73
|
+
description: "Stop the current conversation",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "model",
|
|
77
|
+
description: "Switch this conversation's LLM model",
|
|
78
|
+
options: [
|
|
79
|
+
{
|
|
80
|
+
name: "model",
|
|
81
|
+
description: "provider/model[:thinking], e.g. anthropic/claude-sonnet-4-6:off",
|
|
82
|
+
type: ApplicationCommandOptionType.String,
|
|
83
|
+
required: false,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "sandbox",
|
|
89
|
+
description: "Show or temporarily boost this conversation's sandbox limits",
|
|
90
|
+
options: [
|
|
91
|
+
{
|
|
92
|
+
name: "action",
|
|
93
|
+
description: "Use 'boost' to temporarily apply the configured boost limits",
|
|
94
|
+
type: ApplicationCommandOptionType.String,
|
|
95
|
+
required: false,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
log.logWarning("Failed to register Discord slash commands", err instanceof Error ? err.message : String(err));
|
|
103
|
+
}
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
this.client.once(Events.Error, reject);
|
|
107
|
+
this.client.login(this.token).catch(reject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async postMessage(channel, text) {
|
|
111
|
+
return discordRetry(async () => {
|
|
112
|
+
const ch = await this.fetchTextChannel(channel);
|
|
113
|
+
const msg = await ch.send(text);
|
|
114
|
+
return msg.id;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async updateMessage(channel, ts, text) {
|
|
118
|
+
await this.updateMessageRaw(channel, ts, text);
|
|
119
|
+
}
|
|
120
|
+
enqueueEvent(event) {
|
|
121
|
+
const conversationId = event.conversationId;
|
|
122
|
+
const queue = this.getQueue(conversationId);
|
|
123
|
+
if (queue.size() >= 5) {
|
|
124
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
128
|
+
queue.enqueue(() => {
|
|
129
|
+
const adapters = createDiscordAdapters(event, this);
|
|
130
|
+
return this.handler.handleEvent(event, this, adapters);
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
getPlatformInfo() {
|
|
135
|
+
return {
|
|
136
|
+
name: "discord",
|
|
137
|
+
formattingGuide: "## Discord Formatting (Markdown)\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\ncode```\nLinks: [text](url)",
|
|
138
|
+
channels: this.getAllChannels(),
|
|
139
|
+
users: this.getAllUsers(),
|
|
140
|
+
diagnostics: {
|
|
141
|
+
showUsageSummary: false,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ==========================================================================
|
|
146
|
+
// Internal helpers (used by context.ts)
|
|
147
|
+
// ==========================================================================
|
|
148
|
+
async updateMessageRaw(channelId, messageId, text) {
|
|
149
|
+
return discordRetry(async () => {
|
|
150
|
+
const ch = await this.fetchTextChannel(channelId);
|
|
151
|
+
const msg = await ch.messages.fetch(messageId);
|
|
152
|
+
await msg.edit(text);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async postReply(channelId, replyToId, text) {
|
|
156
|
+
return discordRetry(async () => {
|
|
157
|
+
const ch = await this.fetchTextChannel(channelId);
|
|
158
|
+
const replyTarget = await ch.messages.fetch(replyToId);
|
|
159
|
+
const sent = await replyTarget.reply(text);
|
|
160
|
+
return sent.id;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async postInThread(channelId, threadOrMessageId, text) {
|
|
164
|
+
// Try as a thread channel first, then fall back to posting in the channel
|
|
165
|
+
try {
|
|
166
|
+
const thread = await this.client.channels.fetch(threadOrMessageId);
|
|
167
|
+
if (thread && (thread.isThread() || thread.isTextBased())) {
|
|
168
|
+
return discordRetry(async () => {
|
|
169
|
+
const msg = await thread.send(text);
|
|
170
|
+
return msg.id;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Not a thread channel, treat as message ID for reply
|
|
176
|
+
}
|
|
177
|
+
return this.postReply(channelId, threadOrMessageId, text);
|
|
178
|
+
}
|
|
179
|
+
async deleteMessageRaw(channelId, messageId) {
|
|
180
|
+
try {
|
|
181
|
+
const ch = await this.fetchTextChannel(channelId);
|
|
182
|
+
const msg = await ch.messages.fetch(messageId);
|
|
183
|
+
await msg.delete();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Ignore if already deleted
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async sendTyping(channelId) {
|
|
190
|
+
try {
|
|
191
|
+
const ch = await this.fetchTextChannel(channelId);
|
|
192
|
+
await ch.sendTyping();
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Non-fatal
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async uploadFile(channelId, filePath, title) {
|
|
199
|
+
return discordRetry(async () => {
|
|
200
|
+
const ch = await this.fetchTextChannel(channelId);
|
|
201
|
+
const fileName = title ?? basename(filePath);
|
|
202
|
+
const fileContent = readFileSync(filePath);
|
|
203
|
+
await ch.send({ files: [{ attachment: fileContent, name: fileName }] });
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async sendDirectMessage(userId, text) {
|
|
207
|
+
return discordRetry(async () => {
|
|
208
|
+
const user = await this.client.users.fetch(userId);
|
|
209
|
+
const msg = await user.send(text);
|
|
210
|
+
return msg.id;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async postPrivate(_conversationId, userId, text) {
|
|
214
|
+
await this.sendDirectMessage(userId, text);
|
|
215
|
+
}
|
|
216
|
+
getAllChannels() {
|
|
217
|
+
return Array.from(this.channels.values());
|
|
218
|
+
}
|
|
219
|
+
getAllUsers() {
|
|
220
|
+
return Array.from(this.users.values());
|
|
221
|
+
}
|
|
222
|
+
logToFile(channelId, entry) {
|
|
223
|
+
appendChannelLog(this.workingDir, channelId, entry);
|
|
224
|
+
}
|
|
225
|
+
logBotResponse(channelId, text, ts) {
|
|
226
|
+
appendBotResponseLog(this.workingDir, channelId, text, ts);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Process attachments from a Discord message.
|
|
230
|
+
* Downloads files before returning so the agent can read them immediately.
|
|
231
|
+
*/
|
|
232
|
+
async processAttachments(channelId, attachments, _messageId) {
|
|
233
|
+
const downloads = [];
|
|
234
|
+
// Discord attachments Collection - iterate over values
|
|
235
|
+
for (const attachment of attachments.values()) {
|
|
236
|
+
if (!attachment.name) {
|
|
237
|
+
log.logWarning("Discord attachment missing name, skipping", attachment.url);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const ts = Date.now();
|
|
241
|
+
const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
242
|
+
const filename = `${ts}_${sanitizedName}`;
|
|
243
|
+
const localPath = `${channelId}/attachments/${filename}`;
|
|
244
|
+
const fullDir = join(this.workingDir, channelId, "attachments");
|
|
245
|
+
const result = {
|
|
246
|
+
name: attachment.name,
|
|
247
|
+
localPath,
|
|
248
|
+
};
|
|
249
|
+
downloads.push(this.downloadAttachment(fullDir, filename, attachment.url)
|
|
250
|
+
.then(() => result)
|
|
251
|
+
.catch((err) => {
|
|
252
|
+
log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);
|
|
253
|
+
return null;
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
const results = await Promise.all(downloads);
|
|
257
|
+
return results.filter((attachment) => attachment !== null);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Download an attachment from URL to local file
|
|
261
|
+
*/
|
|
262
|
+
async downloadAttachment(dir, filename, url) {
|
|
263
|
+
if (!existsSync(dir))
|
|
264
|
+
mkdirSync(dir, { recursive: true });
|
|
265
|
+
try {
|
|
266
|
+
const response = await fetch(url);
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
269
|
+
}
|
|
270
|
+
const buffer = await response.arrayBuffer();
|
|
271
|
+
writeFileSync(join(dir, filename), Buffer.from(buffer));
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
275
|
+
cause: err,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// ==========================================================================
|
|
280
|
+
// Private - Event Handlers
|
|
281
|
+
// ==========================================================================
|
|
282
|
+
getQueue(channelId) {
|
|
283
|
+
let queue = this.queues.get(channelId);
|
|
284
|
+
if (!queue) {
|
|
285
|
+
queue = new ChannelQueue("Discord");
|
|
286
|
+
this.queues.set(channelId, queue);
|
|
287
|
+
}
|
|
288
|
+
return queue;
|
|
289
|
+
}
|
|
290
|
+
resolveStopTarget(channelId, sessionKey) {
|
|
291
|
+
const directTarget = resolveStopTarget({
|
|
292
|
+
handler: this.handler,
|
|
293
|
+
conversationId: channelId,
|
|
294
|
+
sessionKey,
|
|
295
|
+
});
|
|
296
|
+
if (directTarget)
|
|
297
|
+
return directTarget;
|
|
298
|
+
if (sessionKey !== channelId)
|
|
299
|
+
return null;
|
|
300
|
+
return resolveOnlyScopedStopTarget(this.handler, channelId);
|
|
301
|
+
}
|
|
302
|
+
loadCachedGuildData() {
|
|
303
|
+
for (const guild of this.client.guilds.cache.values()) {
|
|
304
|
+
for (const channel of guild.channels.cache.values()) {
|
|
305
|
+
if (channel.isTextBased() && "name" in channel) {
|
|
306
|
+
this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const member of guild.members.cache.values()) {
|
|
310
|
+
this.users.set(member.id, {
|
|
311
|
+
id: member.id,
|
|
312
|
+
userName: member.user.username,
|
|
313
|
+
displayName: member.displayName,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
stripBotMention(text) {
|
|
319
|
+
if (!this.botUserId)
|
|
320
|
+
return text;
|
|
321
|
+
return text.replace(new RegExp(`<@!?${this.botUserId}>`, "g"), "").trim();
|
|
322
|
+
}
|
|
323
|
+
resolveConversationContext(input) {
|
|
324
|
+
if (!input.inGuild) {
|
|
325
|
+
return {
|
|
326
|
+
conversationId: input.channelId,
|
|
327
|
+
threadTs: input.referencedMsgId,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (input.isThread) {
|
|
331
|
+
return {
|
|
332
|
+
conversationId: input.parentChannelId ?? input.channelId,
|
|
333
|
+
threadTs: input.channelId,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
conversationId: input.channelId,
|
|
338
|
+
threadTs: input.referencedMsgId,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
createSlashCommandAdapters(interaction, commandText, sessionKey, conversationId) {
|
|
342
|
+
const isDM = !interaction.inGuild();
|
|
343
|
+
const userId = interaction.user.id;
|
|
344
|
+
const userName = interaction.user.username;
|
|
345
|
+
const platform = this.getPlatformInfo();
|
|
346
|
+
const shouldUseEphemeral = !isDM;
|
|
347
|
+
const message = {
|
|
348
|
+
id: interaction.id,
|
|
349
|
+
sessionKey,
|
|
350
|
+
conversationKind: isDM ? "direct" : "shared",
|
|
351
|
+
userId,
|
|
352
|
+
userName,
|
|
353
|
+
text: commandText,
|
|
354
|
+
attachments: [],
|
|
355
|
+
};
|
|
356
|
+
const respondPrivately = async (text, replace = false) => {
|
|
357
|
+
if (interaction.replied || interaction.deferred) {
|
|
358
|
+
if (replace) {
|
|
359
|
+
await interaction.editReply({ content: text });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
await interaction.followUp({ content: text, ephemeral: shouldUseEphemeral });
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await interaction.reply({ content: text, ephemeral: shouldUseEphemeral });
|
|
367
|
+
};
|
|
368
|
+
const responseCtx = {
|
|
369
|
+
respond: async (text) => {
|
|
370
|
+
await respondPrivately(text);
|
|
371
|
+
},
|
|
372
|
+
replaceResponse: async (text) => {
|
|
373
|
+
await respondPrivately(text, true);
|
|
374
|
+
},
|
|
375
|
+
respondDiagnostic: async (text) => {
|
|
376
|
+
await respondPrivately(text);
|
|
377
|
+
},
|
|
378
|
+
respondToolResult: async (result) => {
|
|
379
|
+
const duration = (result.durationMs / 1000).toFixed(1);
|
|
380
|
+
const formatted = `${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`;
|
|
381
|
+
await respondPrivately(formatted);
|
|
382
|
+
},
|
|
383
|
+
setTyping: async () => { },
|
|
384
|
+
setWorking: async () => { },
|
|
385
|
+
uploadFile: async (filePath, title) => {
|
|
386
|
+
await this.uploadFile(conversationId, filePath, title);
|
|
387
|
+
},
|
|
388
|
+
deleteResponse: async () => { },
|
|
389
|
+
};
|
|
390
|
+
return { message, responseCtx, platform };
|
|
391
|
+
}
|
|
392
|
+
setupEventHandlers() {
|
|
393
|
+
this.client.on(Events.InteractionCreate, async (interaction) => {
|
|
394
|
+
if (!interaction.isChatInputCommand())
|
|
395
|
+
return;
|
|
396
|
+
if (interaction.commandName !== "login" &&
|
|
397
|
+
interaction.commandName !== "session" &&
|
|
398
|
+
interaction.commandName !== "new" &&
|
|
399
|
+
interaction.commandName !== "stop" &&
|
|
400
|
+
interaction.commandName !== "model" &&
|
|
401
|
+
interaction.commandName !== "sandbox") {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const isDM = !interaction.inGuild();
|
|
405
|
+
const { conversationId, threadTs } = this.resolveConversationContext({
|
|
406
|
+
channelId: interaction.channelId,
|
|
407
|
+
inGuild: interaction.inGuild(),
|
|
408
|
+
isThread: interaction.channel?.isThread() ?? false,
|
|
409
|
+
parentChannelId: interaction.channel && "parentId" in interaction.channel
|
|
410
|
+
? interaction.channel.parentId
|
|
411
|
+
: null,
|
|
412
|
+
});
|
|
413
|
+
const sessionKey = resolveChatSessionKey({
|
|
414
|
+
conversationId,
|
|
415
|
+
conversationKind: isDM ? "direct" : "shared",
|
|
416
|
+
messageId: interaction.id,
|
|
417
|
+
persistentTopLevel: true,
|
|
418
|
+
threadTs,
|
|
419
|
+
});
|
|
420
|
+
const modelOption = interaction.commandName === "model"
|
|
421
|
+
? interaction.options.getString("model")?.trim()
|
|
422
|
+
: undefined;
|
|
423
|
+
const sandboxAction = interaction.commandName === "sandbox"
|
|
424
|
+
? interaction.options.getString("action")?.trim()
|
|
425
|
+
: undefined;
|
|
426
|
+
const commandArg = modelOption ?? sandboxAction;
|
|
427
|
+
const commandText = commandArg
|
|
428
|
+
? `/${interaction.commandName} ${commandArg}`
|
|
429
|
+
: `/${interaction.commandName}`;
|
|
430
|
+
this.logToFile(conversationId, {
|
|
431
|
+
date: new Date(interaction.createdTimestamp).toISOString(),
|
|
432
|
+
ts: interaction.id,
|
|
433
|
+
...(threadTs ? { threadTs } : {}),
|
|
434
|
+
user: interaction.user.id,
|
|
435
|
+
userName: interaction.user.username,
|
|
436
|
+
text: commandText,
|
|
437
|
+
attachments: [],
|
|
438
|
+
isBot: false,
|
|
439
|
+
});
|
|
440
|
+
const adapters = this.createSlashCommandAdapters(interaction, commandText, sessionKey, conversationId);
|
|
441
|
+
try {
|
|
442
|
+
if (interaction.commandName === "new") {
|
|
443
|
+
await this.handler.handleNewCommand(sessionKey, conversationId, this);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (interaction.commandName === "stop") {
|
|
447
|
+
const stopTarget = this.resolveStopTarget(conversationId, sessionKey);
|
|
448
|
+
if (stopTarget) {
|
|
449
|
+
await this.handler.handleStop(stopTarget, conversationId, this);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
await adapters.responseCtx.respond(formatNothingRunning("discord"));
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const event = {
|
|
457
|
+
type: "dm",
|
|
458
|
+
conversationId,
|
|
459
|
+
conversationKind: isDM ? "direct" : "shared",
|
|
460
|
+
ts: interaction.id,
|
|
461
|
+
thread_ts: threadTs,
|
|
462
|
+
sessionKey,
|
|
463
|
+
user: interaction.user.id,
|
|
464
|
+
text: commandText,
|
|
465
|
+
attachments: [],
|
|
466
|
+
};
|
|
467
|
+
await this.handler.handleEvent(event, this, adapters);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
log.logWarning("Discord slash command error", err instanceof Error ? err.message : String(err));
|
|
471
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
472
|
+
await interaction.reply({
|
|
473
|
+
content: `${interaction.commandName} command failed. Please try again later.`,
|
|
474
|
+
ephemeral: !isDM,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
this.client.on(Events.MessageCreate, async (msg) => {
|
|
480
|
+
// Skip messages from before startup
|
|
481
|
+
if (msg.createdTimestamp < this.startupTime)
|
|
482
|
+
return;
|
|
483
|
+
// Skip bot messages
|
|
484
|
+
if (msg.author.bot)
|
|
485
|
+
return;
|
|
486
|
+
const isDM = msg.channel.type === 1; // ChannelType.DM = 1
|
|
487
|
+
const isInThread = msg.channel.isThread();
|
|
488
|
+
const referencedMsgId = msg.reference?.messageId;
|
|
489
|
+
const isThreadReply = isInThread || !!referencedMsgId;
|
|
490
|
+
const isMentioned = msg.mentions.users.has(this.botUserId ?? "");
|
|
491
|
+
const isAutoReplyCandidate = !isDM && !isMentioned && !isThreadReply;
|
|
492
|
+
const { conversationId, threadTs } = this.resolveConversationContext({
|
|
493
|
+
channelId: msg.channelId,
|
|
494
|
+
inGuild: !isDM,
|
|
495
|
+
isThread: isInThread,
|
|
496
|
+
parentChannelId: "parentId" in msg.channel ? msg.channel.parentId : null,
|
|
497
|
+
referencedMsgId,
|
|
498
|
+
});
|
|
499
|
+
const userId = msg.author.id;
|
|
500
|
+
const userName = msg.author.username;
|
|
501
|
+
const msgId = msg.id;
|
|
502
|
+
// Track user
|
|
503
|
+
this.users.set(userId, {
|
|
504
|
+
id: userId,
|
|
505
|
+
userName,
|
|
506
|
+
displayName: msg.member?.displayName ?? userName,
|
|
507
|
+
});
|
|
508
|
+
// Track channel
|
|
509
|
+
if (!this.channels.has(conversationId) && "name" in msg.channel) {
|
|
510
|
+
const ch = msg.channel;
|
|
511
|
+
this.channels.set(conversationId, { id: conversationId, name: ch.name });
|
|
512
|
+
}
|
|
513
|
+
const conversationKind = isDM ? "direct" : "shared";
|
|
514
|
+
const sessionKey = resolveChatSessionKey({
|
|
515
|
+
conversationId,
|
|
516
|
+
conversationKind,
|
|
517
|
+
messageId: msgId,
|
|
518
|
+
persistentTopLevel: true,
|
|
519
|
+
threadTs,
|
|
520
|
+
});
|
|
521
|
+
const cleanedText = this.stripBotMention(msg.content);
|
|
522
|
+
const eventBase = {
|
|
523
|
+
type: isDM ? "dm" : "mention",
|
|
524
|
+
conversationId,
|
|
525
|
+
conversationKind,
|
|
526
|
+
ts: msgId,
|
|
527
|
+
thread_ts: threadTs,
|
|
528
|
+
sessionKey,
|
|
529
|
+
user: userId,
|
|
530
|
+
userName,
|
|
531
|
+
text: cleanedText,
|
|
532
|
+
};
|
|
533
|
+
// Handle stop before trigger gate — "stop" should never be auto-reply judged.
|
|
534
|
+
if (cleanedText.toLowerCase() === "stop" || cleanedText.toLowerCase() === "/stop") {
|
|
535
|
+
const stopTarget = this.resolveStopTarget(conversationId, sessionKey);
|
|
536
|
+
if (stopTarget) {
|
|
537
|
+
this.handler.handleStop(stopTarget, conversationId, this);
|
|
538
|
+
}
|
|
539
|
+
else if (!isAutoReplyCandidate) {
|
|
540
|
+
await this.postMessage(conversationId, formatNothingRunning("discord"));
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const triggerResult = isAutoReplyCandidate
|
|
545
|
+
? await evaluateAutoReplyPolicy({ event: eventBase, workingDir: this.workingDir })
|
|
546
|
+
: { trigger: true, reason: "addressed" };
|
|
547
|
+
const logEntryBase = {
|
|
548
|
+
date: msg.createdAt.toISOString(),
|
|
549
|
+
ts: msgId,
|
|
550
|
+
...(!isDM && threadTs ? { threadTs } : {}),
|
|
551
|
+
user: userId,
|
|
552
|
+
userName,
|
|
553
|
+
text: cleanedText,
|
|
554
|
+
isBot: false,
|
|
555
|
+
};
|
|
556
|
+
if (!triggerResult.trigger) {
|
|
557
|
+
this.logToFile(conversationId, { ...logEntryBase, attachments: [] });
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const processedAttachments = await this.processAttachments(conversationId, msg.attachments, msgId);
|
|
561
|
+
const event = { ...eventBase, attachments: processedAttachments };
|
|
562
|
+
this.logToFile(conversationId, { ...logEntryBase, attachments: processedAttachments });
|
|
563
|
+
this.getQueue(sessionKey).enqueue(() => {
|
|
564
|
+
const adapters = createDiscordAdapters(event, this);
|
|
565
|
+
return this.handler.handleEvent(event, this, adapters);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
async fetchTextChannel(channelId) {
|
|
570
|
+
const ch = await this.client.channels.fetch(channelId);
|
|
571
|
+
if (!ch || !ch.isTextBased()) {
|
|
572
|
+
throw new Error(`Channel ${channelId} is not a text channel`);
|
|
573
|
+
}
|
|
574
|
+
return ch;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
//# sourceMappingURL=bot.js.map
|