@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 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
- promptTransform: (input) => {
44
- return buildTimeContextPrompt(input, {
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 { buildTimeContextPrompt, type TimeContextPromptOptions, } from "./prompt-context";
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/discord-gateway-client.ts
806
- function buildThreadOpeningPrompt(threadName, content) {
807
- return `<thread_title>${threadName}</thread_title>
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
- ${content}`;
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
- let effectiveContent = content;
973
- if (created && scope.startsWith("thread:")) {
974
- const thread = message.channel;
975
- if (thread.isThread() && thread.name) {
976
- effectiveContent = buildThreadOpeningPrompt(thread.name, content);
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 transformedPrompt = await config.promptTransform(effectiveContent);
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
- buildTimeContextPrompt
1274
+ formatDiscordPromptTime,
1275
+ buildDiscordMessageContextPrompt
1245
1276
  };
@@ -1,6 +1,18 @@
1
- export type TimeContextPromptOptions = {
1
+ export type DiscordPromptTimeFormatOptions = {
2
2
  timeZone?: string;
3
3
  locale?: string;
4
- now?: Date;
5
4
  };
6
- export declare function buildTimeContextPrompt(userMessage: string, options?: TimeContextPromptOptions): string;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@friendlyrobot/discord-pi-agent",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
4
  "description": "Reusable Discord gateway bridge for persistent pi agent sessions",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- export {};