@friendlyrobot/discord-pi-agent 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent-service.d.ts +2 -2
- package/dist/commands.d.ts +1 -1
- package/dist/commands.test.d.ts +1 -0
- package/dist/config.d.ts +2 -9
- package/dist/discord-attachments.d.ts +19 -0
- package/dist/discord-attachments.test.d.ts +1 -0
- package/dist/discord-auth.d.ts +9 -0
- package/dist/discord-auth.test.d.ts +1 -0
- package/dist/discord-gateway-client.d.ts +2 -2
- package/dist/discord-media-resolution.d.ts +17 -0
- package/dist/discord-media-resolution.test.d.ts +1 -0
- package/dist/discord-message-handler.d.ts +6 -0
- package/dist/discord-replies.d.ts +4 -0
- package/dist/discord-typing.d.ts +4 -0
- package/dist/image-description.d.ts +1 -1
- package/dist/index.d.ts +3 -7
- package/dist/index.js +688 -640
- package/dist/prompt-queue.d.ts +1 -1
- package/dist/reply-buffer.d.ts +1 -1
- package/dist/types.d.ts +11 -20
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -110,7 +110,7 @@ async function formatWithPrettier(text) {
|
|
|
110
110
|
|
|
111
111
|
// src/reply-buffer.ts
|
|
112
112
|
var logger3 = createModuleLogger("reply-buffer");
|
|
113
|
-
async function
|
|
113
|
+
async function runPromptAndCollectReply(session, prompt, options = {}) {
|
|
114
114
|
let streamedText = "";
|
|
115
115
|
let eventCount = 0;
|
|
116
116
|
let toolCount = 0;
|
|
@@ -278,7 +278,7 @@ class AgentService {
|
|
|
278
278
|
async prompt(text) {
|
|
279
279
|
const session = this.requireSession();
|
|
280
280
|
const transformedPrompt = await this.config.promptTransform(text);
|
|
281
|
-
return
|
|
281
|
+
return runPromptAndCollectReply(session, transformedPrompt, {
|
|
282
282
|
logPrefix: `[agent:${session.sessionId}]`
|
|
283
283
|
});
|
|
284
284
|
}
|
|
@@ -531,9 +531,10 @@ function isSameModel(currentModel, desiredModel) {
|
|
|
531
531
|
import path2 from "node:path";
|
|
532
532
|
import dotenv from "dotenv";
|
|
533
533
|
function resolveConfig(config) {
|
|
534
|
+
const discordAllowedUserId = readRequiredValue("discordAllowedUserId", config.discordAllowedUserId);
|
|
534
535
|
return {
|
|
535
536
|
discordBotToken: readRequiredValue("discordBotToken", config.discordBotToken),
|
|
536
|
-
discordAllowedUserId
|
|
537
|
+
discordAllowedUserId,
|
|
537
538
|
cwd: readRequiredValue("cwd", config.cwd),
|
|
538
539
|
agentDir: config.agentDir?.trim() || path2.join(config.cwd, ".pi-agent"),
|
|
539
540
|
modelProvider: config.modelProvider?.trim() || "openrouter",
|
|
@@ -544,10 +545,13 @@ function resolveConfig(config) {
|
|
|
544
545
|
promptTransform: config.promptTransform || identityPromptTransform,
|
|
545
546
|
startupMessage: config.startupMessage === undefined ? "Bot is online and ready." : config.startupMessage,
|
|
546
547
|
shutdownOnSignals: config.shutdownOnSignals ?? true,
|
|
547
|
-
visionModelId: config.visionModelId?.trim() || null
|
|
548
|
+
visionModelId: config.visionModelId?.trim() || null,
|
|
549
|
+
discordAllowedForumChannelIds: config.discordAllowedForumChannelIds ?? [],
|
|
550
|
+
discordAllowedUserIds: config.discordAllowedUserIds ?? [discordAllowedUserId],
|
|
551
|
+
sessionIdleTimeoutMs: config.sessionIdleTimeoutMs ?? null
|
|
548
552
|
};
|
|
549
553
|
}
|
|
550
|
-
function
|
|
554
|
+
function loadDiscordGatewayConfigFromEnv(overrides = {}) {
|
|
551
555
|
dotenv.config();
|
|
552
556
|
return resolveConfig({
|
|
553
557
|
discordBotToken: overrides.discordBotToken || process.env.DISCORD_BOT_TOKEN || "",
|
|
@@ -562,19 +566,11 @@ function loadDiscordPiBridgeConfigFromEnv(overrides = {}) {
|
|
|
562
566
|
promptTransform: overrides.promptTransform,
|
|
563
567
|
startupMessage: overrides.startupMessage ?? readStartupMessageFromEnv(),
|
|
564
568
|
shutdownOnSignals: overrides.shutdownOnSignals,
|
|
565
|
-
visionModelId: overrides.visionModelId ?? process.env.PI_VISION_MODEL_ID
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
function loadDiscordGatewayConfigFromEnv(overrides = {}) {
|
|
569
|
-
const base = loadDiscordPiBridgeConfigFromEnv(overrides);
|
|
570
|
-
return {
|
|
571
|
-
...base,
|
|
569
|
+
visionModelId: overrides.visionModelId ?? process.env.PI_VISION_MODEL_ID,
|
|
572
570
|
discordAllowedForumChannelIds: overrides.discordAllowedForumChannelIds ?? parseStringArrayFromEnv("DISCORD_FORUM_CHANNEL_IDS") ?? [],
|
|
573
|
-
discordAllowedUserIds: overrides.discordAllowedUserIds ?? parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS")
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
sessionIdleTimeoutMs: overrides.sessionIdleTimeoutMs ?? parseOptionalIntFromEnv("DISCORD_SESSION_IDLE_TIMEOUT_MS") ?? null
|
|
577
|
-
};
|
|
571
|
+
discordAllowedUserIds: overrides.discordAllowedUserIds ?? parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS"),
|
|
572
|
+
sessionIdleTimeoutMs: overrides.sessionIdleTimeoutMs ?? parseOptionalIntFromEnv("DISCORD_SESSION_IDLE_TIMEOUT_MS") ?? undefined
|
|
573
|
+
});
|
|
578
574
|
}
|
|
579
575
|
function readRequiredValue(name, value) {
|
|
580
576
|
const trimmedValue = value.trim();
|
|
@@ -594,17 +590,6 @@ function readStartupMessageFromEnv() {
|
|
|
594
590
|
}
|
|
595
591
|
return trimmedValue;
|
|
596
592
|
}
|
|
597
|
-
function resolveGatewayConfig(config) {
|
|
598
|
-
const base = resolveConfig(config);
|
|
599
|
-
return {
|
|
600
|
-
...base,
|
|
601
|
-
discordAllowedForumChannelIds: config.discordAllowedForumChannelIds ?? [],
|
|
602
|
-
discordAllowedUserIds: config.discordAllowedUserIds ?? [
|
|
603
|
-
base.discordAllowedUserId
|
|
604
|
-
],
|
|
605
|
-
sessionIdleTimeoutMs: config.sessionIdleTimeoutMs ?? null
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
593
|
function parseThinkingLevel(value) {
|
|
609
594
|
if (!value) {
|
|
610
595
|
return;
|
|
@@ -644,7 +629,6 @@ function parseOptionalIntFromEnv(key) {
|
|
|
644
629
|
|
|
645
630
|
// src/discord-gateway-client.ts
|
|
646
631
|
import {
|
|
647
|
-
ChannelType,
|
|
648
632
|
Client,
|
|
649
633
|
Events,
|
|
650
634
|
GatewayIntentBits,
|
|
@@ -669,7 +653,7 @@ function getSessionStatusText(session, promptQueue, extras) {
|
|
|
669
653
|
`queue-busy: ${queueStatus.busy}`
|
|
670
654
|
];
|
|
671
655
|
if (extras?.tools && extras.tools.length > 0) {
|
|
672
|
-
const toolNames = extras.tools.map((
|
|
656
|
+
const toolNames = extras.tools.map((tool) => tool.name);
|
|
673
657
|
lines.push("", `Tools (${extras.tools.length}): ${toolNames.join(", ")}`);
|
|
674
658
|
}
|
|
675
659
|
if (extras?.skillsSummary) {
|
|
@@ -691,260 +675,410 @@ function getThinkingInfo(session) {
|
|
|
691
675
|
supported: true
|
|
692
676
|
};
|
|
693
677
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (trimmed === "!help") {
|
|
701
|
-
const extraCommands = session ? `
|
|
702
|
-
!archive - archive this thread and end the session` : "";
|
|
678
|
+
function getEffectiveSession(context) {
|
|
679
|
+
return context.session ?? context.agentService.getSession();
|
|
680
|
+
}
|
|
681
|
+
function requireEffectiveSession(context) {
|
|
682
|
+
const session = getEffectiveSession(context);
|
|
683
|
+
if (!session) {
|
|
703
684
|
return {
|
|
704
685
|
handled: true,
|
|
705
|
-
response:
|
|
706
|
-
"Commands:",
|
|
707
|
-
"!help - show this message",
|
|
708
|
-
"!status - show current session status",
|
|
709
|
-
"!thinking - show or set thinking/reasoning level",
|
|
710
|
-
"!model - list available models or switch to one",
|
|
711
|
-
"!compact - compact the persistent session",
|
|
712
|
-
"!reset-session - start a fresh persistent session",
|
|
713
|
-
"!reload - reload resources (AGENTS.md, extensions, skills, etc.)",
|
|
714
|
-
extraCommands,
|
|
715
|
-
"Any other text goes to the agent session."
|
|
716
|
-
].filter(Boolean).join(`
|
|
717
|
-
`)
|
|
686
|
+
response: "No active session."
|
|
718
687
|
};
|
|
719
688
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
689
|
+
return { session };
|
|
690
|
+
}
|
|
691
|
+
async function handleHelpCommand(trimmedInput, context) {
|
|
692
|
+
if (trimmedInput !== "!help") {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
const extraCommands = context.session ? `
|
|
696
|
+
!archive - archive this thread and end the session` : "";
|
|
697
|
+
return {
|
|
698
|
+
handled: true,
|
|
699
|
+
response: [
|
|
700
|
+
"Commands:",
|
|
701
|
+
"!help - show this message",
|
|
702
|
+
"!status - show current session status",
|
|
703
|
+
"!thinking - show or set thinking/reasoning level",
|
|
704
|
+
"!model - list available models or switch to one",
|
|
705
|
+
"!compact - compact the persistent session",
|
|
706
|
+
"!reset-session - start a fresh persistent session",
|
|
707
|
+
"!reload - reload resources (AGENTS.md, extensions, skills, etc.)",
|
|
708
|
+
extraCommands,
|
|
709
|
+
"Any other text goes to the agent session."
|
|
710
|
+
].filter(Boolean).join(`
|
|
711
|
+
`)
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
async function handleArchiveCommand(trimmedInput, context) {
|
|
715
|
+
if (trimmedInput !== "!archive") {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
if (!context.session) {
|
|
727
719
|
return {
|
|
728
720
|
handled: true,
|
|
729
|
-
|
|
730
|
-
response: "Archiving thread and shutting down session."
|
|
721
|
+
response: "!archive is only available in forum threads."
|
|
731
722
|
};
|
|
732
723
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
724
|
+
return {
|
|
725
|
+
handled: true,
|
|
726
|
+
archive: true,
|
|
727
|
+
response: "Archiving thread and shutting down session."
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async function handleStatusCommand(trimmedInput, context) {
|
|
731
|
+
if (trimmedInput !== "!status") {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const effectiveSession = requireEffectiveSession(context);
|
|
735
|
+
if ("handled" in effectiveSession) {
|
|
736
|
+
return effectiveSession;
|
|
737
|
+
}
|
|
738
|
+
const tools = effectiveSession.session.getAllTools();
|
|
739
|
+
const extensionsSummary = context.agentService.getExtensionsSummary();
|
|
740
|
+
const skillsSummary = context.agentService.getSkillsSummary();
|
|
741
|
+
return {
|
|
742
|
+
handled: true,
|
|
743
|
+
response: getSessionStatusText(effectiveSession.session, context.promptQueue, {
|
|
744
|
+
tools,
|
|
745
|
+
extensionsSummary,
|
|
746
|
+
skillsSummary
|
|
747
|
+
})
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
async function handleThinkingCommand(trimmedInput, context) {
|
|
751
|
+
if (trimmedInput !== "!thinking" && !trimmedInput.startsWith("!thinking ")) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
const effectiveSession = requireEffectiveSession(context);
|
|
755
|
+
if ("handled" in effectiveSession) {
|
|
756
|
+
return effectiveSession;
|
|
757
|
+
}
|
|
758
|
+
const parts = trimmedInput.split(" ");
|
|
759
|
+
if (parts.length === 1) {
|
|
760
|
+
const info = getThinkingInfo(effectiveSession.session);
|
|
761
|
+
if (!info.supported) {
|
|
736
762
|
return {
|
|
737
763
|
handled: true,
|
|
738
|
-
response: "
|
|
764
|
+
response: "Current model does not support reasoning/thinking."
|
|
739
765
|
};
|
|
740
766
|
}
|
|
741
|
-
const tools = effectiveSession.getAllTools();
|
|
742
|
-
const extensionsSummary = agentService.getExtensionsSummary();
|
|
743
|
-
const skillsSummary = agentService.getSkillsSummary();
|
|
744
767
|
return {
|
|
745
768
|
handled: true,
|
|
746
|
-
response:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
769
|
+
response: [
|
|
770
|
+
`Current: ${info.current}`,
|
|
771
|
+
`Available: ${info.available.join(", ")}`,
|
|
772
|
+
`Usage: !thinking <level>`
|
|
773
|
+
].join(`
|
|
774
|
+
`)
|
|
751
775
|
};
|
|
752
776
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if (!effectiveSession) {
|
|
756
|
-
return {
|
|
757
|
-
handled: true,
|
|
758
|
-
response: "No active session."
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
const parts = trimmed.split(" ");
|
|
762
|
-
if (parts.length === 1) {
|
|
763
|
-
const info = getThinkingInfo(effectiveSession);
|
|
764
|
-
if (!info.supported) {
|
|
765
|
-
return {
|
|
766
|
-
handled: true,
|
|
767
|
-
response: "Current model does not support reasoning/thinking."
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
return {
|
|
771
|
-
handled: true,
|
|
772
|
-
response: [
|
|
773
|
-
`Current: ${info.current}`,
|
|
774
|
-
`Available: ${info.available.join(", ")}`,
|
|
775
|
-
`Usage: !thinking <level>`
|
|
776
|
-
].join(`
|
|
777
|
-
`)
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
const requestedLevel = parts[1];
|
|
781
|
-
if (!effectiveSession.supportsThinking()) {
|
|
782
|
-
return {
|
|
783
|
-
handled: true,
|
|
784
|
-
response: "Current model does not support reasoning/thinking."
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
const available = effectiveSession.getAvailableThinkingLevels();
|
|
788
|
-
if (!available.includes(requestedLevel)) {
|
|
789
|
-
return {
|
|
790
|
-
handled: true,
|
|
791
|
-
response: `Invalid thinking level "${requestedLevel}" for current model. Available: ${available.join(", ")}`
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
effectiveSession.setThinkingLevel(requestedLevel);
|
|
777
|
+
const requestedLevel = parts[1];
|
|
778
|
+
if (!effectiveSession.session.supportsThinking()) {
|
|
795
779
|
return {
|
|
796
780
|
handled: true,
|
|
797
|
-
response:
|
|
781
|
+
response: "Current model does not support reasoning/thinking."
|
|
798
782
|
};
|
|
799
783
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (!effectiveSession) {
|
|
803
|
-
return {
|
|
804
|
-
handled: true,
|
|
805
|
-
response: "No active session."
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
const parts = trimmed.split(" ");
|
|
809
|
-
if (parts.length === 1) {
|
|
810
|
-
const current = agentService.getCurrentModelDisplay(effectiveSession);
|
|
811
|
-
const modelList = await agentService.listModels(effectiveSession);
|
|
812
|
-
return {
|
|
813
|
-
handled: true,
|
|
814
|
-
response: `Current model: ${current}
|
|
815
|
-
|
|
816
|
-
${modelList}`
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
const arg = parts.slice(1).join(" ");
|
|
820
|
-
const slashIndex = arg.indexOf("/");
|
|
821
|
-
if (slashIndex === -1) {
|
|
822
|
-
return {
|
|
823
|
-
handled: true,
|
|
824
|
-
response: `Usage: !model <provider/modelId>
|
|
825
|
-
Example: !model openrouter/anthropic/claude-sonnet-4
|
|
826
|
-
Use !model without args to see available models.`
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
const provider = arg.substring(0, slashIndex).trim();
|
|
830
|
-
const modelId = arg.substring(slashIndex + 1).trim();
|
|
784
|
+
const available = effectiveSession.session.getAvailableThinkingLevels();
|
|
785
|
+
if (!available.includes(requestedLevel)) {
|
|
831
786
|
return {
|
|
832
787
|
handled: true,
|
|
833
|
-
response:
|
|
788
|
+
response: `Invalid thinking level "${requestedLevel}" for current model. Available: ${available.join(", ")}`
|
|
834
789
|
};
|
|
835
790
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
791
|
+
effectiveSession.session.setThinkingLevel(requestedLevel);
|
|
792
|
+
return {
|
|
793
|
+
handled: true,
|
|
794
|
+
response: `Thinking level set to "${requestedLevel}".`
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async function handleModelCommand(trimmedInput, context) {
|
|
798
|
+
if (trimmedInput !== "!model" && !trimmedInput.startsWith("!model ")) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
const effectiveSession = requireEffectiveSession(context);
|
|
802
|
+
if ("handled" in effectiveSession) {
|
|
803
|
+
return effectiveSession;
|
|
804
|
+
}
|
|
805
|
+
const parts = trimmedInput.split(" ");
|
|
806
|
+
if (parts.length === 1) {
|
|
807
|
+
const current = context.agentService.getCurrentModelDisplay(effectiveSession.session);
|
|
808
|
+
const modelList = await context.agentService.listModels(effectiveSession.session);
|
|
844
809
|
return {
|
|
845
810
|
handled: true,
|
|
846
|
-
response:
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
})
|
|
811
|
+
response: `Current model: ${current}
|
|
812
|
+
|
|
813
|
+
${modelList}`
|
|
850
814
|
};
|
|
851
815
|
}
|
|
852
|
-
|
|
816
|
+
const argument = parts.slice(1).join(" ");
|
|
817
|
+
const slashIndex = argument.indexOf("/");
|
|
818
|
+
if (slashIndex === -1) {
|
|
853
819
|
return {
|
|
854
820
|
handled: true,
|
|
855
|
-
response:
|
|
856
|
-
|
|
857
|
-
|
|
821
|
+
response: `Usage: !model <provider/modelId>
|
|
822
|
+
` + `Example: !model openrouter/anthropic/claude-sonnet-4
|
|
823
|
+
` + "Use !model without args to see available models."
|
|
858
824
|
};
|
|
859
825
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
826
|
+
const provider = argument.substring(0, slashIndex).trim();
|
|
827
|
+
const modelId = argument.substring(slashIndex + 1).trim();
|
|
828
|
+
return {
|
|
829
|
+
handled: true,
|
|
830
|
+
response: await context.agentService.switchModel(provider, modelId, effectiveSession.session)
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async function handleCompactCommand(trimmedInput, context) {
|
|
834
|
+
if (trimmedInput !== "!compact") {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
const effectiveSession = requireEffectiveSession(context);
|
|
838
|
+
if ("handled" in effectiveSession) {
|
|
839
|
+
return effectiveSession;
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
handled: true,
|
|
843
|
+
response: await context.promptQueue.enqueue(async () => {
|
|
844
|
+
await effectiveSession.session.compact();
|
|
845
|
+
return `Compaction finished for session ${effectiveSession.session.sessionId}.`;
|
|
846
|
+
})
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
async function handleReloadCommand(trimmedInput, context) {
|
|
850
|
+
if (trimmedInput !== "!reload") {
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
handled: true,
|
|
855
|
+
response: await context.promptQueue.enqueue(async () => {
|
|
856
|
+
return context.agentService.reloadResources();
|
|
857
|
+
})
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
async function handleResetSessionCommand(trimmedInput, context) {
|
|
861
|
+
if (trimmedInput !== "!reset-session") {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
if (context.session) {
|
|
872
865
|
return {
|
|
873
866
|
handled: true,
|
|
874
|
-
response: await promptQueue.enqueue(async () => {
|
|
875
|
-
|
|
867
|
+
response: await context.promptQueue.enqueue(async () => {
|
|
868
|
+
const previousSession = context.session;
|
|
869
|
+
await previousSession.abort();
|
|
870
|
+
previousSession.dispose();
|
|
871
|
+
return `Session reset. Old session kept at ${previousSession.sessionFile ?? "(unknown path)"}. Use !archive to archive the thread and start fresh.`;
|
|
876
872
|
})
|
|
877
873
|
};
|
|
878
874
|
}
|
|
879
875
|
return {
|
|
880
876
|
handled: true,
|
|
881
|
-
response:
|
|
877
|
+
response: await context.promptQueue.enqueue(async () => {
|
|
878
|
+
return context.agentService.resetSession();
|
|
879
|
+
})
|
|
882
880
|
};
|
|
883
881
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
promptText = userText.trim().length > 0 ? `The user sent a document with the following message: "${userText}". Please extract and summarize the text content of this document. Be thorough — include all important details, sections, and data from the document.` : "Please extract and summarize the text content of this document. Be thorough — include all important details, sections, data, and key points.";
|
|
899
|
-
} else {
|
|
900
|
-
promptText = userText.trim().length > 0 ? `The user sent this image with the following message: "${userText}". Please describe the image in detail and address any questions from the user's message.` : "Please describe this image in detail. What do you see?";
|
|
882
|
+
var commandHandlers = [
|
|
883
|
+
handleHelpCommand,
|
|
884
|
+
handleArchiveCommand,
|
|
885
|
+
handleStatusCommand,
|
|
886
|
+
handleThinkingCommand,
|
|
887
|
+
handleModelCommand,
|
|
888
|
+
handleCompactCommand,
|
|
889
|
+
handleReloadCommand,
|
|
890
|
+
handleResetSessionCommand
|
|
891
|
+
];
|
|
892
|
+
async function executeCommand(input, context) {
|
|
893
|
+
const trimmedInput = input.trim();
|
|
894
|
+
if (!trimmedInput.startsWith("!")) {
|
|
895
|
+
return { handled: false };
|
|
901
896
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
logger5.error({ error, mimeType }, "vision model prompt failed");
|
|
908
|
-
text = "(Vision model failed to process the file.)";
|
|
909
|
-
} finally {
|
|
910
|
-
session.dispose();
|
|
897
|
+
for (const handler of commandHandlers) {
|
|
898
|
+
const result = await handler(trimmedInput, context);
|
|
899
|
+
if (result) {
|
|
900
|
+
return result;
|
|
901
|
+
}
|
|
911
902
|
}
|
|
912
|
-
|
|
913
|
-
|
|
903
|
+
return {
|
|
904
|
+
handled: true,
|
|
905
|
+
response: `Unknown command: ${trimmedInput}. Try !help.`
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/discord-attachments.ts
|
|
910
|
+
var logger5 = createModuleLogger("discord-attachments");
|
|
911
|
+
var TEXT_ATTACHMENT_EXTENSIONS = [
|
|
912
|
+
".txt",
|
|
913
|
+
".md",
|
|
914
|
+
".json",
|
|
915
|
+
".csv",
|
|
916
|
+
".log",
|
|
917
|
+
".yml",
|
|
918
|
+
".yaml",
|
|
919
|
+
".xml",
|
|
920
|
+
".toml",
|
|
921
|
+
".ini",
|
|
922
|
+
".cfg"
|
|
923
|
+
];
|
|
924
|
+
var MAX_TEXT_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
925
|
+
var MEDIA_ATTACHMENT_EXTENSIONS = [
|
|
926
|
+
".png",
|
|
927
|
+
".jpg",
|
|
928
|
+
".jpeg",
|
|
929
|
+
".gif",
|
|
930
|
+
".webp",
|
|
931
|
+
".pdf",
|
|
932
|
+
".docx",
|
|
933
|
+
".doc",
|
|
934
|
+
".pptx",
|
|
935
|
+
".ppt",
|
|
936
|
+
".xlsx",
|
|
937
|
+
".xls"
|
|
938
|
+
];
|
|
939
|
+
var MAX_MEDIA_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
940
|
+
var OFFICE_MIME_TYPES = new Set([
|
|
941
|
+
"application/pdf",
|
|
942
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
943
|
+
"application/msword",
|
|
944
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
945
|
+
"application/vnd.ms-powerpoint",
|
|
946
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
947
|
+
"application/vnd.ms-excel"
|
|
948
|
+
]);
|
|
949
|
+
function isSupportedTextAttachment(attachment) {
|
|
950
|
+
const ext = attachment.name?.slice(attachment.name.lastIndexOf(".")).toLowerCase();
|
|
951
|
+
return Boolean(ext && TEXT_ATTACHMENT_EXTENSIONS.includes(ext));
|
|
952
|
+
}
|
|
953
|
+
function isSupportedMediaAttachment(attachment) {
|
|
954
|
+
const ext = attachment.name?.slice(attachment.name.lastIndexOf(".")).toLowerCase();
|
|
955
|
+
if (!ext || !MEDIA_ATTACHMENT_EXTENSIONS.includes(ext)) {
|
|
956
|
+
return false;
|
|
914
957
|
}
|
|
915
|
-
|
|
916
|
-
|
|
958
|
+
const contentType = attachment.contentType;
|
|
959
|
+
if (!contentType) {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
return contentType.startsWith("image/") || OFFICE_MIME_TYPES.has(contentType);
|
|
917
963
|
}
|
|
918
|
-
function
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
964
|
+
async function readTextAttachments(message) {
|
|
965
|
+
const attachments = message.attachments;
|
|
966
|
+
if (attachments.size === 0) {
|
|
967
|
+
return [];
|
|
968
|
+
}
|
|
969
|
+
const results = [];
|
|
970
|
+
for (const [, attachment] of attachments) {
|
|
971
|
+
if (!isSupportedTextAttachment(attachment)) {
|
|
972
|
+
logger5.debug({ messageId: message.id, filename: attachment.name }, "skipping non-text attachment");
|
|
923
973
|
continue;
|
|
924
974
|
}
|
|
925
|
-
|
|
926
|
-
|
|
975
|
+
if (attachment.size > MAX_TEXT_ATTACHMENT_SIZE_BYTES) {
|
|
976
|
+
logger5.warn({
|
|
977
|
+
messageId: message.id,
|
|
978
|
+
filename: attachment.name,
|
|
979
|
+
size: attachment.size
|
|
980
|
+
}, "attachment too large, skipping");
|
|
927
981
|
continue;
|
|
928
982
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
983
|
+
try {
|
|
984
|
+
logger5.info({
|
|
985
|
+
messageId: message.id,
|
|
986
|
+
filename: attachment.name,
|
|
987
|
+
size: attachment.size
|
|
988
|
+
}, "fetching attachment");
|
|
989
|
+
const response = await fetch(attachment.url);
|
|
990
|
+
if (!response.ok) {
|
|
991
|
+
logger5.warn({
|
|
992
|
+
messageId: message.id,
|
|
993
|
+
filename: attachment.name,
|
|
994
|
+
status: response.status
|
|
995
|
+
}, "failed to fetch attachment");
|
|
996
|
+
continue;
|
|
933
997
|
}
|
|
998
|
+
const content = await response.text();
|
|
999
|
+
results.push({ filename: attachment.name, content });
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
logger5.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
|
|
934
1002
|
}
|
|
935
|
-
return textBlocks.join(`
|
|
936
|
-
`).trim();
|
|
937
1003
|
}
|
|
938
|
-
return
|
|
1004
|
+
return results;
|
|
939
1005
|
}
|
|
940
|
-
function
|
|
941
|
-
|
|
942
|
-
|
|
1006
|
+
async function readMediaAttachments(message) {
|
|
1007
|
+
const attachments = message.attachments;
|
|
1008
|
+
if (attachments.size === 0) {
|
|
1009
|
+
return [];
|
|
943
1010
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1011
|
+
const results = [];
|
|
1012
|
+
for (const [, attachment] of attachments) {
|
|
1013
|
+
if (!isSupportedMediaAttachment(attachment)) {
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (attachment.size > MAX_MEDIA_ATTACHMENT_SIZE_BYTES) {
|
|
1017
|
+
logger5.warn({
|
|
1018
|
+
messageId: message.id,
|
|
1019
|
+
filename: attachment.name,
|
|
1020
|
+
size: attachment.size
|
|
1021
|
+
}, "media attachment too large, skipping");
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
logger5.info({
|
|
1026
|
+
messageId: message.id,
|
|
1027
|
+
filename: attachment.name,
|
|
1028
|
+
size: attachment.size
|
|
1029
|
+
}, "fetching media attachment");
|
|
1030
|
+
const response = await fetch(attachment.url);
|
|
1031
|
+
if (!response.ok) {
|
|
1032
|
+
logger5.warn({
|
|
1033
|
+
messageId: message.id,
|
|
1034
|
+
filename: attachment.name,
|
|
1035
|
+
status: response.status
|
|
1036
|
+
}, "failed to fetch media attachment");
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const buffer = await response.arrayBuffer();
|
|
1040
|
+
results.push({
|
|
1041
|
+
filename: attachment.name,
|
|
1042
|
+
data: Buffer.from(buffer).toString("base64"),
|
|
1043
|
+
mimeType: attachment.contentType ?? "application/octet-stream"
|
|
1044
|
+
});
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
logger5.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return results;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/discord-auth.ts
|
|
1053
|
+
import { ChannelType } from "discord.js";
|
|
1054
|
+
function getAuthorDisplayName(message) {
|
|
1055
|
+
return message.member?.displayName || message.author.globalName || message.author.username;
|
|
1056
|
+
}
|
|
1057
|
+
function resolveMessageScope(message) {
|
|
1058
|
+
if (message.channel.type === ChannelType.DM) {
|
|
1059
|
+
return "dm";
|
|
1060
|
+
}
|
|
1061
|
+
if (message.channel.isThread()) {
|
|
1062
|
+
return `thread:${message.channel.id}`;
|
|
1063
|
+
}
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
function isAuthorizedMessage(message, scope, authConfig) {
|
|
1067
|
+
if (scope === "dm") {
|
|
1068
|
+
return message.author.id === authConfig.discordAllowedUserId;
|
|
1069
|
+
}
|
|
1070
|
+
if (scope.startsWith("thread:")) {
|
|
1071
|
+
const channel = message.channel;
|
|
1072
|
+
if (!channel.isThread()) {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
const parentId = channel.parentId;
|
|
1076
|
+
if (!parentId || !authConfig.discordAllowedForumChannelIds.includes(parentId)) {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
return authConfig.discordAllowedUserIds.includes(message.author.id);
|
|
1080
|
+
}
|
|
1081
|
+
return false;
|
|
948
1082
|
}
|
|
949
1083
|
|
|
950
1084
|
// src/message-chunker.ts
|
|
@@ -978,102 +1112,8 @@ function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
|
|
|
978
1112
|
return chunks.map((chunk) => chunk.slice(0, DISCORD_MESSAGE_LIMIT));
|
|
979
1113
|
}
|
|
980
1114
|
|
|
981
|
-
// src/
|
|
982
|
-
|
|
983
|
-
const contextEntries = [
|
|
984
|
-
["scope", options.scope],
|
|
985
|
-
["sent_at", options.sentAt],
|
|
986
|
-
["sent_at_local", options.sentAtLocal],
|
|
987
|
-
["message_id", options.messageId],
|
|
988
|
-
["author_name", normalizeContextValue(options.authorName)],
|
|
989
|
-
["author_id", options.authorId],
|
|
990
|
-
["thread_title", normalizeContextValue(options.threadTitle)],
|
|
991
|
-
["thread_id", options.threadId],
|
|
992
|
-
["forum_channel_id", options.forumChannelId ?? undefined]
|
|
993
|
-
].filter((entry) => {
|
|
994
|
-
return typeof entry[1] === "string" && entry[1].trim().length > 0;
|
|
995
|
-
});
|
|
996
|
-
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
997
|
-
return [
|
|
998
|
-
"<discord_message_context>",
|
|
999
|
-
contextJson,
|
|
1000
|
-
"</discord_message_context>",
|
|
1001
|
-
"",
|
|
1002
|
-
"User message:",
|
|
1003
|
-
userMessage.trim()
|
|
1004
|
-
].join(`
|
|
1005
|
-
`);
|
|
1006
|
-
}
|
|
1007
|
-
function formatDiscordPromptTime(date, options = {}) {
|
|
1008
|
-
const timeZone = options.timeZone || "UTC";
|
|
1009
|
-
const locale = options.locale || "en-AU";
|
|
1010
|
-
return new Intl.DateTimeFormat(locale, {
|
|
1011
|
-
timeZone,
|
|
1012
|
-
weekday: "short",
|
|
1013
|
-
day: "numeric",
|
|
1014
|
-
month: "short",
|
|
1015
|
-
year: "2-digit",
|
|
1016
|
-
hour: "2-digit",
|
|
1017
|
-
minute: "2-digit",
|
|
1018
|
-
hour12: false,
|
|
1019
|
-
timeZoneName: "short"
|
|
1020
|
-
}).format(date);
|
|
1021
|
-
}
|
|
1022
|
-
function normalizeContextValue(value) {
|
|
1023
|
-
if (value === undefined) {
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
return value.replace(/\s+/g, " ").trim();
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// src/discord-gateway-client.ts
|
|
1030
|
-
var logger6 = createModuleLogger("discord-gateway");
|
|
1031
|
-
function getAuthorDisplayName(message) {
|
|
1032
|
-
return message.member?.displayName || message.author.globalName || message.author.username;
|
|
1033
|
-
}
|
|
1034
|
-
function buildDiscordPromptContent(message, scope, content, config) {
|
|
1035
|
-
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
1036
|
-
return buildDiscordMessageContextPrompt(content, {
|
|
1037
|
-
scope: scope === "dm" ? "dm" : "thread",
|
|
1038
|
-
sentAt: message.createdAt.toISOString(),
|
|
1039
|
-
sentAtLocal: formatDiscordPromptTime(message.createdAt, {
|
|
1040
|
-
timeZone: config.promptTimeZone,
|
|
1041
|
-
locale: config.promptLocale
|
|
1042
|
-
}),
|
|
1043
|
-
messageId: message.id,
|
|
1044
|
-
authorId: message.author.id,
|
|
1045
|
-
authorName: getAuthorDisplayName(message),
|
|
1046
|
-
threadId: isThread ? message.channel.id : undefined,
|
|
1047
|
-
threadTitle: isThread ? message.channel.name : undefined,
|
|
1048
|
-
forumChannelId: isThread ? message.channel.parentId : undefined
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
1051
|
-
function resolveScope(message) {
|
|
1052
|
-
if (message.channel.type === ChannelType.DM) {
|
|
1053
|
-
return "dm";
|
|
1054
|
-
}
|
|
1055
|
-
if (message.channel.isThread()) {
|
|
1056
|
-
return `thread:${message.channel.id}`;
|
|
1057
|
-
}
|
|
1058
|
-
return null;
|
|
1059
|
-
}
|
|
1060
|
-
function isAuthorized(message, scope, authConfig) {
|
|
1061
|
-
if (scope === "dm") {
|
|
1062
|
-
return message.author.id === authConfig.discordAllowedUserId;
|
|
1063
|
-
}
|
|
1064
|
-
if (scope.startsWith("thread:")) {
|
|
1065
|
-
const channel = message.channel;
|
|
1066
|
-
if (!channel.isThread()) {
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
const parentId = channel.parentId;
|
|
1070
|
-
if (!parentId || !authConfig.discordAllowedForumChannelIds.includes(parentId)) {
|
|
1071
|
-
return false;
|
|
1072
|
-
}
|
|
1073
|
-
return authConfig.discordAllowedUserIds.includes(message.author.id);
|
|
1074
|
-
}
|
|
1075
|
-
return false;
|
|
1076
|
-
}
|
|
1115
|
+
// src/discord-replies.ts
|
|
1116
|
+
var logger6 = createModuleLogger("discord-replies");
|
|
1077
1117
|
var WORKING_EMOJI = "⚙️";
|
|
1078
1118
|
async function addWorkingReaction(message) {
|
|
1079
1119
|
try {
|
|
@@ -1092,72 +1132,6 @@ async function removeWorkingReaction(message) {
|
|
|
1092
1132
|
logger6.debug({ messageId: message.id, error }, "failed to remove working reaction");
|
|
1093
1133
|
}
|
|
1094
1134
|
}
|
|
1095
|
-
var TYPING_INTERVAL_MS = 9000;
|
|
1096
|
-
var typingIntervals = new Map;
|
|
1097
|
-
async function sendTypingSafe(channel, channelKey) {
|
|
1098
|
-
try {
|
|
1099
|
-
const token = channel.client.token;
|
|
1100
|
-
const url = `https://discord.com/api/v10/channels/${channel.id}/typing`;
|
|
1101
|
-
const res = await fetch(url, {
|
|
1102
|
-
method: "POST",
|
|
1103
|
-
headers: { Authorization: `Bot ${token}` }
|
|
1104
|
-
});
|
|
1105
|
-
if (res.ok) {
|
|
1106
|
-
logger6.debug(`[TYPING] STATUS UPDATED OK`);
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
if (res.status === 429) {
|
|
1110
|
-
const body = await res.text();
|
|
1111
|
-
let retryMs = 3000;
|
|
1112
|
-
try {
|
|
1113
|
-
const parsed = JSON.parse(body);
|
|
1114
|
-
if (typeof parsed.retry_after === "number") {
|
|
1115
|
-
retryMs = parsed.retry_after * 1000 + 500;
|
|
1116
|
-
}
|
|
1117
|
-
} catch {}
|
|
1118
|
-
logger6.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
|
|
1119
|
-
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
1120
|
-
await fetch(url, {
|
|
1121
|
-
method: "POST",
|
|
1122
|
-
headers: { Authorization: `Bot ${token}` }
|
|
1123
|
-
});
|
|
1124
|
-
logger6.info({ channelKey }, "[TYPING] retry done");
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
logger6.warn({ channelKey, status: res.status }, "[TYPING] unexpected status");
|
|
1128
|
-
} catch (error) {
|
|
1129
|
-
logger6.warn({ channelKey, error }, "[TYPING] FAILED");
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
function startTypingForChannel(channel, channelKey) {
|
|
1133
|
-
const existing = typingIntervals.get(channelKey);
|
|
1134
|
-
if (existing) {
|
|
1135
|
-
existing.refs += 1;
|
|
1136
|
-
logger6.debug({ channelKey, refs: existing.refs }, "[TYPING] ref++ (reusing existing interval)");
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
logger6.debug("[TYPING] started new interval");
|
|
1140
|
-
sendTypingSafe(channel, channelKey);
|
|
1141
|
-
const interval = setInterval(() => {
|
|
1142
|
-
sendTypingSafe(channel, channelKey);
|
|
1143
|
-
}, TYPING_INTERVAL_MS);
|
|
1144
|
-
typingIntervals.set(channelKey, { interval, refs: 1 });
|
|
1145
|
-
}
|
|
1146
|
-
function stopTypingForChannel(channelKey) {
|
|
1147
|
-
const entry = typingIntervals.get(channelKey);
|
|
1148
|
-
if (!entry) {
|
|
1149
|
-
logger6.debug({ channelKey }, "[TYPING] stop called but no entry found");
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
entry.refs -= 1;
|
|
1153
|
-
if (entry.refs <= 0) {
|
|
1154
|
-
clearInterval(entry.interval);
|
|
1155
|
-
typingIntervals.delete(channelKey);
|
|
1156
|
-
logger6.debug("[TYPING] interval cleared (refs hit 0)");
|
|
1157
|
-
} else {
|
|
1158
|
-
logger6.debug("[TYPING] ref-- (interval still active)");
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
1135
|
async function sendReply(message, text) {
|
|
1162
1136
|
const channel = message.channel;
|
|
1163
1137
|
if (!channel.isSendable()) {
|
|
@@ -1183,146 +1157,76 @@ async function sendReply(message, text) {
|
|
|
1183
1157
|
}, "send reply failed");
|
|
1184
1158
|
}
|
|
1185
1159
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
return [];
|
|
1204
|
-
}
|
|
1205
|
-
const results = [];
|
|
1206
|
-
for (const [, attachment] of attachments) {
|
|
1207
|
-
const ext = attachment.name?.slice(attachment.name.lastIndexOf(".")).toLowerCase();
|
|
1208
|
-
if (!ext || !TEXT_ATTACHMENT_EXTENSIONS.includes(ext)) {
|
|
1209
|
-
logger6.debug({ messageId: message.id, filename: attachment.name, ext }, "skipping non-text attachment");
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
if (attachment.size > MAX_ATTACHMENT_SIZE_BYTES) {
|
|
1213
|
-
logger6.warn({
|
|
1214
|
-
messageId: message.id,
|
|
1215
|
-
filename: attachment.name,
|
|
1216
|
-
size: attachment.size
|
|
1217
|
-
}, "attachment too large, skipping");
|
|
1218
|
-
continue;
|
|
1219
|
-
}
|
|
1220
|
-
try {
|
|
1221
|
-
logger6.info({
|
|
1222
|
-
messageId: message.id,
|
|
1223
|
-
filename: attachment.name,
|
|
1224
|
-
size: attachment.size
|
|
1225
|
-
}, "fetching attachment");
|
|
1226
|
-
const response = await fetch(attachment.url);
|
|
1227
|
-
if (!response.ok) {
|
|
1228
|
-
logger6.warn({
|
|
1229
|
-
messageId: message.id,
|
|
1230
|
-
filename: attachment.name,
|
|
1231
|
-
status: response.status
|
|
1232
|
-
}, "failed to fetch attachment");
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1235
|
-
const content = await response.text();
|
|
1236
|
-
results.push({ filename: attachment.name, content });
|
|
1237
|
-
} catch (error) {
|
|
1238
|
-
logger6.error({ messageId: message.id, filename: attachment.name, error }, "error fetching attachment");
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
return results;
|
|
1242
|
-
}
|
|
1243
|
-
var MEDIA_ATTACHMENT_EXTENSIONS = [
|
|
1244
|
-
".png",
|
|
1245
|
-
".jpg",
|
|
1246
|
-
".jpeg",
|
|
1247
|
-
".gif",
|
|
1248
|
-
".webp",
|
|
1249
|
-
".pdf",
|
|
1250
|
-
".docx",
|
|
1251
|
-
".doc",
|
|
1252
|
-
".pptx",
|
|
1253
|
-
".ppt",
|
|
1254
|
-
".xlsx",
|
|
1255
|
-
".xls"
|
|
1256
|
-
];
|
|
1257
|
-
var MAX_MEDIA_ATTACHMENT_SIZE = 25 * 1024 * 1024;
|
|
1258
|
-
var OFFICE_MIME_TYPES = new Set([
|
|
1259
|
-
"application/pdf",
|
|
1260
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1261
|
-
"application/msword",
|
|
1262
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1263
|
-
"application/vnd.ms-powerpoint",
|
|
1264
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1265
|
-
"application/vnd.ms-excel"
|
|
1266
|
-
]);
|
|
1267
|
-
function isMediaAttachment(attachment) {
|
|
1268
|
-
const ext = attachment.name?.slice(attachment.name.lastIndexOf(".")).toLowerCase();
|
|
1269
|
-
if (!ext || !MEDIA_ATTACHMENT_EXTENSIONS.includes(ext)) {
|
|
1270
|
-
return false;
|
|
1271
|
-
}
|
|
1272
|
-
const ct = attachment.contentType;
|
|
1273
|
-
if (!ct) {
|
|
1274
|
-
return false;
|
|
1160
|
+
|
|
1161
|
+
// src/image-description.ts
|
|
1162
|
+
var logger7 = createModuleLogger("image-description");
|
|
1163
|
+
async function describeMediaAttachment(agentService, imageData, mimeType, userText, visionModel) {
|
|
1164
|
+
const session = await agentService.createTemporarySession();
|
|
1165
|
+
await session.setModel(visionModel);
|
|
1166
|
+
const mediaType = getMediaType(mimeType);
|
|
1167
|
+
const imageContent = {
|
|
1168
|
+
type: "image",
|
|
1169
|
+
data: imageData,
|
|
1170
|
+
mimeType
|
|
1171
|
+
};
|
|
1172
|
+
let promptText;
|
|
1173
|
+
if (mediaType === "document") {
|
|
1174
|
+
promptText = userText.trim().length > 0 ? `The user sent a document with the following message: "${userText}". Please extract and summarize the text content of this document. Be thorough — include all important details, sections, and data from the document.` : "Please extract and summarize the text content of this document. Be thorough — include all important details, sections, data, and key points.";
|
|
1175
|
+
} else {
|
|
1176
|
+
promptText = userText.trim().length > 0 ? `The user sent this image with the following message: "${userText}". Please describe the image in detail and address any questions from the user's message.` : "Please describe this image in detail. What do you see?";
|
|
1275
1177
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1178
|
+
let text = "";
|
|
1179
|
+
try {
|
|
1180
|
+
await session.prompt(promptText, { images: [imageContent] });
|
|
1181
|
+
text = extractLastAssistantText(session);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
logger7.error({ error, mimeType }, "vision model prompt failed");
|
|
1184
|
+
text = "(Vision model failed to process the file.)";
|
|
1185
|
+
} finally {
|
|
1186
|
+
session.dispose();
|
|
1282
1187
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1188
|
+
if (!text) {
|
|
1189
|
+
return "(Vision model returned no description.)";
|
|
1190
|
+
}
|
|
1191
|
+
logger7.debug({ textLength: text.length, mimeType }, "media described");
|
|
1192
|
+
return text;
|
|
1193
|
+
}
|
|
1194
|
+
function extractLastAssistantText(session) {
|
|
1195
|
+
const messages = session.messages;
|
|
1196
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
1197
|
+
const msg = messages[i];
|
|
1198
|
+
if (!msg || !isAssistantMessage(msg)) {
|
|
1286
1199
|
continue;
|
|
1287
1200
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
messageId: message.id,
|
|
1291
|
-
filename: attachment.name,
|
|
1292
|
-
size: attachment.size
|
|
1293
|
-
}, "media attachment too large, skipping");
|
|
1201
|
+
const content = msg.content;
|
|
1202
|
+
if (!Array.isArray(content)) {
|
|
1294
1203
|
continue;
|
|
1295
1204
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
size: attachment.size
|
|
1301
|
-
}, "fetching media attachment");
|
|
1302
|
-
const response = await fetch(attachment.url);
|
|
1303
|
-
if (!response.ok) {
|
|
1304
|
-
logger6.warn({
|
|
1305
|
-
messageId: message.id,
|
|
1306
|
-
filename: attachment.name,
|
|
1307
|
-
status: response.status
|
|
1308
|
-
}, "failed to fetch media attachment");
|
|
1309
|
-
continue;
|
|
1205
|
+
const textBlocks = [];
|
|
1206
|
+
for (const item of content) {
|
|
1207
|
+
if (typeof item === "object" && item !== null && "type" in item && item.type === "text") {
|
|
1208
|
+
textBlocks.push(item.text);
|
|
1310
1209
|
}
|
|
1311
|
-
const buffer = await response.arrayBuffer();
|
|
1312
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
1313
|
-
results.push({
|
|
1314
|
-
filename: attachment.name,
|
|
1315
|
-
data: base64,
|
|
1316
|
-
mimeType: attachment.contentType ?? "application/octet-stream"
|
|
1317
|
-
});
|
|
1318
|
-
} catch (error) {
|
|
1319
|
-
logger6.error({ messageId: message.id, filename: attachment.name, error }, "error fetching media attachment");
|
|
1320
1210
|
}
|
|
1211
|
+
return textBlocks.join(`
|
|
1212
|
+
`).trim();
|
|
1321
1213
|
}
|
|
1322
|
-
return
|
|
1214
|
+
return "";
|
|
1215
|
+
}
|
|
1216
|
+
function getMediaType(mimeType) {
|
|
1217
|
+
if (mimeType.startsWith("image/")) {
|
|
1218
|
+
return "image";
|
|
1219
|
+
}
|
|
1220
|
+
return "document";
|
|
1221
|
+
}
|
|
1222
|
+
function isAssistantMessage(msg) {
|
|
1223
|
+
return typeof msg === "object" && msg !== null && "role" in msg && msg.role === "assistant";
|
|
1323
1224
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1225
|
+
|
|
1226
|
+
// src/discord-media-resolution.ts
|
|
1227
|
+
var logger8 = createModuleLogger("discord-media-resolution");
|
|
1228
|
+
function parseProviderModelId(value) {
|
|
1229
|
+
const trimmed = value.trim();
|
|
1326
1230
|
if (!trimmed) {
|
|
1327
1231
|
return null;
|
|
1328
1232
|
}
|
|
@@ -1335,7 +1239,7 @@ function parseVisionModelId(visionModelId) {
|
|
|
1335
1239
|
modelId: trimmed.substring(slashIndex + 1)
|
|
1336
1240
|
};
|
|
1337
1241
|
}
|
|
1338
|
-
function
|
|
1242
|
+
function getMediaAttachmentLabel(filename, mimeType) {
|
|
1339
1243
|
if (mimeType === "application/pdf") {
|
|
1340
1244
|
return `[PDF: ${filename}]`;
|
|
1341
1245
|
}
|
|
@@ -1350,131 +1254,234 @@ function getMediaLabel(filename, mimeType) {
|
|
|
1350
1254
|
}
|
|
1351
1255
|
return `[Image: ${filename}]`;
|
|
1352
1256
|
}
|
|
1353
|
-
async function
|
|
1257
|
+
async function resolveMediaAttachmentsForPrompt(mediaAttachments, content, currentModel, config, agentService) {
|
|
1354
1258
|
const modelSupportsVision = currentModel?.input.includes("image") ?? false;
|
|
1355
1259
|
if (modelSupportsVision) {
|
|
1356
|
-
const names =
|
|
1357
|
-
|
|
1358
|
-
count:
|
|
1260
|
+
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1261
|
+
logger8.info({
|
|
1262
|
+
count: mediaAttachments.length,
|
|
1359
1263
|
filenames: names,
|
|
1360
1264
|
model: currentModel ? `${currentModel.provider}/${currentModel.id}` : "none"
|
|
1361
1265
|
}, "passing media natively to vision-capable model");
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1266
|
+
return {
|
|
1267
|
+
content,
|
|
1268
|
+
images: mediaAttachments.map((media) => ({
|
|
1269
|
+
type: "image",
|
|
1270
|
+
data: media.data,
|
|
1271
|
+
mimeType: media.mimeType
|
|
1272
|
+
}))
|
|
1273
|
+
};
|
|
1368
1274
|
}
|
|
1369
1275
|
if (!config.visionModelId) {
|
|
1370
|
-
const names =
|
|
1371
|
-
|
|
1276
|
+
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1277
|
+
logger8.info({ filenames: names }, "media attachments received but vision model not configured");
|
|
1372
1278
|
const note = `
|
|
1373
1279
|
|
|
1374
1280
|
[User sent media attachment(s): ${names}]
|
|
1375
1281
|
` + "(Media vision not configured. Set visionModelId to enable image/PDF/document understanding.)";
|
|
1376
|
-
return {
|
|
1282
|
+
return {
|
|
1283
|
+
content: content ? content + note : note,
|
|
1284
|
+
images: []
|
|
1285
|
+
};
|
|
1377
1286
|
}
|
|
1378
|
-
const
|
|
1379
|
-
if (!
|
|
1287
|
+
const parsedVisionModelId = parseProviderModelId(config.visionModelId);
|
|
1288
|
+
if (!parsedVisionModelId) {
|
|
1380
1289
|
return { content, images: [] };
|
|
1381
1290
|
}
|
|
1382
|
-
const visionModel = agentService.findModel(
|
|
1291
|
+
const visionModel = agentService.findModel(parsedVisionModelId.provider, parsedVisionModelId.modelId);
|
|
1383
1292
|
if (!visionModel) {
|
|
1384
|
-
|
|
1385
|
-
const names =
|
|
1293
|
+
logger8.warn({ visionModelId: config.visionModelId }, "vision model not found in registry");
|
|
1294
|
+
const names = mediaAttachments.map((media) => media.filename).join(", ");
|
|
1386
1295
|
const note = `
|
|
1387
1296
|
|
|
1388
1297
|
[User sent media attachment(s): ${names}]
|
|
1389
|
-
(Vision model not found: ${config.visionModelId})`;
|
|
1390
|
-
return {
|
|
1298
|
+
` + `(Vision model not found: ${config.visionModelId})`;
|
|
1299
|
+
return {
|
|
1300
|
+
content: content ? content + note : note,
|
|
1301
|
+
images: []
|
|
1302
|
+
};
|
|
1391
1303
|
}
|
|
1392
|
-
|
|
1393
|
-
count:
|
|
1304
|
+
logger8.info({
|
|
1305
|
+
count: mediaAttachments.length,
|
|
1394
1306
|
visionModel: `${visionModel.provider}/${visionModel.id}`
|
|
1395
1307
|
}, "describing media with vision model");
|
|
1396
1308
|
const descriptions = [];
|
|
1397
|
-
for (const
|
|
1398
|
-
const description = await
|
|
1399
|
-
const label =
|
|
1309
|
+
for (const media of mediaAttachments) {
|
|
1310
|
+
const description = await describeMediaAttachment(agentService, media.data, media.mimeType, content, visionModel);
|
|
1311
|
+
const label = getMediaAttachmentLabel(media.filename, media.mimeType);
|
|
1400
1312
|
descriptions.push(`${label}
|
|
1401
1313
|
${description}`);
|
|
1402
1314
|
}
|
|
1403
|
-
if (descriptions.length
|
|
1404
|
-
|
|
1315
|
+
if (descriptions.length === 0) {
|
|
1316
|
+
return { content, images: [] };
|
|
1317
|
+
}
|
|
1318
|
+
const descriptionPrefix = descriptions.join(`
|
|
1405
1319
|
|
|
1406
1320
|
`);
|
|
1407
|
-
|
|
1408
|
-
|
|
1321
|
+
return {
|
|
1322
|
+
content: content ? `${descriptionPrefix}
|
|
1409
1323
|
|
|
1410
1324
|
---
|
|
1411
|
-
${content}` :
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
}
|
|
1415
|
-
return { content, images: [] };
|
|
1325
|
+
${content}` : descriptionPrefix,
|
|
1326
|
+
images: []
|
|
1327
|
+
};
|
|
1416
1328
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1329
|
+
|
|
1330
|
+
// src/discord-typing.ts
|
|
1331
|
+
var logger9 = createModuleLogger("discord-typing");
|
|
1332
|
+
var TYPING_INTERVAL_MS = 9000;
|
|
1333
|
+
var typingIntervals = new Map;
|
|
1334
|
+
async function sendTypingSafe(channel, channelKey) {
|
|
1335
|
+
try {
|
|
1336
|
+
const token = channel.client.token;
|
|
1337
|
+
const url = `https://discord.com/api/v10/channels/${channel.id}/typing`;
|
|
1338
|
+
const response = await fetch(url, {
|
|
1339
|
+
method: "POST",
|
|
1340
|
+
headers: { Authorization: `Bot ${token}` }
|
|
1341
|
+
});
|
|
1342
|
+
if (response.ok) {
|
|
1343
|
+
logger9.debug("[TYPING] STATUS UPDATED OK");
|
|
1430
1344
|
return;
|
|
1431
1345
|
}
|
|
1432
|
-
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1346
|
+
if (response.status === 429) {
|
|
1347
|
+
const body = await response.text();
|
|
1348
|
+
let retryMs = 3000;
|
|
1349
|
+
try {
|
|
1350
|
+
const parsed = JSON.parse(body);
|
|
1351
|
+
if (typeof parsed.retry_after === "number") {
|
|
1352
|
+
retryMs = parsed.retry_after * 1000 + 500;
|
|
1353
|
+
}
|
|
1354
|
+
} catch {}
|
|
1355
|
+
logger9.warn({ channelKey, retryMs, response: body }, `[TYPING] 429, retrying after ${retryMs}ms delay`);
|
|
1356
|
+
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
1357
|
+
await fetch(url, {
|
|
1358
|
+
method: "POST",
|
|
1359
|
+
headers: { Authorization: `Bot ${token}` }
|
|
1360
|
+
});
|
|
1361
|
+
logger9.info({ channelKey }, "[TYPING] retry done");
|
|
1362
|
+
return;
|
|
1449
1363
|
}
|
|
1364
|
+
logger9.warn({ channelKey, status: response.status }, "[TYPING] unexpected status");
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
logger9.warn({ channelKey, error }, "[TYPING] FAILED");
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function startTypingForChannel(channel, channelKey) {
|
|
1370
|
+
const existing = typingIntervals.get(channelKey);
|
|
1371
|
+
if (existing) {
|
|
1372
|
+
existing.refs += 1;
|
|
1373
|
+
logger9.debug({ channelKey, refs: existing.refs }, "[TYPING] ref++ (reusing existing interval)");
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
logger9.debug("[TYPING] started new interval");
|
|
1377
|
+
sendTypingSafe(channel, channelKey);
|
|
1378
|
+
const interval = setInterval(() => {
|
|
1379
|
+
sendTypingSafe(channel, channelKey);
|
|
1380
|
+
}, TYPING_INTERVAL_MS);
|
|
1381
|
+
typingIntervals.set(channelKey, { interval, refs: 1 });
|
|
1382
|
+
}
|
|
1383
|
+
function stopTypingForChannel(channelKey) {
|
|
1384
|
+
const entry = typingIntervals.get(channelKey);
|
|
1385
|
+
if (!entry) {
|
|
1386
|
+
logger9.debug({ channelKey }, "[TYPING] stop called but no entry found");
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
entry.refs -= 1;
|
|
1390
|
+
if (entry.refs <= 0) {
|
|
1391
|
+
clearInterval(entry.interval);
|
|
1392
|
+
typingIntervals.delete(channelKey);
|
|
1393
|
+
logger9.debug("[TYPING] interval cleared (refs hit 0)");
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
logger9.debug("[TYPING] ref-- (interval still active)");
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/prompt-context.ts
|
|
1400
|
+
function buildDiscordMessageContextPrompt(userMessage, options) {
|
|
1401
|
+
const contextEntries = [
|
|
1402
|
+
["scope", options.scope],
|
|
1403
|
+
["sent_at", options.sentAt],
|
|
1404
|
+
["sent_at_local", options.sentAtLocal],
|
|
1405
|
+
["message_id", options.messageId],
|
|
1406
|
+
["author_name", normalizeContextValue(options.authorName)],
|
|
1407
|
+
["author_id", options.authorId],
|
|
1408
|
+
["thread_title", normalizeContextValue(options.threadTitle)],
|
|
1409
|
+
["thread_id", options.threadId],
|
|
1410
|
+
["forum_channel_id", options.forumChannelId ?? undefined]
|
|
1411
|
+
].filter((entry) => {
|
|
1412
|
+
return typeof entry[1] === "string" && entry[1].trim().length > 0;
|
|
1450
1413
|
});
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1414
|
+
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
1415
|
+
return [
|
|
1416
|
+
"<discord_message_context>",
|
|
1417
|
+
contextJson,
|
|
1418
|
+
"</discord_message_context>",
|
|
1419
|
+
"",
|
|
1420
|
+
"User message:",
|
|
1421
|
+
userMessage.trim()
|
|
1422
|
+
].join(`
|
|
1423
|
+
`);
|
|
1424
|
+
}
|
|
1425
|
+
function formatDiscordPromptTime(date, options = {}) {
|
|
1426
|
+
const timeZone = options.timeZone || "UTC";
|
|
1427
|
+
const locale = options.locale || "en-AU";
|
|
1428
|
+
return new Intl.DateTimeFormat(locale, {
|
|
1429
|
+
timeZone,
|
|
1430
|
+
weekday: "short",
|
|
1431
|
+
day: "numeric",
|
|
1432
|
+
month: "short",
|
|
1433
|
+
year: "2-digit",
|
|
1434
|
+
hour: "2-digit",
|
|
1435
|
+
minute: "2-digit",
|
|
1436
|
+
hour12: false,
|
|
1437
|
+
timeZoneName: "short"
|
|
1438
|
+
}).format(date);
|
|
1439
|
+
}
|
|
1440
|
+
function normalizeContextValue(value) {
|
|
1441
|
+
if (value === undefined) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/discord-message-handler.ts
|
|
1448
|
+
var logger10 = createModuleLogger("discord-message-handler");
|
|
1449
|
+
function buildDiscordPromptContent(message, scope, content, config) {
|
|
1450
|
+
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
1451
|
+
return buildDiscordMessageContextPrompt(content, {
|
|
1452
|
+
scope: scope === "dm" ? "dm" : "thread",
|
|
1453
|
+
sentAt: message.createdAt.toISOString(),
|
|
1454
|
+
sentAtLocal: formatDiscordPromptTime(message.createdAt, {
|
|
1455
|
+
timeZone: config.promptTimeZone,
|
|
1456
|
+
locale: config.promptLocale
|
|
1457
|
+
}),
|
|
1458
|
+
messageId: message.id,
|
|
1459
|
+
authorId: message.author.id,
|
|
1460
|
+
authorName: getAuthorDisplayName(message),
|
|
1461
|
+
threadId: isThread ? message.channel.id : undefined,
|
|
1462
|
+
threadTitle: isThread ? message.channel.name : undefined,
|
|
1463
|
+
forumChannelId: isThread ? message.channel.parentId : undefined
|
|
1455
1464
|
});
|
|
1456
|
-
await client.login(config.discordBotToken);
|
|
1457
|
-
return client;
|
|
1458
1465
|
}
|
|
1459
|
-
async function
|
|
1466
|
+
async function handleDiscordMessage(message, config, agentService, sessionRegistry, authConfig) {
|
|
1460
1467
|
if (message.author.bot) {
|
|
1461
|
-
|
|
1468
|
+
logger10.debug("ignored bot message");
|
|
1462
1469
|
return;
|
|
1463
1470
|
}
|
|
1464
1471
|
if (message.system) {
|
|
1465
|
-
|
|
1472
|
+
logger10.debug({ messageId: message.id }, "ignored system message");
|
|
1466
1473
|
return;
|
|
1467
1474
|
}
|
|
1468
|
-
const scope =
|
|
1475
|
+
const scope = resolveMessageScope(message);
|
|
1469
1476
|
if (scope === null) {
|
|
1470
|
-
|
|
1477
|
+
logger10.debug({
|
|
1471
1478
|
messageId: message.id,
|
|
1472
1479
|
channelType: message.channel.type
|
|
1473
1480
|
}, "unsupported channel type, ignoring");
|
|
1474
1481
|
return;
|
|
1475
1482
|
}
|
|
1476
|
-
if (!
|
|
1477
|
-
|
|
1483
|
+
if (!isAuthorizedMessage(message, scope, authConfig)) {
|
|
1484
|
+
logger10.debug({
|
|
1478
1485
|
messageId: message.id,
|
|
1479
1486
|
authorId: message.author.id,
|
|
1480
1487
|
scope
|
|
@@ -1482,20 +1489,22 @@ async function onMessage(message, config, agentService, sessionRegistry, authCon
|
|
|
1482
1489
|
return;
|
|
1483
1490
|
}
|
|
1484
1491
|
let content = message.content.trim();
|
|
1485
|
-
const
|
|
1486
|
-
if (
|
|
1487
|
-
const
|
|
1492
|
+
const textAttachments = await readTextAttachments(message);
|
|
1493
|
+
if (textAttachments.length > 0) {
|
|
1494
|
+
const attachmentSuffix = textAttachments.map((attachment) => {
|
|
1495
|
+
return `
|
|
1488
1496
|
|
|
1489
|
-
--- Attachment: ${
|
|
1490
|
-
${
|
|
1491
|
-
|
|
1497
|
+
--- Attachment: ${attachment.filename} ---
|
|
1498
|
+
${attachment.content}`;
|
|
1499
|
+
}).join("");
|
|
1500
|
+
content = content ? content + attachmentSuffix : textAttachments[0].content;
|
|
1492
1501
|
}
|
|
1493
1502
|
const mediaAttachments = await readMediaAttachments(message);
|
|
1494
1503
|
if (!content && mediaAttachments.length === 0) {
|
|
1495
|
-
|
|
1504
|
+
logger10.debug({ messageId: message.id }, "ignored empty message (no text or images)");
|
|
1496
1505
|
return;
|
|
1497
1506
|
}
|
|
1498
|
-
|
|
1507
|
+
logger10.info({
|
|
1499
1508
|
direction: "IN",
|
|
1500
1509
|
scope,
|
|
1501
1510
|
messageId: message.id,
|
|
@@ -1504,19 +1513,18 @@ ${a.content}`).join("");
|
|
|
1504
1513
|
content
|
|
1505
1514
|
}, "message received");
|
|
1506
1515
|
const channelKey = message.channel.id;
|
|
1507
|
-
|
|
1508
|
-
if (canSend) {
|
|
1516
|
+
if (message.channel.isSendable()) {
|
|
1509
1517
|
startTypingForChannel(message.channel, channelKey);
|
|
1510
1518
|
}
|
|
1511
1519
|
const { entry, created } = await sessionRegistry.getOrCreate(scope);
|
|
1512
1520
|
const { session, promptQueue } = entry;
|
|
1513
1521
|
if (created && scope.startsWith("thread:") && message.channel.isThread()) {
|
|
1514
|
-
|
|
1522
|
+
logger10.info({
|
|
1515
1523
|
scope,
|
|
1516
1524
|
threadName: message.channel.name
|
|
1517
1525
|
}, "new thread session");
|
|
1518
1526
|
}
|
|
1519
|
-
const commandResult = await
|
|
1527
|
+
const commandResult = await executeCommand(content, {
|
|
1520
1528
|
agentService,
|
|
1521
1529
|
promptQueue,
|
|
1522
1530
|
session
|
|
@@ -1524,7 +1532,7 @@ ${a.content}`).join("");
|
|
|
1524
1532
|
if (commandResult.handled) {
|
|
1525
1533
|
stopTypingForChannel(channelKey);
|
|
1526
1534
|
if (commandResult.archive && scope.startsWith("thread:")) {
|
|
1527
|
-
|
|
1535
|
+
logger10.info({ scope }, "archiving thread");
|
|
1528
1536
|
const archiveChannel = message.channel;
|
|
1529
1537
|
if (archiveChannel.isSendable()) {
|
|
1530
1538
|
await archiveChannel.send(commandResult.response ?? "Archiving...");
|
|
@@ -1534,12 +1542,12 @@ ${a.content}`).join("");
|
|
|
1534
1542
|
await archiveChannel.setArchived(true);
|
|
1535
1543
|
}
|
|
1536
1544
|
} catch (error) {
|
|
1537
|
-
|
|
1545
|
+
logger10.error({ error }, "failed to archive thread");
|
|
1538
1546
|
}
|
|
1539
1547
|
await sessionRegistry.remove(scope);
|
|
1540
1548
|
return;
|
|
1541
1549
|
}
|
|
1542
|
-
|
|
1550
|
+
logger10.info({
|
|
1543
1551
|
messageId: message.id,
|
|
1544
1552
|
command: content,
|
|
1545
1553
|
hasResponse: Boolean(commandResult.response)
|
|
@@ -1551,7 +1559,7 @@ ${a.content}`).join("");
|
|
|
1551
1559
|
}
|
|
1552
1560
|
if (!message.channel.isSendable()) {
|
|
1553
1561
|
stopTypingForChannel(channelKey);
|
|
1554
|
-
|
|
1562
|
+
logger10.debug({ messageId: message.id }, "channel not sendable");
|
|
1555
1563
|
return;
|
|
1556
1564
|
}
|
|
1557
1565
|
await addWorkingReaction(message);
|
|
@@ -1565,15 +1573,15 @@ ${a.content}`).join("");
|
|
|
1565
1573
|
let promptContent = content;
|
|
1566
1574
|
let promptImages;
|
|
1567
1575
|
if (mediaAttachments.length > 0) {
|
|
1568
|
-
const
|
|
1569
|
-
promptContent =
|
|
1570
|
-
if (
|
|
1571
|
-
promptImages =
|
|
1576
|
+
const resolvedPromptMedia = await resolveMediaAttachmentsForPrompt(mediaAttachments, promptContent, session.model, config, agentService);
|
|
1577
|
+
promptContent = resolvedPromptMedia.content;
|
|
1578
|
+
if (resolvedPromptMedia.images.length > 0) {
|
|
1579
|
+
promptImages = resolvedPromptMedia.images;
|
|
1572
1580
|
}
|
|
1573
1581
|
}
|
|
1574
1582
|
const wrappedContent = buildDiscordPromptContent(message, scope, promptContent, config);
|
|
1575
1583
|
const transformedPrompt = await config.promptTransform(wrappedContent);
|
|
1576
|
-
return
|
|
1584
|
+
return runPromptAndCollectReply(session, transformedPrompt, {
|
|
1577
1585
|
logPrefix: `[agent:${session.sessionId}]`,
|
|
1578
1586
|
images: promptImages
|
|
1579
1587
|
});
|
|
@@ -1585,6 +1593,51 @@ ${a.content}`).join("");
|
|
|
1585
1593
|
await sendReply(message, response);
|
|
1586
1594
|
}
|
|
1587
1595
|
|
|
1596
|
+
// src/discord-gateway-client.ts
|
|
1597
|
+
var logger11 = createModuleLogger("discord-gateway");
|
|
1598
|
+
async function startGatewayClient(config, agentService, sessionRegistry, authConfig) {
|
|
1599
|
+
const client = new Client({
|
|
1600
|
+
intents: [
|
|
1601
|
+
GatewayIntentBits.DirectMessages,
|
|
1602
|
+
GatewayIntentBits.Guilds,
|
|
1603
|
+
GatewayIntentBits.GuildMessages,
|
|
1604
|
+
GatewayIntentBits.MessageContent
|
|
1605
|
+
],
|
|
1606
|
+
partials: [Partials.Channel]
|
|
1607
|
+
});
|
|
1608
|
+
client.once(Events.ClientReady, async (readyClient) => {
|
|
1609
|
+
logger11.info({ userTag: readyClient.user.tag }, "logged in");
|
|
1610
|
+
if (!authConfig.startupMessage) {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
try {
|
|
1614
|
+
const user = await readyClient.users.fetch(authConfig.discordAllowedUserId);
|
|
1615
|
+
const dmChannel = await user.createDM();
|
|
1616
|
+
await dmChannel.send(authConfig.startupMessage);
|
|
1617
|
+
logger11.info({
|
|
1618
|
+
userId: authConfig.discordAllowedUserId
|
|
1619
|
+
}, "sent startup dm");
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
logger11.error({ error }, "failed to send startup dm");
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
client.on(Events.MessageCreate, async (message) => {
|
|
1625
|
+
try {
|
|
1626
|
+
await handleDiscordMessage(message, config, agentService, sessionRegistry, authConfig);
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
logger11.error({ error, direction: "IN" }, "message handling failed");
|
|
1629
|
+
await sendReply(message, "The bot hit an error while handling that message.");
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
client.on(Events.ThreadDelete, async (thread) => {
|
|
1633
|
+
const scope = `thread:${thread.id}`;
|
|
1634
|
+
logger11.info({ threadId: thread.id, scope }, "thread deleted");
|
|
1635
|
+
await sessionRegistry.remove(scope);
|
|
1636
|
+
});
|
|
1637
|
+
await client.login(config.discordBotToken);
|
|
1638
|
+
return client;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1588
1641
|
// src/session-registry.ts
|
|
1589
1642
|
import path3 from "node:path";
|
|
1590
1643
|
|
|
@@ -1601,7 +1654,7 @@ class PromptQueue {
|
|
|
1601
1654
|
reject(error);
|
|
1602
1655
|
}
|
|
1603
1656
|
});
|
|
1604
|
-
this.
|
|
1657
|
+
this.runNextTask();
|
|
1605
1658
|
});
|
|
1606
1659
|
}
|
|
1607
1660
|
getSnapshot() {
|
|
@@ -1610,7 +1663,7 @@ class PromptQueue {
|
|
|
1610
1663
|
busy: this.running
|
|
1611
1664
|
};
|
|
1612
1665
|
}
|
|
1613
|
-
|
|
1666
|
+
runNextTask() {
|
|
1614
1667
|
if (this.running) {
|
|
1615
1668
|
return;
|
|
1616
1669
|
}
|
|
@@ -1621,7 +1674,7 @@ class PromptQueue {
|
|
|
1621
1674
|
this.running = true;
|
|
1622
1675
|
next().finally(() => {
|
|
1623
1676
|
this.running = false;
|
|
1624
|
-
this.
|
|
1677
|
+
this.runNextTask();
|
|
1625
1678
|
});
|
|
1626
1679
|
}
|
|
1627
1680
|
}
|
|
@@ -1637,7 +1690,7 @@ function sessionDirForScope(agentDir, scope) {
|
|
|
1637
1690
|
}
|
|
1638
1691
|
throw new Error(`Unknown session scope: ${scope}`);
|
|
1639
1692
|
}
|
|
1640
|
-
var
|
|
1693
|
+
var logger12 = createModuleLogger("session-registry");
|
|
1641
1694
|
|
|
1642
1695
|
class SessionRegistry {
|
|
1643
1696
|
scopes = new Map;
|
|
@@ -1659,7 +1712,7 @@ class SessionRegistry {
|
|
|
1659
1712
|
createdAt: new Date
|
|
1660
1713
|
};
|
|
1661
1714
|
this.scopes.set(scope, entry);
|
|
1662
|
-
|
|
1715
|
+
logger12.debug({
|
|
1663
1716
|
scope,
|
|
1664
1717
|
sessionDir,
|
|
1665
1718
|
sessionId: session.sessionId
|
|
@@ -1671,7 +1724,7 @@ class SessionRegistry {
|
|
|
1671
1724
|
if (!entry) {
|
|
1672
1725
|
return;
|
|
1673
1726
|
}
|
|
1674
|
-
|
|
1727
|
+
logger12.debug({ scope }, "removing scope");
|
|
1675
1728
|
await entry.session.abort();
|
|
1676
1729
|
entry.session.dispose();
|
|
1677
1730
|
this.scopes.delete(scope);
|
|
@@ -1683,7 +1736,7 @@ class SessionRegistry {
|
|
|
1683
1736
|
return Array.from(this.scopes.keys());
|
|
1684
1737
|
}
|
|
1685
1738
|
async shutdownAll() {
|
|
1686
|
-
|
|
1739
|
+
logger12.info({ count: this.scopes.size }, "shutting down all scopes");
|
|
1687
1740
|
const scopes = Array.from(this.scopes.keys());
|
|
1688
1741
|
for (const scope of scopes) {
|
|
1689
1742
|
await this.remove(scope);
|
|
@@ -1692,13 +1745,13 @@ class SessionRegistry {
|
|
|
1692
1745
|
}
|
|
1693
1746
|
|
|
1694
1747
|
// src/index.ts
|
|
1695
|
-
var
|
|
1748
|
+
var logger13 = createModuleLogger("index");
|
|
1696
1749
|
async function startDiscordGateway(config) {
|
|
1697
|
-
const resolvedConfig =
|
|
1750
|
+
const resolvedConfig = resolveConfig(config);
|
|
1698
1751
|
const agentService = new AgentService(resolvedConfig);
|
|
1699
|
-
|
|
1752
|
+
logger13.info("initializing agent service");
|
|
1700
1753
|
await agentService.initialize();
|
|
1701
|
-
|
|
1754
|
+
logger13.info(agentService.getStatus(), "agent ready");
|
|
1702
1755
|
const authConfig = {
|
|
1703
1756
|
discordAllowedUserId: resolvedConfig.discordAllowedUserId,
|
|
1704
1757
|
discordAllowedForumChannelIds: resolvedConfig.discordAllowedForumChannelIds,
|
|
@@ -1719,9 +1772,6 @@ async function startDiscordGateway(config) {
|
|
|
1719
1772
|
}
|
|
1720
1773
|
};
|
|
1721
1774
|
}
|
|
1722
|
-
async function startDiscordPiBridge(config) {
|
|
1723
|
-
return startDiscordGateway(config);
|
|
1724
|
-
}
|
|
1725
1775
|
function createGatewayStopHandler(client, agentService, sessionRegistry, config) {
|
|
1726
1776
|
let stopped = false;
|
|
1727
1777
|
return async () => {
|
|
@@ -1729,7 +1779,7 @@ function createGatewayStopHandler(client, agentService, sessionRegistry, config)
|
|
|
1729
1779
|
return;
|
|
1730
1780
|
}
|
|
1731
1781
|
stopped = true;
|
|
1732
|
-
|
|
1782
|
+
logger13.info({
|
|
1733
1783
|
cwd: config.cwd,
|
|
1734
1784
|
agentDir: config.agentDir
|
|
1735
1785
|
}, "stopping discord gateway");
|
|
@@ -1740,9 +1790,9 @@ function createGatewayStopHandler(client, agentService, sessionRegistry, config)
|
|
|
1740
1790
|
}
|
|
1741
1791
|
function registerSignalHandlers(stop) {
|
|
1742
1792
|
const handleSignal = (signal) => {
|
|
1743
|
-
|
|
1793
|
+
logger13.info({ signal }, "received signal");
|
|
1744
1794
|
stop().finally(() => {
|
|
1745
|
-
|
|
1795
|
+
logger13.info("done");
|
|
1746
1796
|
process.exit(0);
|
|
1747
1797
|
});
|
|
1748
1798
|
};
|
|
@@ -1754,10 +1804,8 @@ function registerSignalHandlers(stop) {
|
|
|
1754
1804
|
});
|
|
1755
1805
|
}
|
|
1756
1806
|
export {
|
|
1757
|
-
startDiscordPiBridge,
|
|
1758
1807
|
startDiscordGateway,
|
|
1759
1808
|
resolveConfig,
|
|
1760
|
-
loadDiscordPiBridgeConfigFromEnv,
|
|
1761
1809
|
loadDiscordGatewayConfigFromEnv,
|
|
1762
1810
|
formatDiscordPromptTime,
|
|
1763
1811
|
buildDiscordMessageContextPrompt
|