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