skimpyclaw 0.3.14 → 0.4.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 +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder, } from 'discord.js';
|
|
1
|
+
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder, ChannelType, } from 'discord.js';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
5
|
-
import { getCurrentModel, setCurrentModel } from '../../gateway.js';
|
|
5
|
+
import { getCurrentModel, getCurrentThinking, setCurrentModel, setCurrentThinking } from '../../gateway.js';
|
|
6
6
|
import { getCronJobs, runCronJob } from '../../cron.js';
|
|
7
7
|
import { runAgentTurn } from '../../agent.js';
|
|
8
8
|
import { runHeartbeatCheck } from '../../heartbeat.js';
|
|
@@ -13,14 +13,554 @@ import { transcribeAudio, synthesizeSpeech } from '../../voice.js';
|
|
|
13
13
|
import * as sessions from '../../sessions.js';
|
|
14
14
|
import { formatAliases, formatModelSelectionError, getModelSelectionUsage, resolveModelSelection } from '../../model-selection.js';
|
|
15
15
|
import { KNOWN_COMMANDS } from './types.js';
|
|
16
|
-
import { getHistory, addToHistory, clearHistory, replaceHistory, getDiscordToolConfig, getDiscordRunContext, conversationKey, buildHelpText, sendLongText, startTypingIndicator, } from './utils.js';
|
|
16
|
+
import { getHistory, addToHistory, clearHistory, replaceHistory, getDiscordToolConfig, getDiscordRunContext, conversationKey, buildCodeAgentThreadContext, buildHelpText, sendLongText, sendLongTextToChannel, startTypingIndicator, startTypingIndicatorForChannel, } from './utils.js';
|
|
17
|
+
import { createTaskThread, buildThreadUrl } from './threads.js';
|
|
18
|
+
import { bindThreadAgent, getAgentProfileByAlias, getThreadAgentByThreadId, listAgentProfiles, parseDiscordAgentMention, removeAgentProfile, setAgentProfileModel, setAgentProfilePrompt, setAgentProfileThinking, upsertAgentProfile, } from './thread-agents.js';
|
|
19
|
+
import { isDocumentAttachment, processAttachments, supportedExtensions } from './attachments.js';
|
|
20
|
+
import { getSession, linkThread } from '../../code-agents/interactive-sessions.js';
|
|
21
|
+
import { handleInteractiveThreadMessage } from '../../code-agents/interactive-resume.js';
|
|
17
22
|
// ── Command handler ─────────────────────────────────────────────────
|
|
23
|
+
const THREAD_AGENT_USAGE = [
|
|
24
|
+
'Usage:',
|
|
25
|
+
'/agent create <alias> [agent-id]',
|
|
26
|
+
'/agent use <alias> [message]',
|
|
27
|
+
'/agent model [alias] <model-alias|provider/model|model-id>',
|
|
28
|
+
'/agent effort [alias] <none|low|medium|high|xhigh>',
|
|
29
|
+
'/agent prompt [alias] <prompt text>',
|
|
30
|
+
'/agent delete <alias>',
|
|
31
|
+
'/agent list',
|
|
32
|
+
'@alias <message>',
|
|
33
|
+
].join('\n');
|
|
34
|
+
const THINKING_LEVELS = ['none', 'low', 'medium', 'high', 'xhigh'];
|
|
35
|
+
const AGENT_PROMPT_MAX_CHARS = 20_000;
|
|
36
|
+
function parseThinkingLevel(value) {
|
|
37
|
+
const normalized = (value || '').trim().toLowerCase().replace(/^x[-_ ]?high$/, 'xhigh');
|
|
38
|
+
if (normalized === 'off')
|
|
39
|
+
return 'none';
|
|
40
|
+
return THINKING_LEVELS.includes(normalized) ? normalized : null;
|
|
41
|
+
}
|
|
42
|
+
function formatThinkingUsage(command = '/effort') {
|
|
43
|
+
return `Usage: ${command} <${THINKING_LEVELS.join('|')}>`;
|
|
44
|
+
}
|
|
45
|
+
function formatAgentIds(config) {
|
|
46
|
+
return Object.keys(config.agents.list).join(', ') || '(none configured)';
|
|
47
|
+
}
|
|
48
|
+
function resolveConfiguredAgentId(config, rawAgentId) {
|
|
49
|
+
const candidate = (rawAgentId || config.agents.default).trim();
|
|
50
|
+
return config.agents.list[candidate] ? candidate : null;
|
|
51
|
+
}
|
|
52
|
+
function formatThreadAgent(record) {
|
|
53
|
+
const prompt = record.promptOverlay
|
|
54
|
+
? `\nPrompt: ${record.promptOverlay.length > 120 ? record.promptOverlay.slice(0, 120) + '...' : record.promptOverlay}`
|
|
55
|
+
: '';
|
|
56
|
+
const model = record.model ? `, model ${record.model}` : '';
|
|
57
|
+
const thinking = record.thinking ? `, effort ${record.thinking}` : '';
|
|
58
|
+
return `This thread uses @${record.alias} -> ${record.agentId}${model}${thinking}${prompt}`;
|
|
59
|
+
}
|
|
60
|
+
function formatAgentProfile(profile) {
|
|
61
|
+
const prompt = profile.promptOverlay
|
|
62
|
+
? `\nPrompt: ${profile.promptOverlay.length > 120 ? profile.promptOverlay.slice(0, 120) + '...' : profile.promptOverlay}`
|
|
63
|
+
: '';
|
|
64
|
+
const model = profile.model ? `, model ${profile.model}` : '';
|
|
65
|
+
const thinking = profile.thinking ? `, effort ${profile.thinking}` : '';
|
|
66
|
+
return `@${profile.alias} -> ${profile.agentId}${model}${thinking}${prompt}`;
|
|
67
|
+
}
|
|
68
|
+
function formatThreadAgentThreadName(alias, taskText) {
|
|
69
|
+
const task = (taskText || '')
|
|
70
|
+
.replace(/https?:\/\/\S+\/pull\/(\d+)\S*/g, '#$1')
|
|
71
|
+
.replace(/https?:\/\/\S+\/issues\/(\d+)\S*/g, '#$1')
|
|
72
|
+
.replace(/https?:\/\/\S+/g, '')
|
|
73
|
+
.replace(/\s+/g, ' ')
|
|
74
|
+
.trim();
|
|
75
|
+
const base = task || 'agent';
|
|
76
|
+
const maxTaskLength = Math.max(10, 96 - alias.length);
|
|
77
|
+
const suffix = base.length > maxTaskLength ? `${base.slice(0, maxTaskLength - 3).trim()}...` : base;
|
|
78
|
+
return `${alias}: ${suffix}`.slice(0, 100);
|
|
79
|
+
}
|
|
80
|
+
function isDiscordThreadChannel(channel) {
|
|
81
|
+
if (channel.isDMBased())
|
|
82
|
+
return false;
|
|
83
|
+
if (channel.isThread())
|
|
84
|
+
return true;
|
|
85
|
+
const channelType = channel.type;
|
|
86
|
+
return channelType === ChannelType.PublicThread
|
|
87
|
+
|| channelType === ChannelType.PrivateThread
|
|
88
|
+
|| channelType === ChannelType.AnnouncementThread;
|
|
89
|
+
}
|
|
90
|
+
async function createThreadAgentThread(message, alias, taskText) {
|
|
91
|
+
if (message.channel.isDMBased())
|
|
92
|
+
return null;
|
|
93
|
+
if (!('threads' in message.channel))
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
const thread = await message.startThread({
|
|
97
|
+
name: formatThreadAgentThreadName(alias, taskText),
|
|
98
|
+
autoArchiveDuration: 1440,
|
|
99
|
+
});
|
|
100
|
+
return thread;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error('[discord-thread-agents] Failed to create thread agent thread:', err);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function getThreadAgentForMessage(message, config) {
|
|
108
|
+
if (!isDiscordThreadChannel(message.channel))
|
|
109
|
+
return null;
|
|
110
|
+
const record = getThreadAgentByThreadId(message.channel.id);
|
|
111
|
+
if (!record)
|
|
112
|
+
return null;
|
|
113
|
+
if (!config.agents.list[record.agentId])
|
|
114
|
+
return null;
|
|
115
|
+
return record;
|
|
116
|
+
}
|
|
117
|
+
function profileAsThreadAgent(profile, message, channel) {
|
|
118
|
+
const channelId = isThreadChannel(channel)
|
|
119
|
+
? channel.parentId ?? message.channelId
|
|
120
|
+
: message.channelId;
|
|
121
|
+
return {
|
|
122
|
+
...profile,
|
|
123
|
+
threadId: channel.id,
|
|
124
|
+
profileAlias: profile.alias,
|
|
125
|
+
guildId: message.guildId || undefined,
|
|
126
|
+
channelId,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function isDMBasedChannel(channel) {
|
|
130
|
+
return Boolean(channel.isDMBased?.());
|
|
131
|
+
}
|
|
132
|
+
function isThreadChannel(channel) {
|
|
133
|
+
return Boolean(channel.isThread?.());
|
|
134
|
+
}
|
|
135
|
+
function conversationKeyForChannel(message, channel) {
|
|
136
|
+
if (isDMBasedChannel(channel)) {
|
|
137
|
+
return `dm:${message.author.id}`;
|
|
138
|
+
}
|
|
139
|
+
return `channel:${channel.id}`;
|
|
140
|
+
}
|
|
141
|
+
function sendableChannel(channel) {
|
|
142
|
+
return channel;
|
|
143
|
+
}
|
|
144
|
+
function getDiscordRunContextForChannel(message, channel) {
|
|
145
|
+
const context = getDiscordRunContext(message);
|
|
146
|
+
const isDm = isDMBasedChannel(channel);
|
|
147
|
+
const isThread = !isDm && isThreadChannel(channel);
|
|
148
|
+
return {
|
|
149
|
+
...context,
|
|
150
|
+
sessionId: channel.id,
|
|
151
|
+
metadata: {
|
|
152
|
+
...(context.metadata || {}),
|
|
153
|
+
isDm,
|
|
154
|
+
...(isThread ? {
|
|
155
|
+
discordThreadId: channel.id,
|
|
156
|
+
discordChannelId: channel.parentId ?? message.channelId,
|
|
157
|
+
} : {}),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function getThreadAgentRunContext(message, threadAgent, channel = message.channel) {
|
|
162
|
+
const context = getDiscordRunContextForChannel(message, channel);
|
|
163
|
+
if (!threadAgent) {
|
|
164
|
+
return {
|
|
165
|
+
...context,
|
|
166
|
+
metadata: {
|
|
167
|
+
...(context.metadata || {}),
|
|
168
|
+
thinkingOverride: getCurrentThinking(),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...context,
|
|
174
|
+
metadata: {
|
|
175
|
+
...(context.metadata || {}),
|
|
176
|
+
threadAgentAlias: threadAgent.alias,
|
|
177
|
+
threadAgentId: threadAgent.agentId,
|
|
178
|
+
threadAgentModel: threadAgent.model,
|
|
179
|
+
threadAgentThinking: threadAgent.thinking,
|
|
180
|
+
thinkingOverride: threadAgent.thinking ?? getCurrentThinking(),
|
|
181
|
+
threadAgentPromptOverlay: threadAgent.promptOverlay,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function runThreadAgentPrompt(message, targetChannel, threadAgent, promptText, config, options = {}) {
|
|
186
|
+
const prompt = promptText.trim();
|
|
187
|
+
if (!prompt)
|
|
188
|
+
return;
|
|
189
|
+
if (options.announceTask) {
|
|
190
|
+
await sendLongTextToChannel(sendableChannel(targetChannel), `Task from @${message.author.username || message.author.id}:\n${prompt}`);
|
|
191
|
+
}
|
|
192
|
+
const stopTyping = startTypingIndicatorForChannel(targetChannel);
|
|
193
|
+
try {
|
|
194
|
+
const key = conversationKeyForChannel(message, targetChannel);
|
|
195
|
+
const history = await getHistory(key);
|
|
196
|
+
const response = await runAgentTurn(threadAgent.agentId, prompt, config, threadAgent.model || getCurrentModel(), getDiscordToolConfig(config), history, getThreadAgentRunContext(message, threadAgent, targetChannel));
|
|
197
|
+
await addToHistory(key, prompt, response);
|
|
198
|
+
await sendLongTextToChannel(sendableChannel(targetChannel), response);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
202
|
+
await sendLongTextToChannel(sendableChannel(targetChannel), `Error: ${msg}`);
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
stopTyping();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function runMentionedAgentPrompt(message, text, config) {
|
|
209
|
+
const invocation = parseDiscordAgentMention(text);
|
|
210
|
+
if (!invocation)
|
|
211
|
+
return false;
|
|
212
|
+
const profile = getAgentProfileByAlias(invocation.alias);
|
|
213
|
+
if (!profile) {
|
|
214
|
+
await message.reply(`No Discord agent profile found for @${invocation.alias}. Create it with /agent create ${invocation.alias}.`);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (!config.agents.list[profile.agentId]) {
|
|
218
|
+
await message.reply(`Agent profile @${profile.alias} points to missing configured agent "${profile.agentId}".`);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (!invocation.prompt) {
|
|
222
|
+
await message.reply(`Usage: @${profile.alias} <message>`);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (message.channel.isDMBased()) {
|
|
227
|
+
const record = profileAsThreadAgent(profile, message, message.channel);
|
|
228
|
+
await runThreadAgentPrompt(message, message.channel, record, invocation.prompt, config);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const isThread = isDiscordThreadChannel(message.channel);
|
|
232
|
+
const targetChannel = isThread
|
|
233
|
+
? message.channel
|
|
234
|
+
: await createThreadAgentThread(message, profile.alias, invocation.prompt);
|
|
235
|
+
if (!targetChannel) {
|
|
236
|
+
await message.reply('Agent mentions can only run in a DM, inside a Discord thread, or from a channel where I can create one.');
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
const record = bindThreadAgent({
|
|
240
|
+
threadId: targetChannel.id,
|
|
241
|
+
alias: profile.alias,
|
|
242
|
+
createdBy: message.author.id,
|
|
243
|
+
guildId: message.guildId,
|
|
244
|
+
channelId: targetChannel.isThread() ? targetChannel.parentId ?? message.channelId : message.channelId,
|
|
245
|
+
});
|
|
246
|
+
if (!isThread) {
|
|
247
|
+
const url = buildThreadUrl(message.guildId, targetChannel.id);
|
|
248
|
+
await message.reply(url ? `Started @${profile.alias}: ${url}` : `Started @${profile.alias}.`);
|
|
249
|
+
}
|
|
250
|
+
await runThreadAgentPrompt(message, targetChannel, record, invocation.prompt, config, {
|
|
251
|
+
announceTask: !isThread,
|
|
252
|
+
});
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
257
|
+
await message.reply(`Error: ${msg}`);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function readPromptAttachment(message) {
|
|
262
|
+
const docAttachments = message.attachments.filter(a => isDocumentAttachment(a));
|
|
263
|
+
if (docAttachments.size === 0)
|
|
264
|
+
return {};
|
|
265
|
+
const results = await processAttachments([...docAttachments.values()]);
|
|
266
|
+
const firstOk = results.find(result => result.ok && result.text?.trim());
|
|
267
|
+
if (firstOk?.text) {
|
|
268
|
+
return {
|
|
269
|
+
text: firstOk.text.trim(),
|
|
270
|
+
filename: firstOk.filename,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const errors = results.map(result => result.error).filter(Boolean);
|
|
274
|
+
return {
|
|
275
|
+
error: errors.join('\n') || `Could not read attached prompt. Supported file types: ${supportedExtensions().map(e => `.${e}`).join(', ')}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function normalizeAliasArg(value) {
|
|
279
|
+
return (value || '').trim().replace(/^@/, '').toLowerCase();
|
|
280
|
+
}
|
|
281
|
+
function getAgentProfileCommandTarget(message, alias) {
|
|
282
|
+
const normalizedAlias = normalizeAliasArg(alias);
|
|
283
|
+
if (normalizedAlias) {
|
|
284
|
+
const profile = getAgentProfileByAlias(normalizedAlias);
|
|
285
|
+
return profile ? { profile } : { profile: null, error: `No Discord agent profile found for @${normalizedAlias}.` };
|
|
286
|
+
}
|
|
287
|
+
if (isDiscordThreadChannel(message.channel)) {
|
|
288
|
+
const record = getThreadAgentByThreadId(message.channel.id);
|
|
289
|
+
if (record)
|
|
290
|
+
return { profile: getAgentProfileByAlias(record.alias), record };
|
|
291
|
+
}
|
|
292
|
+
const profiles = listAgentProfiles();
|
|
293
|
+
if (profiles.length === 1)
|
|
294
|
+
return { profile: profiles[0] };
|
|
295
|
+
return {
|
|
296
|
+
profile: null,
|
|
297
|
+
error: `No target agent profile found. Pass an alias. Known profiles: ${profiles.map(profile => `@${profile.alias}`).join(', ') || '(none)'}.`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function getOrCreateAgentProfileCommandTarget(message, config, alias) {
|
|
301
|
+
const normalizedAlias = normalizeAliasArg(alias);
|
|
302
|
+
if (!normalizedAlias)
|
|
303
|
+
return getAgentProfileCommandTarget(message);
|
|
304
|
+
const existing = getAgentProfileByAlias(normalizedAlias);
|
|
305
|
+
if (existing)
|
|
306
|
+
return { profile: existing };
|
|
307
|
+
try {
|
|
308
|
+
return {
|
|
309
|
+
profile: upsertAgentProfile({
|
|
310
|
+
alias: normalizedAlias,
|
|
311
|
+
agentId: config.agents.default,
|
|
312
|
+
createdBy: message.author.id,
|
|
313
|
+
}),
|
|
314
|
+
created: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
319
|
+
return { profile: null, error: msg };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function handleEffortCommand(message, args) {
|
|
323
|
+
if (!args[0]) {
|
|
324
|
+
await message.reply(`Current effort: ${getCurrentThinking() || 'default'}\n${formatThinkingUsage()}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const next = parseThinkingLevel(args[0]);
|
|
328
|
+
if (!next) {
|
|
329
|
+
await message.reply(`Invalid effort: ${args[0]}\n${formatThinkingUsage()}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
setCurrentThinking(next);
|
|
333
|
+
await message.reply(`Effort set to: ${next}`);
|
|
334
|
+
}
|
|
335
|
+
async function handleThreadAgentCommand(message, args, config) {
|
|
336
|
+
const subcommand = (args[0] || 'show').toLowerCase();
|
|
337
|
+
if (subcommand === 'list') {
|
|
338
|
+
const profiles = listAgentProfiles();
|
|
339
|
+
if (profiles.length === 0) {
|
|
340
|
+
await message.reply('No Discord agent profiles configured.');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const profileText = profiles.map(formatAgentProfile).join('\n\n');
|
|
344
|
+
await sendLongText(message, `Profiles:\n${profileText}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const isThread = isDiscordThreadChannel(message.channel);
|
|
348
|
+
if (subcommand === 'show' || subcommand === 'status') {
|
|
349
|
+
if (!isThread) {
|
|
350
|
+
await message.reply(THREAD_AGENT_USAGE);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const record = getThreadAgentByThreadId(message.channel.id);
|
|
354
|
+
await message.reply(record ? formatThreadAgent(record) : `No agent profile is active in this thread.\n\n${THREAD_AGENT_USAGE}`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (subcommand === 'create') {
|
|
358
|
+
const alias = args[1];
|
|
359
|
+
const agentId = resolveConfiguredAgentId(config, args[2]);
|
|
360
|
+
if (!alias || !agentId) {
|
|
361
|
+
await message.reply(`Usage: /agent create <alias> [agent-id]\nConfigured agents: ${formatAgentIds(config)}`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const profile = upsertAgentProfile({
|
|
366
|
+
alias,
|
|
367
|
+
agentId,
|
|
368
|
+
createdBy: message.author.id,
|
|
369
|
+
});
|
|
370
|
+
await message.reply(`Configured agent profile ${formatAgentProfile(profile)}`);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
374
|
+
await message.reply(`Error: ${msg}`);
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (subcommand === 'use') {
|
|
379
|
+
const alias = args[1];
|
|
380
|
+
const profile = getAgentProfileByAlias(alias);
|
|
381
|
+
if (!alias || !profile) {
|
|
382
|
+
await message.reply(`Usage: /agent use <alias> [message]\nKnown profiles: ${listAgentProfiles().map(p => p.alias).join(', ') || '(none)'}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const initialPrompt = args.slice(2).join(' ').trim();
|
|
386
|
+
try {
|
|
387
|
+
const targetChannel = isThread
|
|
388
|
+
? message.channel
|
|
389
|
+
: await createThreadAgentThread(message, profile.alias, initialPrompt);
|
|
390
|
+
if (!targetChannel) {
|
|
391
|
+
await message.reply('Agent profiles can only run in a Discord thread or from a channel where I can create one.');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const record = bindThreadAgent({
|
|
395
|
+
threadId: targetChannel.id,
|
|
396
|
+
alias: profile.alias,
|
|
397
|
+
createdBy: message.author.id,
|
|
398
|
+
guildId: message.guildId,
|
|
399
|
+
channelId: targetChannel.isThread() ? targetChannel.parentId ?? message.channelId : message.channelId,
|
|
400
|
+
});
|
|
401
|
+
const url = buildThreadUrl(message.guildId, targetChannel.id);
|
|
402
|
+
await message.reply(!isThread && url
|
|
403
|
+
? `Started @${profile.alias}: ${url}`
|
|
404
|
+
: `This thread now uses ${formatAgentProfile(profile)}`);
|
|
405
|
+
if (initialPrompt) {
|
|
406
|
+
await runThreadAgentPrompt(message, targetChannel, record, initialPrompt, config, {
|
|
407
|
+
announceTask: !isThread,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
413
|
+
await message.reply(`Error: ${msg}`);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (subcommand === 'prompt') {
|
|
418
|
+
let alias;
|
|
419
|
+
let promptStart = 1;
|
|
420
|
+
const attachmentPrompt = await readPromptAttachment(message);
|
|
421
|
+
if (attachmentPrompt.error) {
|
|
422
|
+
await message.reply(attachmentPrompt.error);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (args[1]) {
|
|
426
|
+
const candidateAlias = normalizeAliasArg(args[1]);
|
|
427
|
+
if ((attachmentPrompt.text && args.length === 2) || (args.length > 2 && getAgentProfileByAlias(candidateAlias))) {
|
|
428
|
+
alias = args[1];
|
|
429
|
+
promptStart = 2;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const inlinePrompt = args.slice(promptStart).join(' ').trim();
|
|
433
|
+
const prompt = attachmentPrompt.text || inlinePrompt;
|
|
434
|
+
if (!prompt) {
|
|
435
|
+
await message.reply('Usage: /agent prompt [@alias] <prompt text>\nYou can also attach a .txt or .md file.');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (prompt.length > AGENT_PROMPT_MAX_CHARS) {
|
|
439
|
+
await message.reply(`Prompt is too long. Keep agent prompts under ${AGENT_PROMPT_MAX_CHARS.toLocaleString()} characters.`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const target = getAgentProfileCommandTarget(message, alias);
|
|
443
|
+
if (!target.profile) {
|
|
444
|
+
await message.reply(`${target.error || 'No target agent profile found.'}\n\n${THREAD_AGENT_USAGE}`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const profile = setAgentProfilePrompt(target.profile.alias, prompt);
|
|
448
|
+
if (!profile)
|
|
449
|
+
return;
|
|
450
|
+
const source = attachmentPrompt.filename ? ` from ${attachmentPrompt.filename}` : '';
|
|
451
|
+
await message.reply(`Updated prompt for @${profile.alias}${source}.`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (subcommand === 'model') {
|
|
455
|
+
let alias;
|
|
456
|
+
let modelStart = 1;
|
|
457
|
+
if (args[1] && args.length > 2) {
|
|
458
|
+
alias = args[1];
|
|
459
|
+
modelStart = 2;
|
|
460
|
+
}
|
|
461
|
+
const modelInput = args.slice(modelStart).join(' ').trim();
|
|
462
|
+
if (!modelInput) {
|
|
463
|
+
await message.reply(`Usage: /agent model [agent-alias] <model-alias|provider/model|model-id>\nAvailable model aliases: ${formatAliases(config)}`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const selection = resolveModelSelection(modelInput, config);
|
|
467
|
+
if (!selection.ok || !selection.resolved) {
|
|
468
|
+
await message.reply(formatModelSelectionError(selection.error || 'Invalid model selection', config));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const target = getOrCreateAgentProfileCommandTarget(message, config, alias);
|
|
472
|
+
if (!target.profile) {
|
|
473
|
+
await message.reply(`${target.error || 'No target agent profile found.'}\n\n${THREAD_AGENT_USAGE}`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const profile = setAgentProfileModel(target.profile.alias, selection.resolved);
|
|
477
|
+
if (!profile)
|
|
478
|
+
return;
|
|
479
|
+
const aliasText = selection.aliasUsed ? ` (${selection.aliasUsed})` : '';
|
|
480
|
+
const createdText = target.created ? `Created agent profile @${profile.alias} -> ${profile.agentId}\n` : '';
|
|
481
|
+
await message.reply(`${createdText}Updated model for @${profile.alias}: ${selection.resolved}${aliasText}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (subcommand === 'think' || subcommand === 'effort') {
|
|
485
|
+
let alias;
|
|
486
|
+
let effortArg = args[1];
|
|
487
|
+
if (args[1] && args.length > 2) {
|
|
488
|
+
alias = args[1];
|
|
489
|
+
effortArg = args[2];
|
|
490
|
+
}
|
|
491
|
+
const next = parseThinkingLevel(effortArg);
|
|
492
|
+
if (!effortArg || !next) {
|
|
493
|
+
await message.reply(`Usage: /agent ${subcommand} [agent-alias] <${THINKING_LEVELS.join('|')}>`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const target = getOrCreateAgentProfileCommandTarget(message, config, alias);
|
|
497
|
+
if (!target.profile) {
|
|
498
|
+
await message.reply(`${target.error || 'No target agent profile found.'}\n\n${THREAD_AGENT_USAGE}`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const profile = setAgentProfileThinking(target.profile.alias, next);
|
|
502
|
+
if (!profile)
|
|
503
|
+
return;
|
|
504
|
+
const createdText = target.created ? `Created agent profile @${profile.alias} -> ${profile.agentId}\n` : '';
|
|
505
|
+
await message.reply(`${createdText}Updated effort for @${profile.alias}: ${next}`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (subcommand === 'clear-model') {
|
|
509
|
+
const target = getAgentProfileCommandTarget(message, args[1]);
|
|
510
|
+
if (!target.profile) {
|
|
511
|
+
await message.reply(target.error || 'No target agent profile found.');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const profile = setAgentProfileModel(target.profile.alias, undefined);
|
|
515
|
+
await message.reply(profile ? `Cleared model override for @${profile.alias}.` : 'No target agent profile found.');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (subcommand === 'clear-think' || subcommand === 'clear-effort') {
|
|
519
|
+
const target = getAgentProfileCommandTarget(message, args[1]);
|
|
520
|
+
if (!target.profile) {
|
|
521
|
+
await message.reply(target.error || 'No target agent profile found.');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const profile = setAgentProfileThinking(target.profile.alias, undefined);
|
|
525
|
+
await message.reply(profile ? `Cleared effort override for @${profile.alias}.` : 'No target agent profile found.');
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (subcommand === 'clear-prompt') {
|
|
529
|
+
const target = getAgentProfileCommandTarget(message, args[1]);
|
|
530
|
+
if (!target.profile) {
|
|
531
|
+
await message.reply(target.error || 'No target agent profile found.');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const profile = setAgentProfilePrompt(target.profile.alias, undefined);
|
|
535
|
+
await message.reply(profile ? `Cleared prompt for @${profile.alias}.` : 'No target agent profile found.');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (subcommand === 'delete') {
|
|
539
|
+
const alias = args[1];
|
|
540
|
+
if (!alias) {
|
|
541
|
+
await message.reply('Usage: /agent delete <alias>');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const removed = removeAgentProfile(alias);
|
|
545
|
+
await message.reply(removed ? `Deleted agent profile @${normalizeAliasArg(alias)} and its thread bindings.` : `No Discord agent profile found for @${normalizeAliasArg(alias)}.`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
await message.reply(THREAD_AGENT_USAGE);
|
|
549
|
+
}
|
|
18
550
|
export async function handleCommand(message, command, args, config, silenceUntil, setSilenceUntil) {
|
|
19
551
|
const rawArgs = args.join(' ').trim();
|
|
20
552
|
if (command === 'start' || command === 'help') {
|
|
21
553
|
await sendLongText(message, buildHelpText(config));
|
|
22
554
|
return;
|
|
23
555
|
}
|
|
556
|
+
if (command === 'agent') {
|
|
557
|
+
await handleThreadAgentCommand(message, args, config);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (command === 'effort' || command === 'think') {
|
|
561
|
+
await handleEffortCommand(message, args);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
24
564
|
if (command === 'model') {
|
|
25
565
|
if (!rawArgs) {
|
|
26
566
|
const current = getCurrentModel();
|
|
@@ -45,6 +585,7 @@ export async function handleCommand(message, command, args, config, silenceUntil
|
|
|
45
585
|
}
|
|
46
586
|
if (command === 'status') {
|
|
47
587
|
const model = getCurrentModel();
|
|
588
|
+
const effort = getCurrentThinking();
|
|
48
589
|
const { getLastMessage } = await import('../../gateway.js');
|
|
49
590
|
const last = getLastMessage();
|
|
50
591
|
const jobs = getCronJobs();
|
|
@@ -77,6 +618,7 @@ export async function handleCommand(message, command, args, config, silenceUntil
|
|
|
77
618
|
}
|
|
78
619
|
await message.reply(`Agent: ${config.agents.default}\n` +
|
|
79
620
|
`Model: ${model}\n` +
|
|
621
|
+
`Effort: ${effort || 'default'}\n` +
|
|
80
622
|
`Last message: ${last?.toLocaleString() || 'never'}\n` +
|
|
81
623
|
`Silence until: ${silenceUntil?.toLocaleString() || 'not silenced'}\n\n` +
|
|
82
624
|
`${caLine}\n\n` +
|
|
@@ -267,6 +809,35 @@ export async function handleIncomingMessage(message, config) {
|
|
|
267
809
|
await message.reply('Too many messages. Please wait a moment.');
|
|
268
810
|
return;
|
|
269
811
|
}
|
|
812
|
+
// Interactive coding session intercept: if this is a thread bound to an active
|
|
813
|
+
// interactive session, route the message to --resume instead of the main agent.
|
|
814
|
+
if (message.channel.isThread()) {
|
|
815
|
+
try {
|
|
816
|
+
const session = getSession(message.channel.id);
|
|
817
|
+
if (session) {
|
|
818
|
+
const stopTyping = startTypingIndicator(message);
|
|
819
|
+
try {
|
|
820
|
+
await handleInteractiveThreadMessage({
|
|
821
|
+
discordThreadId: message.channel.id,
|
|
822
|
+
userMessage: message.content,
|
|
823
|
+
postToThread: async (chunks) => {
|
|
824
|
+
for (const c of chunks) {
|
|
825
|
+
await message.channel.send(c);
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
finally {
|
|
831
|
+
stopTyping();
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
console.error('[discord] Interactive-session intercept failed:', err);
|
|
838
|
+
// fall through to normal handling
|
|
839
|
+
}
|
|
840
|
+
}
|
|
270
841
|
// Check for image attachments
|
|
271
842
|
const imageAttachments = message.attachments.filter(a => a.contentType?.startsWith('image/'));
|
|
272
843
|
if (imageAttachments.size > 0) {
|
|
@@ -294,7 +865,8 @@ export async function handleIncomingMessage(message, config) {
|
|
|
294
865
|
];
|
|
295
866
|
const key = conversationKey(message);
|
|
296
867
|
const history = await getHistory(key);
|
|
297
|
-
const
|
|
868
|
+
const threadAgent = getThreadAgentForMessage(message, config);
|
|
869
|
+
const response = await runAgentTurn(threadAgent?.agentId || config.agents.default, content, config, threadAgent?.model || getCurrentModel(), getDiscordToolConfig(config), history, getThreadAgentRunContext(message, threadAgent));
|
|
298
870
|
await addToHistory(key, `[Image: ${caption}]`, response);
|
|
299
871
|
await sendLongText(message, response);
|
|
300
872
|
}
|
|
@@ -307,6 +879,53 @@ export async function handleIncomingMessage(message, config) {
|
|
|
307
879
|
}
|
|
308
880
|
return;
|
|
309
881
|
}
|
|
882
|
+
// Check for document attachments (txt, pdf, etc.)
|
|
883
|
+
const docAttachments = message.attachments.filter(a => isDocumentAttachment(a));
|
|
884
|
+
if (docAttachments.size > 0) {
|
|
885
|
+
const stopTyping = startTypingIndicator(message);
|
|
886
|
+
try {
|
|
887
|
+
const results = await processAttachments([...docAttachments.values()]);
|
|
888
|
+
const extracted = [];
|
|
889
|
+
const errors = [];
|
|
890
|
+
for (const r of results) {
|
|
891
|
+
if (r.ok && r.text) {
|
|
892
|
+
extracted.push(`--- ${r.filename} ---\n${r.text}`);
|
|
893
|
+
}
|
|
894
|
+
else if (r.error) {
|
|
895
|
+
errors.push(r.error);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (errors.length > 0 && extracted.length === 0) {
|
|
899
|
+
// All attachments failed
|
|
900
|
+
const supported = supportedExtensions().map(e => `.${e}`).join(', ');
|
|
901
|
+
await message.reply(errors.join('\n') + `\n\nSupported file types: ${supported}`);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
// Build context: extracted text + user's message (or default prompt)
|
|
905
|
+
const userText = message.content.trim() || 'Please read and summarize the attached file(s).';
|
|
906
|
+
const attachmentContext = extracted.join('\n\n');
|
|
907
|
+
const prompt = `The user uploaded the following file(s):\n\n${attachmentContext}\n\n${userText}`;
|
|
908
|
+
// Include any errors as a note
|
|
909
|
+
const errorNote = errors.length > 0
|
|
910
|
+
? `\n\n(Note: some attachments could not be processed: ${errors.join('; ')})`
|
|
911
|
+
: '';
|
|
912
|
+
const key = conversationKey(message);
|
|
913
|
+
const history = await getHistory(key);
|
|
914
|
+
const threadAgent = getThreadAgentForMessage(message, config);
|
|
915
|
+
const response = await runAgentTurn(threadAgent?.agentId || config.agents.default, prompt + errorNote, config, threadAgent?.model || getCurrentModel(), getDiscordToolConfig(config), history, getThreadAgentRunContext(message, threadAgent));
|
|
916
|
+
const filenames = results.map(r => r.filename).join(', ');
|
|
917
|
+
await addToHistory(key, `[Attachments: ${filenames}] ${userText}`, response);
|
|
918
|
+
await sendLongText(message, response);
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
922
|
+
await message.reply(`Error processing attachment(s): ${msg}`);
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
stopTyping();
|
|
926
|
+
}
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
310
929
|
// Check for voice message attachments
|
|
311
930
|
const voiceAttachments = message.attachments.filter(a => a.contentType?.startsWith('audio/') || a.contentType?.startsWith('voice/'));
|
|
312
931
|
if (voiceAttachments.size > 0) {
|
|
@@ -335,7 +954,8 @@ export async function handleIncomingMessage(message, config) {
|
|
|
335
954
|
}
|
|
336
955
|
const key = conversationKey(message);
|
|
337
956
|
const history = await getHistory(key);
|
|
338
|
-
const
|
|
957
|
+
const threadAgent = getThreadAgentForMessage(message, config);
|
|
958
|
+
const agentResponse = await runAgentTurn(threadAgent?.agentId || config.agents.default, transcription, config, threadAgent?.model || getCurrentModel(), getDiscordToolConfig(config), history, getThreadAgentRunContext(message, threadAgent));
|
|
339
959
|
await addToHistory(key, transcription, agentResponse);
|
|
340
960
|
console.log('[discord] TTS check - sendVoice:', config.voice?.channels?.['discord']?.sendVoice);
|
|
341
961
|
if (config.voice?.channels?.['discord']?.sendVoice) {
|
|
@@ -343,7 +963,7 @@ export async function handleIncomingMessage(message, config) {
|
|
|
343
963
|
try {
|
|
344
964
|
const speech = await synthesizeSpeech(agentResponse, config.voice);
|
|
345
965
|
console.log('[discord] TTS synthesis success:', speech.format, speech.provider, 'buffer size:', speech.buffer.length);
|
|
346
|
-
const voiceAttachment = new AttachmentBuilder(speech.buffer, {
|
|
966
|
+
const voiceAttachment = new AttachmentBuilder(Buffer.from(speech.buffer), {
|
|
347
967
|
name: `voice-reply.${speech.format}`,
|
|
348
968
|
description: 'Voice reply'
|
|
349
969
|
});
|
|
@@ -376,6 +996,9 @@ export async function handleIncomingMessage(message, config) {
|
|
|
376
996
|
const text = message.content.trim();
|
|
377
997
|
if (!text)
|
|
378
998
|
return;
|
|
999
|
+
if (await runMentionedAgentPrompt(message, text, config)) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
379
1002
|
const isPrefixedCommand = text.startsWith('/') || text.startsWith('!');
|
|
380
1003
|
const isDm = message.channel.isDMBased();
|
|
381
1004
|
if (isPrefixedCommand || isDm) {
|
|
@@ -398,9 +1021,73 @@ export async function handleIncomingMessage(message, config) {
|
|
|
398
1021
|
const stopTyping = startTypingIndicator(message);
|
|
399
1022
|
try {
|
|
400
1023
|
const history = await getHistory(key);
|
|
401
|
-
const
|
|
1024
|
+
const threadAgent = getThreadAgentForMessage(message, config);
|
|
1025
|
+
const codeAgentContext = buildCodeAgentThreadContext(message);
|
|
1026
|
+
const prompt = codeAgentContext
|
|
1027
|
+
? `${codeAgentContext}\n\nUser message in this Discord thread:\n${text}`
|
|
1028
|
+
: text;
|
|
1029
|
+
const response = await runAgentTurn(threadAgent?.agentId || config.agents.default, prompt, config, threadAgent?.model || getCurrentModel(), getDiscordToolConfig(config), history, getThreadAgentRunContext(message, threadAgent));
|
|
402
1030
|
await addToHistory(key, text, response);
|
|
403
1031
|
await sendLongText(message, response);
|
|
1032
|
+
// If the response started coding agent(s), create threads for status updates.
|
|
1033
|
+
// Only consider tasks started within the current turn (last 2 min) — older
|
|
1034
|
+
// unthreaded tasks are stale registry entries that will steal the single
|
|
1035
|
+
// thread Discord allows per message.
|
|
1036
|
+
const useThreads = config.channels.discord?.threadedReplies !== false;
|
|
1037
|
+
if (useThreads && !message.channel.isDMBased()) {
|
|
1038
|
+
try {
|
|
1039
|
+
const { getUnthreadedTasksForChat, writeCodeAgentTask } = await import('../../code-agents/registry.js');
|
|
1040
|
+
const chatId = Number(message.channel.id);
|
|
1041
|
+
const freshnessCutoffMs = Date.now() - 2 * 60 * 1000;
|
|
1042
|
+
const unthreadedTasks = getUnthreadedTasksForChat(chatId).filter(t => {
|
|
1043
|
+
const started = Date.parse(t.startedAt);
|
|
1044
|
+
return Number.isFinite(started) && started >= freshnessCutoffMs;
|
|
1045
|
+
});
|
|
1046
|
+
const isThread = message.channel.isThread();
|
|
1047
|
+
for (const task of unthreadedTasks) {
|
|
1048
|
+
let assignedThreadId;
|
|
1049
|
+
if (isThread) {
|
|
1050
|
+
// Already in a thread — use it directly instead of creating a sub-thread
|
|
1051
|
+
task.discordThreadId = message.channel.id;
|
|
1052
|
+
task.discordChannelId = message.channel.parentId ?? message.channelId;
|
|
1053
|
+
writeCodeAgentTask(task);
|
|
1054
|
+
assignedThreadId = task.discordThreadId;
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
const taskPreview = task.task.length > 90 ? task.task.slice(0, 90) + '...' : task.task;
|
|
1058
|
+
const threadId = await createTaskThread(message, task.id, taskPreview);
|
|
1059
|
+
if (threadId) {
|
|
1060
|
+
task.discordThreadId = threadId;
|
|
1061
|
+
task.discordChannelId = message.channelId;
|
|
1062
|
+
writeCodeAgentTask(task);
|
|
1063
|
+
assignedThreadId = threadId;
|
|
1064
|
+
// Post a clickable link to the new thread for easy mobile access.
|
|
1065
|
+
const url = buildThreadUrl(message.guildId, threadId);
|
|
1066
|
+
if (url) {
|
|
1067
|
+
try {
|
|
1068
|
+
await message.channel.send(`→ ${task.interactive ? 'Interactive session' : 'Thread'} ${task.id}: ${url}`);
|
|
1069
|
+
}
|
|
1070
|
+
catch (err) {
|
|
1071
|
+
console.warn(`[discord] Failed to post thread link for ${task.id}:`, err);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// Promote any pending interactive session from taskId-keyed to threadId-keyed.
|
|
1077
|
+
if (assignedThreadId && task.interactive) {
|
|
1078
|
+
try {
|
|
1079
|
+
linkThread(task.id, assignedThreadId);
|
|
1080
|
+
}
|
|
1081
|
+
catch (err) {
|
|
1082
|
+
console.error(`[discord] Failed to link interactive session ${task.id} → ${assignedThreadId}:`, err);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
console.error(`[discord] Failed to create threads for spawned tasks:`, err);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
404
1091
|
}
|
|
405
1092
|
catch (error) {
|
|
406
1093
|
const msg = error instanceof Error ? error.message : 'Unknown error';
|