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