@friendlyrobot/discord-pi-agent 0.21.0 → 0.21.3

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
@@ -53,7 +53,7 @@ DM prompts omit thread-only fields. `sent_at_local` uses `promptTimeZone` and `p
53
53
  ## Install
54
54
 
55
55
  ```bash
56
- bun add @friendlyrobot/discord-pi-agent
56
+ npm install @friendlyrobot/discord-pi-agent
57
57
  ```
58
58
 
59
59
  ## Usage
@@ -163,10 +163,21 @@ Not all models support thinking/reasoning. The configured `thinkingLevel` is app
163
163
  ## Build
164
164
 
165
165
  ```bash
166
- bun run build
167
- bun run typecheck
166
+ npm run build
167
+ npm run typecheck
168
168
  ```
169
169
 
170
+ ## Dependency updates
171
+
172
+ To check for newer package versions and update `package.json`, run:
173
+
174
+ ```bash
175
+ npx npm-check-updates -u
176
+ npm install
177
+ ```
178
+
179
+ This is the npm-side replacement for the old `bun update` workflow.
180
+
170
181
  ## Notes
171
182
 
172
183
  - DM and forum threads supported via `startDiscordGateway`
@@ -0,0 +1,132 @@
1
+ import { createModuleLogger } from "./logger";
2
+ const logger = createModuleLogger("agent-model-service");
3
+ export class AgentModelService {
4
+ config;
5
+ modelRegistry;
6
+ constructor(config, modelRegistry) {
7
+ this.config = config;
8
+ this.modelRegistry = modelRegistry;
9
+ }
10
+ findModel(provider, modelId) {
11
+ return this.modelRegistry.find(provider, modelId);
12
+ }
13
+ async ensureSessionHasConfiguredModel(session) {
14
+ if (session.model) {
15
+ logger.debug({
16
+ model: `${session.model.provider}/${session.model.id}`,
17
+ }, "retaining existing session model");
18
+ return;
19
+ }
20
+ const desiredModel = this.modelRegistry.find(this.config.modelProvider, this.config.modelId);
21
+ const availableModels = await this.modelRegistry.getAvailable();
22
+ logger.debug({
23
+ count: availableModels.length,
24
+ matches: availableModels
25
+ .filter((model) => {
26
+ return model.provider === this.config.modelProvider;
27
+ })
28
+ .map((model) => `${model.provider}/${model.id}`),
29
+ }, "available models");
30
+ if (!desiredModel) {
31
+ throw new Error(`Configured model not found: ${this.config.modelProvider}/${this.config.modelId}. Check your pi agent config and installed extensions.`);
32
+ }
33
+ logger.info({
34
+ to: `${desiredModel.provider}/${desiredModel.id}`,
35
+ }, "setting initial session model");
36
+ await session.setModel(desiredModel);
37
+ await this.applyConfiguredThinkingLevelForSession(session);
38
+ }
39
+ async listModels(session) {
40
+ const availableModels = await this.modelRegistry.getAvailable();
41
+ const currentDisplay = session?.model
42
+ ? `${session.model.provider}/${session.model.id}`
43
+ : null;
44
+ const lines = availableModels.map((model) => {
45
+ const display = `${model.provider}/${model.id}`;
46
+ const marker = currentDisplay === display ? " (current)" : "";
47
+ return ` ${display}${marker}`;
48
+ });
49
+ return [
50
+ `Available models (${availableModels.length}):`,
51
+ ...lines,
52
+ `\nUsage: !model <provider/modelId> to switch.`,
53
+ ].join("\n");
54
+ }
55
+ async switchModel(provider, modelId, session) {
56
+ const model = this.modelRegistry.find(provider, modelId);
57
+ if (!model) {
58
+ const availableModels = await this.modelRegistry.getAvailable();
59
+ const matches = availableModels
60
+ .filter((availableModel) => {
61
+ return availableModel.provider === provider;
62
+ })
63
+ .map((availableModel) => {
64
+ return `${availableModel.provider}/${availableModel.id}`;
65
+ });
66
+ const hint = matches.length > 0
67
+ ? `\nModels from "${provider}": ${matches.join(", ")}`
68
+ : `\nUse !model to see all available models.`;
69
+ return `Model not found: ${provider}/${modelId}.${hint}`;
70
+ }
71
+ if (isSameModel(session.model, model)) {
72
+ return `Already using ${provider}/${modelId}.`;
73
+ }
74
+ await session.setModel(model);
75
+ await this.applyConfiguredThinkingLevelForSession(session);
76
+ const thinkingInfo = session.supportsThinking()
77
+ ? ` (thinking: ${session.thinkingLevel})`
78
+ : "";
79
+ return `Switched to ${provider}/${modelId}${thinkingInfo}.`;
80
+ }
81
+ getCurrentModelDisplay(session) {
82
+ if (!session?.model) {
83
+ return "(no model selected)";
84
+ }
85
+ return `${session.model.provider}/${session.model.id}`;
86
+ }
87
+ getThinkingLevel(session) {
88
+ if (!session.supportsThinking()) {
89
+ return { current: "off", available: [], supported: false };
90
+ }
91
+ return {
92
+ current: session.thinkingLevel,
93
+ available: session.getAvailableThinkingLevels(),
94
+ supported: true,
95
+ };
96
+ }
97
+ setThinkingLevel(session, level) {
98
+ if (!session.supportsThinking()) {
99
+ return "Current model does not support reasoning/thinking.";
100
+ }
101
+ const available = session.getAvailableThinkingLevels();
102
+ if (!available.includes(level)) {
103
+ return `Invalid thinking level "${level}" for current model. Available: ${available.join(", ")}`;
104
+ }
105
+ session.setThinkingLevel(level);
106
+ return `Thinking level set to "${level}".`;
107
+ }
108
+ async applyConfiguredThinkingLevelForSession(session) {
109
+ if (session.supportsThinking()) {
110
+ const available = session.getAvailableThinkingLevels();
111
+ if (available.includes(this.config.thinkingLevel)) {
112
+ session.setThinkingLevel(this.config.thinkingLevel);
113
+ logger.debug({
114
+ level: this.config.thinkingLevel,
115
+ }, "thinking level applied");
116
+ }
117
+ else {
118
+ logger.debug({
119
+ requested: this.config.thinkingLevel,
120
+ available,
121
+ }, "thinking level not available for model");
122
+ }
123
+ }
124
+ }
125
+ }
126
+ function isSameModel(currentModel, desiredModel) {
127
+ if (!currentModel) {
128
+ return false;
129
+ }
130
+ return (currentModel.provider === desiredModel.provider &&
131
+ currentModel.id === desiredModel.id);
132
+ }
@@ -0,0 +1,70 @@
1
+ export class AgentResourceService {
2
+ resourceLoader;
3
+ constructor(resourceLoader) {
4
+ this.resourceLoader = resourceLoader;
5
+ }
6
+ getSkillsSummary() {
7
+ const result = this.resourceLoader.getSkills();
8
+ const { skills } = result;
9
+ if (skills.length === 0) {
10
+ return "Skills: (none loaded)";
11
+ }
12
+ const names = skills.map((skill) => {
13
+ return skill.name;
14
+ });
15
+ return `Skills (${skills.length}): ${names.join(", ") || "(none)"}`;
16
+ }
17
+ getExtensionsSummary() {
18
+ const result = this.resourceLoader.getExtensions();
19
+ const { extensions, errors } = result;
20
+ if (extensions.length === 0) {
21
+ return "Extensions: (none loaded)";
22
+ }
23
+ const lines = extensions.map((extension) => {
24
+ const toolCount = extension.tools.size;
25
+ const commandCount = extension.commands.size;
26
+ const parts = [];
27
+ if (toolCount > 0) {
28
+ parts.push(`${toolCount} tool${toolCount !== 1 ? "s" : ""}`);
29
+ }
30
+ if (commandCount > 0) {
31
+ parts.push(`${commandCount} command${commandCount !== 1 ? "s" : ""}`);
32
+ }
33
+ const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
34
+ return ` ${extension.path}${summary}`;
35
+ });
36
+ const header = `Extensions (${extensions.length}):`;
37
+ const errorLines = errors.length > 0
38
+ ? [
39
+ `Errors (${errors.length}):`,
40
+ ...errors.map((error) => {
41
+ return ` ${error.path}: ${error.error}`;
42
+ }),
43
+ ]
44
+ : [];
45
+ return [header, ...lines, ...errorLines].join("\n");
46
+ }
47
+ async reloadResources() {
48
+ await this.resourceLoader.reload();
49
+ const extensions = this.resourceLoader
50
+ .getExtensions()
51
+ .extensions.map((extension) => {
52
+ return extension.path;
53
+ });
54
+ const skills = this.resourceLoader.getSkills();
55
+ const skillNames = skills.skills.map((skill) => {
56
+ return skill.name;
57
+ });
58
+ const agentsFiles = this.resourceLoader
59
+ .getAgentsFiles()
60
+ .agentsFiles.map((file) => {
61
+ return file.path;
62
+ });
63
+ return [
64
+ "Resources reloaded.",
65
+ `Extensions (${extensions.length}): ${extensions.join(", ") || "(none)"}`,
66
+ `Skills (${skills.skills.length}): ${skillNames.join(", ") || "(none)"}`,
67
+ `AGENTS.md files (${agentsFiles.length}): ${agentsFiles.join(", ") || "(none)"}`,
68
+ ].join("\n");
69
+ }
70
+ }
@@ -0,0 +1,189 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
4
+ import { AgentModelService } from "./agent-model-service";
5
+ import { AgentResourceService } from "./agent-resource-service";
6
+ import { createModuleLogger } from "./logger";
7
+ import { runAgentTurn } from "./agent-turn-runner";
8
+ const logger = createModuleLogger("agent-service");
9
+ export class AgentService {
10
+ config;
11
+ authStorage;
12
+ modelRegistry;
13
+ settingsManager;
14
+ resourceLoader;
15
+ session = null;
16
+ models;
17
+ resources;
18
+ constructor(config) {
19
+ this.config = config;
20
+ this.authStorage = AuthStorage.create(path.join(config.agentDir, "auth.json"));
21
+ this.modelRegistry = ModelRegistry.create(this.authStorage, path.join(config.agentDir, "models.json"));
22
+ this.settingsManager = SettingsManager.create(config.cwd, config.agentDir);
23
+ this.resourceLoader = new DefaultResourceLoader({
24
+ cwd: config.cwd,
25
+ agentDir: config.agentDir,
26
+ settingsManager: this.settingsManager,
27
+ });
28
+ this.models = new AgentModelService(config, this.modelRegistry);
29
+ this.resources = new AgentResourceService(this.resourceLoader);
30
+ }
31
+ async initialize() {
32
+ await fs.mkdir(this.config.agentDir, { recursive: true });
33
+ await fs.mkdir(this.getSessionDir(), { recursive: true });
34
+ logger.info({
35
+ cwd: this.config.cwd,
36
+ agentDir: this.config.agentDir,
37
+ sessionDir: this.getSessionDir(),
38
+ modelProvider: this.config.modelProvider,
39
+ modelId: this.config.modelId,
40
+ thinkingLevel: this.config.thinkingLevel,
41
+ }, "config");
42
+ await this.resourceLoader.reload();
43
+ logger.info({
44
+ extensions: this.resourceLoader
45
+ .getExtensions()
46
+ .extensions.map((extension) => extension.path),
47
+ agentsFiles: this.resourceLoader
48
+ .getAgentsFiles()
49
+ .agentsFiles.map((file) => file.path),
50
+ }, "resources loaded");
51
+ await this.createOrResumeSession();
52
+ await this.ensureConfiguredModel();
53
+ }
54
+ getSession() {
55
+ return this.session;
56
+ }
57
+ getAgentDir() {
58
+ return this.config.agentDir;
59
+ }
60
+ /**
61
+ * Create a temporary in-memory session. For one-shot tasks like image
62
+ * description — no file persistence, no cleanup needed. The caller must
63
+ * setModel() before prompting and dispose() when done.
64
+ */
65
+ async createTemporarySession() {
66
+ const { session } = await createAgentSession({
67
+ cwd: this.config.cwd,
68
+ agentDir: this.config.agentDir,
69
+ authStorage: this.authStorage,
70
+ modelRegistry: this.modelRegistry,
71
+ resourceLoader: this.resourceLoader,
72
+ settingsManager: this.settingsManager,
73
+ sessionManager: SessionManager.inMemory(),
74
+ thinkingLevel: "off",
75
+ });
76
+ logger.debug({ sessionId: session.sessionId }, "temporary session created");
77
+ return session;
78
+ }
79
+ async createSession(sessionDir) {
80
+ await fs.mkdir(sessionDir, { recursive: true });
81
+ const { session } = await createAgentSession({
82
+ cwd: this.config.cwd,
83
+ agentDir: this.config.agentDir,
84
+ authStorage: this.authStorage,
85
+ modelRegistry: this.modelRegistry,
86
+ resourceLoader: this.resourceLoader,
87
+ settingsManager: this.settingsManager,
88
+ sessionManager: SessionManager.continueRecent(this.config.cwd, sessionDir),
89
+ thinkingLevel: this.config.thinkingLevel,
90
+ });
91
+ logger.debug({
92
+ sessionDir,
93
+ sessionId: session.sessionId,
94
+ sessionFile: session.sessionFile,
95
+ }, "scoped session created");
96
+ await this.models.ensureSessionHasConfiguredModel(session);
97
+ return session;
98
+ }
99
+ async prompt(text) {
100
+ const session = this.requireSession();
101
+ const transformedPrompt = await this.config.promptTransform({
102
+ rawContent: text,
103
+ discordMetadata: "",
104
+ now: () => "",
105
+ userMessage: () => text,
106
+ });
107
+ return runAgentTurn(session, transformedPrompt);
108
+ }
109
+ async compact() {
110
+ const session = this.requireSession();
111
+ await session.compact();
112
+ return `Compaction finished for session ${session.sessionId}.`;
113
+ }
114
+ async resetSession() {
115
+ const previousSession = this.requireSession();
116
+ await previousSession.abort();
117
+ previousSession.dispose();
118
+ this.session = null;
119
+ const { session } = await createAgentSession({
120
+ cwd: this.config.cwd,
121
+ agentDir: this.config.agentDir,
122
+ authStorage: this.authStorage,
123
+ modelRegistry: this.modelRegistry,
124
+ resourceLoader: this.resourceLoader,
125
+ settingsManager: this.settingsManager,
126
+ sessionManager: SessionManager.create(this.config.cwd, this.getSessionDir()),
127
+ thinkingLevel: this.config.thinkingLevel,
128
+ });
129
+ this.session = session;
130
+ await this.ensureConfiguredModel();
131
+ return `Started a fresh session. Old session kept at ${previousSession.sessionFile ?? "(unknown path)"}.`;
132
+ }
133
+ getStatus() {
134
+ const session = this.requireSession();
135
+ const model = this.models.getCurrentModelDisplay(session);
136
+ const contextUsage = session.getContextUsage();
137
+ const thinkingInfo = session.supportsThinking()
138
+ ? `thinking: ${session.thinkingLevel} (available: ${session.getAvailableThinkingLevels().join(", ")})`
139
+ : "thinking: not supported";
140
+ return {
141
+ sessionId: session.sessionId,
142
+ sessionFile: session.sessionFile,
143
+ model,
144
+ streaming: session.isStreaming,
145
+ contextUsage,
146
+ thinkingInfo,
147
+ };
148
+ }
149
+ async shutdown() {
150
+ const session = this.session;
151
+ if (session) {
152
+ await session.abort();
153
+ session.dispose();
154
+ }
155
+ await this.settingsManager.flush();
156
+ }
157
+ async createOrResumeSession() {
158
+ const { session } = await createAgentSession({
159
+ cwd: this.config.cwd,
160
+ agentDir: this.config.agentDir,
161
+ authStorage: this.authStorage,
162
+ modelRegistry: this.modelRegistry,
163
+ resourceLoader: this.resourceLoader,
164
+ settingsManager: this.settingsManager,
165
+ sessionManager: SessionManager.continueRecent(this.config.cwd, this.getSessionDir()),
166
+ thinkingLevel: this.config.thinkingLevel,
167
+ });
168
+ this.session = session;
169
+ logger.info({
170
+ sessionId: session.sessionId,
171
+ sessionFile: session.sessionFile,
172
+ restoredModel: session.model
173
+ ? `${session.model.provider}/${session.model.id}`
174
+ : null,
175
+ }, "session ready");
176
+ }
177
+ async ensureConfiguredModel() {
178
+ await this.models.ensureSessionHasConfiguredModel(this.requireSession());
179
+ }
180
+ requireSession() {
181
+ if (!this.session) {
182
+ throw new Error("Agent session has not been initialized.");
183
+ }
184
+ return this.session;
185
+ }
186
+ getSessionDir() {
187
+ return path.join(this.config.agentDir, "sessions");
188
+ }
189
+ }
@@ -0,0 +1,148 @@
1
+ import { debugPrint } from "./debug-print";
2
+ import { createModuleLogger } from "./logger";
3
+ import { transformMarkdownTablesToCodeBlocks } from "./markdown-table-transformer";
4
+ const logger = createModuleLogger("agent-turn-runner");
5
+ export async function runAgentTurn(session, prompt, options = {}) {
6
+ let streamedText = "";
7
+ let eventCount = 0;
8
+ let toolCount = 0;
9
+ const toolInputsByCallId = new Map();
10
+ const model = session.model
11
+ ? `${session.model.provider}/${session.model.id}`
12
+ : "none";
13
+ debugPrint(prompt, "Full Prompt");
14
+ // logger.debug(
15
+ // {
16
+ // promptLength: prompt.length,
17
+ // model,
18
+ // prompt,
19
+ // },
20
+ // "prompt start",
21
+ // );
22
+ const unsubscribe = session.subscribe((event) => {
23
+ eventCount += 1;
24
+ if (event.type === "message_update") {
25
+ if (event.assistantMessageEvent.type === "text_delta") {
26
+ streamedText += event.assistantMessageEvent.delta;
27
+ }
28
+ if (event.assistantMessageEvent.type === "thinking_delta") {
29
+ // Intentionally ignored. Thinking deltas are too noisy for routine logs.
30
+ }
31
+ }
32
+ if (event.type === "tool_execution_start") {
33
+ toolCount += 1;
34
+ const input = event.toolName === "bash" ? event.args.command : event.args;
35
+ toolInputsByCallId.set(event.toolCallId, input);
36
+ if (event.toolName === "bash") {
37
+ debugPrint(input, "CMD");
38
+ // logger.debug(
39
+ // {
40
+ // toolName: event.toolName,
41
+ // },
42
+ // `agent tool start: [${event.toolName}]`,
43
+ // );
44
+ }
45
+ else {
46
+ logger.debug({
47
+ toolName: event.toolName,
48
+ // input,
49
+ }, `agent tool start: [${event.toolName}]`);
50
+ }
51
+ }
52
+ if (event.type === "tool_execution_end") {
53
+ const input = toolInputsByCallId.get(event.toolCallId);
54
+ toolInputsByCallId.delete(event.toolCallId);
55
+ if (event.toolName === "bash") {
56
+ debugPrint(extractToolOutput(event.result), event.isError ? "BASH TOOL ERROR OUTPUT" : "BASH TOOL OUTPUT");
57
+ // logger.debug(
58
+ // {
59
+ // toolName: event.toolName,
60
+ // isError: event.isError,
61
+ // },
62
+ // `agent tool end: [${event.toolName}] ${truncateForLog(
63
+ // typeof input === "string" ? input : "",
64
+ // )}`,
65
+ // );
66
+ }
67
+ else {
68
+ logger.debug({
69
+ toolName: event.toolName,
70
+ // input: truncateForLog(extractToolOutput(input)),
71
+ isError: event.isError,
72
+ // output: event.result,
73
+ // output: truncateForLog(extractToolOutput(event.result)),
74
+ }, `agent tool end: [${event.toolName}]`);
75
+ }
76
+ }
77
+ // if (event.type === "agent_end") {
78
+ // logger.debug(
79
+ // {
80
+ // messageCount: event.messages.length,
81
+ // model,
82
+ // toolCount,
83
+ // eventCount,
84
+ // },
85
+ // "agent end",
86
+ // );
87
+ // }
88
+ });
89
+ try {
90
+ await session.prompt(prompt, { images: options.images });
91
+ }
92
+ finally {
93
+ unsubscribe();
94
+ }
95
+ const errorMessage = session.agent.state.errorMessage?.trim();
96
+ const fallbackText = getLatestAssistantText(session.messages);
97
+ const finalText = streamedText.trim() || fallbackText.trim();
98
+ if (errorMessage) {
99
+ return errorMessage;
100
+ }
101
+ if (finalText) {
102
+ const transformed = await transformMarkdownTablesToCodeBlocks(finalText);
103
+ debugPrint(finalText, "BEFORE TRANSFORM");
104
+ debugPrint(transformed, "TRANSFORMED");
105
+ return transformed;
106
+ }
107
+ return "No response generated.";
108
+ }
109
+ function truncateForLog(value, maxLength = 400) {
110
+ if (value.length <= maxLength) {
111
+ return value;
112
+ }
113
+ return `${value.slice(0, maxLength)}...`;
114
+ }
115
+ function extractToolOutput(output) {
116
+ if (typeof output === "object" && output !== null) {
117
+ const obj = output;
118
+ if (Array.isArray(obj.content)) {
119
+ return obj.content
120
+ .map((item) => {
121
+ if (item.type === "text" && typeof item.text === "string") {
122
+ return item.text;
123
+ }
124
+ return JSON.stringify(item);
125
+ })
126
+ .join("\n");
127
+ }
128
+ }
129
+ return String(output);
130
+ }
131
+ function getLatestAssistantText(messages) {
132
+ const latestAssistantMessage = [...messages].reverse().find((message) => {
133
+ return message.role === "assistant";
134
+ });
135
+ if (!latestAssistantMessage ||
136
+ !Array.isArray(latestAssistantMessage.content)) {
137
+ return "";
138
+ }
139
+ return latestAssistantMessage.content
140
+ .filter((item) => {
141
+ return item.type === "text";
142
+ })
143
+ .map((item) => {
144
+ return item.text ?? "";
145
+ })
146
+ .join("\n")
147
+ .trim();
148
+ }
package/dist/config.js ADDED
@@ -0,0 +1,103 @@
1
+ import path from "node:path";
2
+ import dotenv from "dotenv";
3
+ export function resolveConfig(config) {
4
+ const discordAllowedUserId = requireNonEmptyConfigValue("discordAllowedUserId", config.discordAllowedUserId);
5
+ const cwd = requireNonEmptyConfigValue("cwd", config.cwd);
6
+ return {
7
+ discordBotToken: requireNonEmptyConfigValue("discordBotToken", config.discordBotToken),
8
+ discordAllowedUserId,
9
+ cwd,
10
+ agentDir: config.agentDir?.trim() || path.join(cwd, ".pi-agent"),
11
+ modelProvider: config.modelProvider?.trim() || "openrouter",
12
+ modelId: config.modelId?.trim() || "anthropic/claude-3.5-haiku",
13
+ thinkingLevel: parseThinkingLevel(config.thinkingLevel) || "medium",
14
+ promptTimeZone: config.promptTimeZone?.trim() || "UTC",
15
+ promptLocale: config.promptLocale?.trim() || "en-AU",
16
+ promptTransform: config.promptTransform || defaultPromptTransform,
17
+ startupMessage: config.startupMessage === undefined
18
+ ? "Bot is online and ready."
19
+ : config.startupMessage,
20
+ shutdownOnSignals: config.shutdownOnSignals ?? true,
21
+ visionModelId: config.visionModelId?.trim() || null,
22
+ discordAllowedForumChannelIds: config.discordAllowedForumChannelIds ?? [],
23
+ discordAllowedUserIds: config.discordAllowedUserIds ?? [
24
+ discordAllowedUserId,
25
+ ],
26
+ };
27
+ }
28
+ export function loadDiscordGatewayConfigFromEnv(overrides = {}) {
29
+ dotenv.config();
30
+ return resolveConfig({
31
+ discordBotToken: overrides.discordBotToken || process.env.DISCORD_BOT_TOKEN || "",
32
+ discordAllowedUserId: overrides.discordAllowedUserId ||
33
+ process.env.DISCORD_ALLOWED_USER_ID ||
34
+ "",
35
+ cwd: overrides.cwd || process.env.PI_AGENT_CWD || process.cwd(),
36
+ agentDir: overrides.agentDir || process.env.PI_AGENT_DIR,
37
+ modelProvider: overrides.modelProvider || process.env.PI_MODEL_PROVIDER,
38
+ modelId: overrides.modelId || process.env.PI_MODEL_ID,
39
+ thinkingLevel: parseThinkingLevel(overrides.thinkingLevel || process.env.PI_THINKING_LEVEL),
40
+ promptTimeZone: overrides.promptTimeZone || process.env.PI_PROMPT_TIME_ZONE,
41
+ promptLocale: overrides.promptLocale || process.env.PI_PROMPT_LOCALE,
42
+ promptTransform: overrides.promptTransform,
43
+ startupMessage: overrides.startupMessage ?? readStartupMessageFromEnv(),
44
+ shutdownOnSignals: overrides.shutdownOnSignals,
45
+ visionModelId: overrides.visionModelId ?? process.env.PI_VISION_MODEL_ID,
46
+ discordAllowedForumChannelIds: overrides.discordAllowedForumChannelIds ??
47
+ parseStringArrayFromEnv("DISCORD_FORUM_CHANNEL_IDS") ??
48
+ [],
49
+ discordAllowedUserIds: overrides.discordAllowedUserIds ??
50
+ parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS"),
51
+ });
52
+ }
53
+ function requireNonEmptyConfigValue(name, value) {
54
+ const trimmedValue = value.trim();
55
+ if (!trimmedValue) {
56
+ throw new Error(`Missing required config value: ${name}`);
57
+ }
58
+ return trimmedValue;
59
+ }
60
+ function readStartupMessageFromEnv() {
61
+ const value = process.env.DISCORD_STARTUP_MESSAGE;
62
+ if (value === undefined) {
63
+ return undefined;
64
+ }
65
+ const trimmedValue = value.trim();
66
+ if (!trimmedValue || trimmedValue.toLowerCase() === "false") {
67
+ return false;
68
+ }
69
+ return trimmedValue;
70
+ }
71
+ function parseThinkingLevel(value) {
72
+ if (!value) {
73
+ return undefined;
74
+ }
75
+ const trimmed = value.trim().toLowerCase();
76
+ const validLevels = [
77
+ "off",
78
+ "minimal",
79
+ "low",
80
+ "medium",
81
+ "high",
82
+ "xhigh",
83
+ ];
84
+ if (validLevels.includes(trimmed)) {
85
+ return trimmed;
86
+ }
87
+ return undefined;
88
+ }
89
+ function defaultPromptTransform(ctx) {
90
+ return [ctx.now(), ctx.discordMetadata, "", ctx.userMessage()]
91
+ .filter(Boolean)
92
+ .join("\n");
93
+ }
94
+ function parseStringArrayFromEnv(key) {
95
+ const value = process.env[key];
96
+ if (!value) {
97
+ return undefined;
98
+ }
99
+ return value
100
+ .split(",")
101
+ .map((id) => id.trim())
102
+ .filter(Boolean);
103
+ }