@friendlyrobot/discord-pi-agent 0.5.9 → 0.6.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 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) {
@@ -969,16 +1036,11 @@ async function onMessage(message, config, agentService, sessionRegistry, authCon
969
1036
  }
970
1037
  const { entry, created } = await sessionRegistry.getOrCreate(scope);
971
1038
  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
- }
1039
+ if (created && scope.startsWith("thread:") && message.channel.isThread()) {
1040
+ console.log("[gateway] new thread session", {
1041
+ scope,
1042
+ threadName: message.channel.name
1043
+ });
982
1044
  }
983
1045
  let typingInterval = null;
984
1046
  if (message.channel.isSendable()) {
@@ -1033,7 +1095,8 @@ async function onMessage(message, config, agentService, sessionRegistry, authCon
1033
1095
  }
1034
1096
  const response = await promptQueue.enqueue(async () => {
1035
1097
  console.log(`[queue] processing message ${message.id} in scope ${scope}`);
1036
- const transformedPrompt = await config.promptTransform(effectiveContent);
1098
+ const promptContent = buildDiscordPromptContent(message, scope, content, config);
1099
+ const transformedPrompt = await config.promptTransform(promptContent);
1037
1100
  return collectReply(session, transformedPrompt, {
1038
1101
  logPrefix: `[agent:${session.sessionId}]`
1039
1102
  });
@@ -1153,27 +1216,6 @@ class SessionRegistry {
1153
1216
  }
1154
1217
  }
1155
1218
 
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
1219
  // src/index.ts
1178
1220
  async function startDiscordGateway(config) {
1179
1221
  const resolvedConfig = resolveGatewayConfig(config);
@@ -1241,5 +1283,6 @@ export {
1241
1283
  resolveConfig,
1242
1284
  loadDiscordPiBridgeConfigFromEnv,
1243
1285
  loadDiscordGatewayConfigFromEnv,
1244
- buildTimeContextPrompt
1286
+ formatDiscordPromptTime,
1287
+ buildDiscordMessageContextPrompt
1245
1288
  };
@@ -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.0",
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 {};