@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.10
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/README.md +171 -334
- package/dist/adapter.d.ts +36 -10
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +10 -5
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +349 -114
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +102 -31
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +71 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +168 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +29 -22
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +620 -186
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +22 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +97 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +136 -71
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +3 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +16 -0
- package/dist/adapters/slack/session.js.map +1 -0
- package/dist/adapters/slack/tools/attach.d.ts +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +2 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +190 -123
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +57 -59
- package/dist/adapters/telegram/context.js.map +1 -1
- 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/agent.d.ts +9 -10
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +645 -555
- package/dist/agent.js.map +1 -1
- package/dist/commands/auto-reply.d.ts +16 -0
- package/dist/commands/auto-reply.d.ts.map +1 -0
- package/dist/commands/auto-reply.js +69 -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 +19 -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 +76 -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 +112 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +28 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/registry.d.ts +7 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +14 -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 +88 -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 +53 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +320 -55
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +10 -42
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +15 -128
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +16 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +127 -58
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +24 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +115 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/file-guards.d.ts +6 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +48 -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 +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +3 -3
- package/dist/instrument.js.map +1 -1
- package/dist/log.d.ts +3 -7
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +20 -45
- package/dist/log.js.map +1 -1
- package/dist/login/index.d.ts +41 -0
- package/dist/login/index.d.ts.map +1 -0
- package/dist/login/index.js +202 -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 +1453 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/login/session.d.ts +33 -0
- package/dist/login/session.d.ts.map +1 -0
- package/dist/login/session.js +68 -0
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +229 -264
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +79 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +437 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/runtime/conversation-orchestrator.d.ts +42 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +150 -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 +27 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +211 -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 +137 -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 +126 -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/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +2 -2
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +6 -4
- package/dist/sentry.js.map +1 -1
- package/dist/session-policy.d.ts +13 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +23 -0
- package/dist/session-policy.js.map +1 -0
- package/dist/session-store.d.ts +35 -8
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +182 -23
- package/dist/session-store.js.map +1 -1
- 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 +11 -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 +1742 -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 +427 -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 +39 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/store.d.ts +4 -7
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +26 -52
- package/dist/store.js.map +1 -1
- 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 +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- 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 +8 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- 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 +264 -0
- package/dist/vault.js.map +1 -0
- package/package.json +16 -13
|
@@ -1,35 +1,24 @@
|
|
|
1
|
-
import { Client, Events, GatewayIntentBits, Partials, } from "discord.js";
|
|
2
|
-
import {
|
|
1
|
+
import { ApplicationCommandOptionType, Client, Events, GatewayIntentBits, Partials, } from "discord.js";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { basename, join } from "path";
|
|
4
4
|
import * as log from "../../log.js";
|
|
5
|
+
import { resolveChatSessionKey } from "../../session-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";
|
|
5
9
|
import { createDiscordAdapters } from "./context.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return this.queue.length;
|
|
17
|
-
}
|
|
18
|
-
async processNext() {
|
|
19
|
-
if (this.processing || this.queue.length === 0)
|
|
20
|
-
return;
|
|
21
|
-
this.processing = true;
|
|
22
|
-
const work = this.queue.shift();
|
|
23
|
-
try {
|
|
24
|
-
await work();
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
log.logWarning("Discord queue error", err instanceof Error ? err.message : String(err));
|
|
28
|
-
}
|
|
29
|
-
this.processing = false;
|
|
30
|
-
this.processNext();
|
|
31
|
-
}
|
|
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;
|
|
32
20
|
}
|
|
21
|
+
const discordRetry = (fn) => withRetry(fn, { isRateLimited: discordIsRateLimited });
|
|
33
22
|
// ============================================================================
|
|
34
23
|
// DiscordBot
|
|
35
24
|
// ============================================================================
|
|
@@ -41,6 +30,7 @@ export class DiscordBot {
|
|
|
41
30
|
this.channels = new Map();
|
|
42
31
|
this.users = new Map();
|
|
43
32
|
this.handler = handler;
|
|
33
|
+
this.token = config.token;
|
|
44
34
|
this.workingDir = config.workingDir;
|
|
45
35
|
this.client = new Client({
|
|
46
36
|
intents: [
|
|
@@ -57,34 +47,84 @@ export class DiscordBot {
|
|
|
57
47
|
// ==========================================================================
|
|
58
48
|
async start() {
|
|
59
49
|
await new Promise((resolve, reject) => {
|
|
60
|
-
this.client.once(Events.ClientReady, (readyClient) => {
|
|
50
|
+
this.client.once(Events.ClientReady, async (readyClient) => {
|
|
61
51
|
this.botUserId = readyClient.user.id;
|
|
62
52
|
this.startupTime = Date.now();
|
|
63
|
-
log.logConnected();
|
|
53
|
+
log.logConnected("Discord");
|
|
64
54
|
log.logInfo(`Discord bot started as ${readyClient.user.tag}`);
|
|
65
55
|
this.loadCachedGuildData();
|
|
66
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
|
+
}
|
|
67
104
|
resolve();
|
|
68
105
|
});
|
|
69
106
|
this.client.once(Events.Error, reject);
|
|
70
|
-
this.client.login(
|
|
107
|
+
this.client.login(this.token).catch(reject);
|
|
71
108
|
});
|
|
72
109
|
}
|
|
73
110
|
async postMessage(channel, text) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
return discordRetry(async () => {
|
|
112
|
+
const ch = await this.fetchTextChannel(channel);
|
|
113
|
+
const msg = await ch.send(text);
|
|
114
|
+
return msg.id;
|
|
115
|
+
});
|
|
77
116
|
}
|
|
78
117
|
async updateMessage(channel, ts, text) {
|
|
79
118
|
await this.updateMessageRaw(channel, ts, text);
|
|
80
119
|
}
|
|
81
120
|
enqueueEvent(event) {
|
|
82
|
-
const
|
|
121
|
+
const conversationId = event.conversationId;
|
|
122
|
+
const queue = this.getQueue(conversationId);
|
|
83
123
|
if (queue.size() >= 5) {
|
|
84
|
-
log.logWarning(`Event queue full for ${
|
|
124
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
85
125
|
return false;
|
|
86
126
|
}
|
|
87
|
-
log.logInfo(`Enqueueing event for ${
|
|
127
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
88
128
|
queue.enqueue(() => {
|
|
89
129
|
const adapters = createDiscordAdapters(event, this, true);
|
|
90
130
|
return this.handler.handleEvent(event, this, adapters, true);
|
|
@@ -97,29 +137,38 @@ export class DiscordBot {
|
|
|
97
137
|
formattingGuide: "## Discord Formatting (Markdown)\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\ncode```\nLinks: [text](url)",
|
|
98
138
|
channels: this.getAllChannels(),
|
|
99
139
|
users: this.getAllUsers(),
|
|
140
|
+
diagnostics: {
|
|
141
|
+
showUsageSummary: false,
|
|
142
|
+
},
|
|
100
143
|
};
|
|
101
144
|
}
|
|
102
145
|
// ==========================================================================
|
|
103
146
|
// Internal helpers (used by context.ts)
|
|
104
147
|
// ==========================================================================
|
|
105
148
|
async updateMessageRaw(channelId, messageId, text) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
});
|
|
109
154
|
}
|
|
110
155
|
async postReply(channelId, replyToId, text) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
});
|
|
115
162
|
}
|
|
116
163
|
async postInThread(channelId, threadOrMessageId, text) {
|
|
117
164
|
// Try as a thread channel first, then fall back to posting in the channel
|
|
118
165
|
try {
|
|
119
166
|
const thread = await this.client.channels.fetch(threadOrMessageId);
|
|
120
167
|
if (thread && (thread.isThread() || thread.isTextBased())) {
|
|
121
|
-
|
|
122
|
-
|
|
168
|
+
return discordRetry(async () => {
|
|
169
|
+
const msg = await thread.send(text);
|
|
170
|
+
return msg.id;
|
|
171
|
+
});
|
|
123
172
|
}
|
|
124
173
|
}
|
|
125
174
|
catch {
|
|
@@ -147,10 +196,22 @@ export class DiscordBot {
|
|
|
147
196
|
}
|
|
148
197
|
}
|
|
149
198
|
async uploadFile(channelId, filePath, title) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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);
|
|
154
215
|
}
|
|
155
216
|
getAllChannels() {
|
|
156
217
|
return Array.from(this.channels.values());
|
|
@@ -159,50 +220,41 @@ export class DiscordBot {
|
|
|
159
220
|
return Array.from(this.users.values());
|
|
160
221
|
}
|
|
161
222
|
logToFile(channelId, entry) {
|
|
162
|
-
|
|
163
|
-
if (!existsSync(dir))
|
|
164
|
-
mkdirSync(dir, { recursive: true });
|
|
165
|
-
appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
223
|
+
appendChannelLog(this.workingDir, channelId, entry);
|
|
166
224
|
}
|
|
167
225
|
logBotResponse(channelId, text, ts) {
|
|
168
|
-
this.
|
|
169
|
-
date: new Date().toISOString(),
|
|
170
|
-
ts,
|
|
171
|
-
user: "bot",
|
|
172
|
-
text,
|
|
173
|
-
attachments: [],
|
|
174
|
-
isBot: true,
|
|
175
|
-
});
|
|
226
|
+
appendBotResponseLog(this.workingDir, channelId, text, ts);
|
|
176
227
|
}
|
|
177
228
|
/**
|
|
178
|
-
* Process attachments from a Discord message
|
|
179
|
-
* Downloads files
|
|
180
|
-
* Returns format compatible with ChatMessage: { name: string, localPath: string }[]
|
|
229
|
+
* Process attachments from a Discord message.
|
|
230
|
+
* Downloads files before returning so the agent can read them immediately.
|
|
181
231
|
*/
|
|
182
|
-
processAttachments(channelId, attachments, _messageId) {
|
|
183
|
-
const
|
|
232
|
+
async processAttachments(channelId, attachments, _messageId) {
|
|
233
|
+
const downloads = [];
|
|
184
234
|
// Discord attachments Collection - iterate over values
|
|
185
235
|
for (const attachment of attachments.values()) {
|
|
186
236
|
if (!attachment.name) {
|
|
187
237
|
log.logWarning("Discord attachment missing name, skipping", attachment.url);
|
|
188
238
|
continue;
|
|
189
239
|
}
|
|
190
|
-
// Generate local filename
|
|
191
240
|
const ts = Date.now();
|
|
192
241
|
const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
193
242
|
const filename = `${ts}_${sanitizedName}`;
|
|
194
243
|
const localPath = `${channelId}/attachments/${filename}`;
|
|
195
244
|
const fullDir = join(this.workingDir, channelId, "attachments");
|
|
196
|
-
result
|
|
245
|
+
const result = {
|
|
197
246
|
name: attachment.name,
|
|
198
|
-
localPath
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
247
|
+
localPath,
|
|
248
|
+
};
|
|
249
|
+
downloads.push(this.downloadAttachment(fullDir, filename, attachment.url)
|
|
250
|
+
.then(() => result)
|
|
251
|
+
.catch((err) => {
|
|
202
252
|
log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);
|
|
203
|
-
|
|
253
|
+
return null;
|
|
254
|
+
}));
|
|
204
255
|
}
|
|
205
|
-
|
|
256
|
+
const results = await Promise.all(downloads);
|
|
257
|
+
return results.filter((attachment) => attachment !== null);
|
|
206
258
|
}
|
|
207
259
|
/**
|
|
208
260
|
* Download an attachment from URL to local file
|
|
@@ -219,7 +271,9 @@ export class DiscordBot {
|
|
|
219
271
|
writeFileSync(join(dir, filename), Buffer.from(buffer));
|
|
220
272
|
}
|
|
221
273
|
catch (err) {
|
|
222
|
-
throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}
|
|
274
|
+
throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
275
|
+
cause: err,
|
|
276
|
+
});
|
|
223
277
|
}
|
|
224
278
|
}
|
|
225
279
|
// ==========================================================================
|
|
@@ -228,11 +282,23 @@ export class DiscordBot {
|
|
|
228
282
|
getQueue(channelId) {
|
|
229
283
|
let queue = this.queues.get(channelId);
|
|
230
284
|
if (!queue) {
|
|
231
|
-
queue = new ChannelQueue();
|
|
285
|
+
queue = new ChannelQueue("Discord");
|
|
232
286
|
this.queues.set(channelId, queue);
|
|
233
287
|
}
|
|
234
288
|
return queue;
|
|
235
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
|
+
}
|
|
236
302
|
loadCachedGuildData() {
|
|
237
303
|
for (const guild of this.client.guilds.cache.values()) {
|
|
238
304
|
for (const channel of guild.channels.cache.values()) {
|
|
@@ -254,7 +320,162 @@ export class DiscordBot {
|
|
|
254
320
|
return text;
|
|
255
321
|
return text.replace(new RegExp(`<@!?${this.botUserId}>`, "g"), "").trim();
|
|
256
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
|
+
}
|
|
257
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, false);
|
|
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
|
+
});
|
|
258
479
|
this.client.on(Events.MessageCreate, async (msg) => {
|
|
259
480
|
// Skip messages from before startup
|
|
260
481
|
if (msg.createdTimestamp < this.startupTime)
|
|
@@ -262,12 +483,19 @@ export class DiscordBot {
|
|
|
262
483
|
// Skip bot messages
|
|
263
484
|
if (msg.author.bot)
|
|
264
485
|
return;
|
|
265
|
-
// Skip if bot isn't mentioned and it's not a DM
|
|
266
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;
|
|
267
490
|
const isMentioned = msg.mentions.users.has(this.botUserId ?? "");
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
});
|
|
271
499
|
const userId = msg.author.id;
|
|
272
500
|
const userName = msg.author.username;
|
|
273
501
|
const msgId = msg.id;
|
|
@@ -278,57 +506,64 @@ export class DiscordBot {
|
|
|
278
506
|
displayName: msg.member?.displayName ?? userName,
|
|
279
507
|
});
|
|
280
508
|
// Track channel
|
|
281
|
-
if (!this.channels.has(
|
|
509
|
+
if (!this.channels.has(conversationId) && "name" in msg.channel) {
|
|
282
510
|
const ch = msg.channel;
|
|
283
|
-
this.channels.set(
|
|
511
|
+
this.channels.set(conversationId, { id: conversationId, name: ch.name });
|
|
284
512
|
}
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
513
|
+
const conversationKind = isDM ? "direct" : "shared";
|
|
514
|
+
const sessionKey = resolveChatSessionKey({
|
|
515
|
+
conversationId,
|
|
516
|
+
conversationKind,
|
|
517
|
+
messageId: msgId,
|
|
518
|
+
persistentTopLevel: true,
|
|
519
|
+
threadTs,
|
|
520
|
+
});
|
|
290
521
|
const cleanedText = this.stripBotMention(msg.content);
|
|
291
|
-
|
|
292
|
-
const processedAttachments = this.processAttachments(channelId, msg.attachments, msgId);
|
|
293
|
-
const event = {
|
|
522
|
+
const eventBase = {
|
|
294
523
|
type: isDM ? "dm" : "mention",
|
|
295
|
-
|
|
524
|
+
conversationId,
|
|
525
|
+
conversationKind,
|
|
296
526
|
ts: msgId,
|
|
297
527
|
thread_ts: threadTs,
|
|
528
|
+
sessionKey,
|
|
298
529
|
user: userId,
|
|
299
530
|
userName,
|
|
300
531
|
text: cleanedText,
|
|
301
|
-
attachments: processedAttachments,
|
|
302
532
|
};
|
|
303
|
-
//
|
|
304
|
-
|
|
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 = {
|
|
305
548
|
date: msg.createdAt.toISOString(),
|
|
306
549
|
ts: msgId,
|
|
550
|
+
...(!isDM && threadTs ? { threadTs } : {}),
|
|
307
551
|
user: userId,
|
|
308
552
|
userName,
|
|
309
553
|
text: cleanedText,
|
|
310
|
-
attachments: processedAttachments,
|
|
311
554
|
isBot: false,
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (this.handler.isRunning(sessionKey)) {
|
|
316
|
-
this.handler.handleStop(sessionKey, channelId, this);
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
await this.postMessage(channelId, "_Nothing running_");
|
|
320
|
-
}
|
|
555
|
+
};
|
|
556
|
+
if (!triggerResult.trigger) {
|
|
557
|
+
this.logToFile(conversationId, { ...logEntryBase, attachments: [] });
|
|
321
558
|
return;
|
|
322
559
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
});
|
|
331
|
-
}
|
|
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, false);
|
|
565
|
+
return this.handler.handleEvent(event, this, adapters, false);
|
|
566
|
+
});
|
|
332
567
|
});
|
|
333
568
|
}
|
|
334
569
|
async fetchTextChannel(channelId) {
|