@friendlyrobot/discord-pi-agent 0.5.9 → 0.6.1
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 +31 -7
- package/dist/discord-gateway-client.d.ts +0 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +81 -50
- package/dist/prompt-context.d.ts +15 -3
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/dist/discord-gateway-client.test.d.ts +0 -1
package/README.md
CHANGED
|
@@ -23,6 +23,31 @@ Reusable Discord gateway bridge for persistent pi agent sessions — DM and foru
|
|
|
23
23
|
|
|
24
24
|
Any other text is sent to the active session (DM or thread).
|
|
25
25
|
|
|
26
|
+
## Prompt metadata
|
|
27
|
+
|
|
28
|
+
Every Discord prompt is wrapped with lightweight Discord context before `promptTransform` runs:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
<discord_message_context>
|
|
32
|
+
{
|
|
33
|
+
"scope": "thread",
|
|
34
|
+
"sent_at": "2026-05-07T04:31:00.000Z",
|
|
35
|
+
"sent_at_local": "Thu, 7 May 26, 14:31 AEST",
|
|
36
|
+
"message_id": "...",
|
|
37
|
+
"author_name": "Alice",
|
|
38
|
+
"author_id": "...",
|
|
39
|
+
"thread_title": "Bug report",
|
|
40
|
+
"thread_id": "...",
|
|
41
|
+
"forum_channel_id": "..."
|
|
42
|
+
}
|
|
43
|
+
</discord_message_context>
|
|
44
|
+
|
|
45
|
+
User message:
|
|
46
|
+
...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
DM prompts omit thread-only fields. `sent_at_local` uses `promptTimeZone` and `promptLocale`.
|
|
50
|
+
|
|
26
51
|
## Install
|
|
27
52
|
|
|
28
53
|
```bash
|
|
@@ -33,19 +58,14 @@ bun add @friendlyrobot/discord-pi-agent
|
|
|
33
58
|
|
|
34
59
|
```ts
|
|
35
60
|
import {
|
|
36
|
-
buildTimeContextPrompt,
|
|
37
61
|
loadDiscordGatewayConfigFromEnv,
|
|
38
62
|
startDiscordGateway,
|
|
39
63
|
} from "@friendlyrobot/discord-pi-agent";
|
|
40
64
|
|
|
41
65
|
const config = loadDiscordGatewayConfigFromEnv({
|
|
42
66
|
cwd: process.cwd(),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
timeZone: "Australia/Sydney",
|
|
46
|
-
locale: "en-AU",
|
|
47
|
-
});
|
|
48
|
-
},
|
|
67
|
+
promptTimeZone: "Australia/Sydney",
|
|
68
|
+
promptLocale: "en-AU",
|
|
49
69
|
// Enable forum channel support (omit for DM-only)
|
|
50
70
|
discordAllowedForumChannelIds: ["1498563501780897832"],
|
|
51
71
|
});
|
|
@@ -70,6 +90,8 @@ The initial post body becomes the first prompt. Sessions survive restarts.
|
|
|
70
90
|
- `modelProvider` default: `openrouter`
|
|
71
91
|
- `modelId` default: `anthropic/claude-3.5-haiku`
|
|
72
92
|
- `thinkingLevel` default: `medium` (values: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`)
|
|
93
|
+
- `promptTimeZone` default: `UTC` — used for `sent_at_local` in Discord prompt metadata
|
|
94
|
+
- `promptLocale` default: `en-AU` — used for `sent_at_local` in Discord prompt metadata
|
|
73
95
|
- `promptTransform` default: identity
|
|
74
96
|
- `startupMessage` default: `Bot is online and ready.`
|
|
75
97
|
- `shutdownOnSignals` default: `true`
|
|
@@ -90,6 +112,8 @@ The initial post body becomes the first prompt. Sessions survive restarts.
|
|
|
90
112
|
- `PI_AGENT_DIR`
|
|
91
113
|
- `PI_MODEL_PROVIDER`
|
|
92
114
|
- `PI_MODEL_ID`
|
|
115
|
+
- `PI_PROMPT_TIME_ZONE`
|
|
116
|
+
- `PI_PROMPT_LOCALE`
|
|
93
117
|
- `DISCORD_STARTUP_MESSAGE`
|
|
94
118
|
- `DISCORD_FORUM_CHANNEL_IDS` — comma-separated forum channel IDs
|
|
95
119
|
- `DISCORD_ALLOWED_USER_IDS` — comma-separated allowed user IDs
|
|
@@ -8,8 +8,4 @@ export type GatewayAuthConfig = {
|
|
|
8
8
|
discordAllowedUserIds: string[];
|
|
9
9
|
startupMessage: string | false;
|
|
10
10
|
};
|
|
11
|
-
/**
|
|
12
|
-
* Combine a forum thread title with the post body for the initial session prompt.
|
|
13
|
-
*/
|
|
14
|
-
export declare function buildThreadOpeningPrompt(threadName: string, content: string): string;
|
|
15
11
|
export declare function startGatewayClient(config: ResolvedDiscordPiBridgeConfig, agentService: AgentService, sessionRegistry: SessionRegistry, authConfig: GatewayAuthConfig): Promise<Client>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DiscordGateway, DiscordGatewayConfig, DiscordPiBridge, DiscordPiBridgeConfig } from "./types";
|
|
2
|
-
export {
|
|
2
|
+
export { buildDiscordMessageContextPrompt, formatDiscordPromptTime, type DiscordMessageContextPromptOptions, type DiscordPromptScope, type DiscordPromptTimeFormatOptions, } from "./prompt-context";
|
|
3
3
|
export { loadDiscordPiBridgeConfigFromEnv, loadDiscordGatewayConfigFromEnv, resolveConfig, } from "./config";
|
|
4
4
|
export type { AgentStatus, DiscordGateway, DiscordGatewayConfig, DiscordPiBridge, DiscordPiBridgeConfig, PromptTransform, ResolvedDiscordPiBridgeConfig, } from "./types";
|
|
5
5
|
/**
|
package/dist/index.js
CHANGED
|
@@ -452,6 +452,8 @@ function resolveConfig(config) {
|
|
|
452
452
|
modelProvider: config.modelProvider?.trim() || "openrouter",
|
|
453
453
|
modelId: config.modelId?.trim() || "anthropic/claude-3.5-haiku",
|
|
454
454
|
thinkingLevel: parseThinkingLevel(config.thinkingLevel) || "medium",
|
|
455
|
+
promptTimeZone: config.promptTimeZone?.trim() || "UTC",
|
|
456
|
+
promptLocale: config.promptLocale?.trim() || "en-AU",
|
|
455
457
|
promptTransform: config.promptTransform || identityPromptTransform,
|
|
456
458
|
startupMessage: config.startupMessage === undefined ? "Bot is online and ready." : config.startupMessage,
|
|
457
459
|
shutdownOnSignals: config.shutdownOnSignals ?? true
|
|
@@ -467,6 +469,8 @@ function loadDiscordPiBridgeConfigFromEnv(overrides = {}) {
|
|
|
467
469
|
modelProvider: overrides.modelProvider || process.env.PI_MODEL_PROVIDER,
|
|
468
470
|
modelId: overrides.modelId || process.env.PI_MODEL_ID,
|
|
469
471
|
thinkingLevel: parseThinkingLevel(overrides.thinkingLevel || process.env.PI_THINKING_LEVEL),
|
|
472
|
+
promptTimeZone: overrides.promptTimeZone || process.env.PI_PROMPT_TIME_ZONE,
|
|
473
|
+
promptLocale: overrides.promptLocale || process.env.PI_PROMPT_LOCALE,
|
|
470
474
|
promptTransform: overrides.promptTransform,
|
|
471
475
|
startupMessage: overrides.startupMessage ?? readStartupMessageFromEnv(),
|
|
472
476
|
shutdownOnSignals: overrides.shutdownOnSignals
|
|
@@ -802,11 +806,74 @@ function chunkMessage(text, maxChunkSize = SAFE_MESSAGE_LIMIT) {
|
|
|
802
806
|
return chunks.map((chunk) => chunk.slice(0, DISCORD_MESSAGE_LIMIT));
|
|
803
807
|
}
|
|
804
808
|
|
|
805
|
-
// src/
|
|
806
|
-
function
|
|
807
|
-
|
|
809
|
+
// src/prompt-context.ts
|
|
810
|
+
function buildDiscordMessageContextPrompt(userMessage, options) {
|
|
811
|
+
const contextEntries = [
|
|
812
|
+
["scope", options.scope],
|
|
813
|
+
["sent_at", options.sentAt],
|
|
814
|
+
["sent_at_local", options.sentAtLocal],
|
|
815
|
+
["message_id", options.messageId],
|
|
816
|
+
["author_name", normalizeContextValue(options.authorName)],
|
|
817
|
+
["author_id", options.authorId],
|
|
818
|
+
["thread_title", normalizeContextValue(options.threadTitle)],
|
|
819
|
+
["thread_id", options.threadId],
|
|
820
|
+
["forum_channel_id", options.forumChannelId ?? undefined]
|
|
821
|
+
].filter((entry) => {
|
|
822
|
+
return typeof entry[1] === "string" && entry[1].trim().length > 0;
|
|
823
|
+
});
|
|
824
|
+
const contextJson = JSON.stringify(Object.fromEntries(contextEntries), null, 2);
|
|
825
|
+
return [
|
|
826
|
+
"<discord_message_context>",
|
|
827
|
+
contextJson,
|
|
828
|
+
"</discord_message_context>",
|
|
829
|
+
"",
|
|
830
|
+
"User message:",
|
|
831
|
+
userMessage.trim()
|
|
832
|
+
].join(`
|
|
833
|
+
`);
|
|
834
|
+
}
|
|
835
|
+
function formatDiscordPromptTime(date, options = {}) {
|
|
836
|
+
const timeZone = options.timeZone || "UTC";
|
|
837
|
+
const locale = options.locale || "en-AU";
|
|
838
|
+
return new Intl.DateTimeFormat(locale, {
|
|
839
|
+
timeZone,
|
|
840
|
+
weekday: "short",
|
|
841
|
+
day: "numeric",
|
|
842
|
+
month: "short",
|
|
843
|
+
year: "2-digit",
|
|
844
|
+
hour: "2-digit",
|
|
845
|
+
minute: "2-digit",
|
|
846
|
+
hour12: false,
|
|
847
|
+
timeZoneName: "short"
|
|
848
|
+
}).format(date);
|
|
849
|
+
}
|
|
850
|
+
function normalizeContextValue(value) {
|
|
851
|
+
if (value === undefined) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
return value.replace(/\s+/g, " ").trim();
|
|
855
|
+
}
|
|
808
856
|
|
|
809
|
-
|
|
857
|
+
// src/discord-gateway-client.ts
|
|
858
|
+
function getAuthorDisplayName(message) {
|
|
859
|
+
return message.member?.displayName || message.author.globalName || message.author.username;
|
|
860
|
+
}
|
|
861
|
+
function buildDiscordPromptContent(message, scope, content, config) {
|
|
862
|
+
const isThread = scope.startsWith("thread:") && message.channel.isThread();
|
|
863
|
+
return buildDiscordMessageContextPrompt(content, {
|
|
864
|
+
scope: scope === "dm" ? "dm" : "thread",
|
|
865
|
+
sentAt: message.createdAt.toISOString(),
|
|
866
|
+
sentAtLocal: formatDiscordPromptTime(message.createdAt, {
|
|
867
|
+
timeZone: config.promptTimeZone,
|
|
868
|
+
locale: config.promptLocale
|
|
869
|
+
}),
|
|
870
|
+
messageId: message.id,
|
|
871
|
+
authorId: message.author.id,
|
|
872
|
+
authorName: getAuthorDisplayName(message),
|
|
873
|
+
threadId: isThread ? message.channel.id : undefined,
|
|
874
|
+
threadTitle: isThread ? message.channel.name : undefined,
|
|
875
|
+
forumChannelId: isThread ? message.channel.parentId : undefined
|
|
876
|
+
});
|
|
810
877
|
}
|
|
811
878
|
function resolveScope(message) {
|
|
812
879
|
if (message.channel.type === ChannelType.DM) {
|
|
@@ -903,19 +970,6 @@ async function startGatewayClient(config, agentService, sessionRegistry, authCon
|
|
|
903
970
|
}
|
|
904
971
|
});
|
|
905
972
|
client.on(Events.MessageCreate, async (message) => {
|
|
906
|
-
if (message.channel.isThread()) {
|
|
907
|
-
console.log("[gateway:debug] thread message raw", {
|
|
908
|
-
messageId: message.id,
|
|
909
|
-
authorId: message.author.id,
|
|
910
|
-
authorTag: message.author.tag,
|
|
911
|
-
channelId: message.channel.id,
|
|
912
|
-
channelType: message.channel.type,
|
|
913
|
-
parentId: message.channel.parentId,
|
|
914
|
-
parentType: message.channel.parent?.type,
|
|
915
|
-
guildId: message.guild?.id,
|
|
916
|
-
content: message.content.slice(0, 500)
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
973
|
console.log("[gateway] message received", {
|
|
920
974
|
messageId: message.id,
|
|
921
975
|
authorId: message.author.id,
|
|
@@ -969,16 +1023,11 @@ async function onMessage(message, config, agentService, sessionRegistry, authCon
|
|
|
969
1023
|
}
|
|
970
1024
|
const { entry, created } = await sessionRegistry.getOrCreate(scope);
|
|
971
1025
|
const { session, promptQueue } = entry;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
console.log("[gateway] new thread session — prepending title", {
|
|
978
|
-
scope,
|
|
979
|
-
threadName: thread.name
|
|
980
|
-
});
|
|
981
|
-
}
|
|
1026
|
+
if (created && scope.startsWith("thread:") && message.channel.isThread()) {
|
|
1027
|
+
console.log("[gateway] new thread session", {
|
|
1028
|
+
scope,
|
|
1029
|
+
threadName: message.channel.name
|
|
1030
|
+
});
|
|
982
1031
|
}
|
|
983
1032
|
let typingInterval = null;
|
|
984
1033
|
if (message.channel.isSendable()) {
|
|
@@ -1033,7 +1082,9 @@ async function onMessage(message, config, agentService, sessionRegistry, authCon
|
|
|
1033
1082
|
}
|
|
1034
1083
|
const response = await promptQueue.enqueue(async () => {
|
|
1035
1084
|
console.log(`[queue] processing message ${message.id} in scope ${scope}`);
|
|
1036
|
-
const
|
|
1085
|
+
const promptContent = buildDiscordPromptContent(message, scope, content, config);
|
|
1086
|
+
console.log("[gateway:debug] prompt content", promptContent);
|
|
1087
|
+
const transformedPrompt = await config.promptTransform(promptContent);
|
|
1037
1088
|
return collectReply(session, transformedPrompt, {
|
|
1038
1089
|
logPrefix: `[agent:${session.sessionId}]`
|
|
1039
1090
|
});
|
|
@@ -1153,27 +1204,6 @@ class SessionRegistry {
|
|
|
1153
1204
|
}
|
|
1154
1205
|
}
|
|
1155
1206
|
|
|
1156
|
-
// src/prompt-context.ts
|
|
1157
|
-
function buildTimeContextPrompt(userMessage, options = {}) {
|
|
1158
|
-
const timeZone = options.timeZone || "UTC";
|
|
1159
|
-
const locale = options.locale || "en-AU";
|
|
1160
|
-
const now = options.now || new Date;
|
|
1161
|
-
const localTime = new Intl.DateTimeFormat(locale, {
|
|
1162
|
-
timeZone,
|
|
1163
|
-
weekday: "short",
|
|
1164
|
-
day: "numeric",
|
|
1165
|
-
month: "short",
|
|
1166
|
-
year: "2-digit",
|
|
1167
|
-
hour: "2-digit",
|
|
1168
|
-
minute: "2-digit",
|
|
1169
|
-
hour12: false,
|
|
1170
|
-
timeZoneName: "short"
|
|
1171
|
-
}).format(now);
|
|
1172
|
-
const trimmedMessage = userMessage.trim();
|
|
1173
|
-
return [`<time>${localTime}</time>`, "", trimmedMessage].join(`
|
|
1174
|
-
`);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1207
|
// src/index.ts
|
|
1178
1208
|
async function startDiscordGateway(config) {
|
|
1179
1209
|
const resolvedConfig = resolveGatewayConfig(config);
|
|
@@ -1241,5 +1271,6 @@ export {
|
|
|
1241
1271
|
resolveConfig,
|
|
1242
1272
|
loadDiscordPiBridgeConfigFromEnv,
|
|
1243
1273
|
loadDiscordGatewayConfigFromEnv,
|
|
1244
|
-
|
|
1274
|
+
formatDiscordPromptTime,
|
|
1275
|
+
buildDiscordMessageContextPrompt
|
|
1245
1276
|
};
|
package/dist/prompt-context.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type DiscordPromptTimeFormatOptions = {
|
|
2
2
|
timeZone?: string;
|
|
3
3
|
locale?: string;
|
|
4
|
-
now?: Date;
|
|
5
4
|
};
|
|
6
|
-
export
|
|
5
|
+
export type DiscordPromptScope = "dm" | "thread";
|
|
6
|
+
export type DiscordMessageContextPromptOptions = {
|
|
7
|
+
scope: DiscordPromptScope;
|
|
8
|
+
messageId: string;
|
|
9
|
+
authorId: string;
|
|
10
|
+
sentAt?: string;
|
|
11
|
+
sentAtLocal?: string;
|
|
12
|
+
authorName?: string;
|
|
13
|
+
threadId?: string;
|
|
14
|
+
threadTitle?: string;
|
|
15
|
+
forumChannelId?: string | null;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildDiscordMessageContextPrompt(userMessage: string, options: DiscordMessageContextPromptOptions): string;
|
|
18
|
+
export declare function formatDiscordPromptTime(date: Date, options?: DiscordPromptTimeFormatOptions): string;
|
package/dist/types.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type DiscordPiBridgeConfig = {
|
|
|
9
9
|
modelProvider?: string;
|
|
10
10
|
modelId?: string;
|
|
11
11
|
thinkingLevel?: ThinkingLevel;
|
|
12
|
+
promptTimeZone?: string;
|
|
13
|
+
promptLocale?: string;
|
|
12
14
|
promptTransform?: PromptTransform;
|
|
13
15
|
startupMessage?: string | false;
|
|
14
16
|
shutdownOnSignals?: boolean;
|
|
@@ -21,6 +23,8 @@ export type ResolvedDiscordPiBridgeConfig = {
|
|
|
21
23
|
modelProvider: string;
|
|
22
24
|
modelId: string;
|
|
23
25
|
thinkingLevel: ThinkingLevel;
|
|
26
|
+
promptTimeZone: string;
|
|
27
|
+
promptLocale: string;
|
|
24
28
|
promptTransform: PromptTransform;
|
|
25
29
|
startupMessage: string | false;
|
|
26
30
|
shutdownOnSignals: boolean;
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|