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,23 @@
|
|
|
1
1
|
import type { Config } from '../../types.js';
|
|
2
|
+
import { type DiscordTextAttachment } from './threads.js';
|
|
2
3
|
export declare function initDiscord(cfg: Config): Promise<boolean>;
|
|
3
4
|
export declare function startDiscord(): Promise<void>;
|
|
4
5
|
export declare function stopDiscord(): Promise<void>;
|
|
5
6
|
export declare function isDiscordSilenced(): boolean;
|
|
6
7
|
export declare function getDiscordDefaultTarget(cfg: Config): string | null;
|
|
7
8
|
export declare function sendDiscordProactiveMessage(target: string | number, message: string): Promise<void>;
|
|
8
|
-
export declare function
|
|
9
|
+
export declare function sendDiscordProactiveMessageWithAttachments(target: string | number, message: string, attachments: DiscordTextAttachment[]): Promise<void>;
|
|
10
|
+
export declare function sendDiscordProactiveVoice(target: string | number, buffer: Uint8Array, format: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Send a message to a Discord thread by ID.
|
|
13
|
+
* Used by code-agent notifications to route updates to task-specific threads.
|
|
14
|
+
* Returns true if sent successfully, false if client unavailable or thread not found.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sendToDiscordThread(threadId: string, message: string): Promise<boolean>;
|
|
17
|
+
export declare function sendToDiscordThreadWithAttachments(threadId: string, message: string, attachments: DiscordTextAttachment[]): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Send a message with optional voice attachment to a Discord thread by ID.
|
|
20
|
+
* Used by cron jobs to route both text and voice output to the same thread.
|
|
21
|
+
* Returns true if sent successfully, false if client unavailable or thread not found.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sendToDiscordThreadWithVoice(threadId: string, message: string, voiceBuffer?: Uint8Array, voiceFormat?: string): Promise<boolean>;
|
|
@@ -3,6 +3,9 @@ import { onApprovalEvent } from '../../exec-approval.js';
|
|
|
3
3
|
import { KNOWN_COMMANDS } from './types.js';
|
|
4
4
|
import { handleCommand, handleIncomingMessage, handleInteraction, sendApprovalCard } from './handlers.js';
|
|
5
5
|
import { splitToChunks } from './utils.js';
|
|
6
|
+
import { sendToThread, sendToThreadWithAttachments, sendToThreadWithVoice } from './threads.js';
|
|
7
|
+
import { registerDelegateToAgentHandler } from '../../tools/agent-delegation.js';
|
|
8
|
+
import { createDiscordAgentDelegateHandler } from './delegation.js';
|
|
6
9
|
let client = null;
|
|
7
10
|
let config;
|
|
8
11
|
let silenceUntil = null;
|
|
@@ -10,6 +13,7 @@ export async function initDiscord(cfg) {
|
|
|
10
13
|
const discord = cfg.channels.discord;
|
|
11
14
|
if (!discord?.enabled || !discord.token) {
|
|
12
15
|
console.log('[discord] Disabled or no token configured');
|
|
16
|
+
registerDelegateToAgentHandler(null);
|
|
13
17
|
return false;
|
|
14
18
|
}
|
|
15
19
|
config = cfg;
|
|
@@ -22,6 +26,7 @@ export async function initDiscord(cfg) {
|
|
|
22
26
|
],
|
|
23
27
|
partials: [Partials.Channel],
|
|
24
28
|
});
|
|
29
|
+
registerDelegateToAgentHandler(createDiscordAgentDelegateHandler(() => client));
|
|
25
30
|
client.on('messageCreate', (message) => {
|
|
26
31
|
if (message.author.bot)
|
|
27
32
|
return;
|
|
@@ -89,6 +94,7 @@ export async function stopDiscord() {
|
|
|
89
94
|
if (!client)
|
|
90
95
|
return;
|
|
91
96
|
client.destroy();
|
|
97
|
+
registerDelegateToAgentHandler(null);
|
|
92
98
|
console.log('[discord] Bot stopped');
|
|
93
99
|
}
|
|
94
100
|
export function isDiscordSilenced() {
|
|
@@ -110,11 +116,21 @@ export function getDiscordDefaultTarget(cfg) {
|
|
|
110
116
|
return null;
|
|
111
117
|
}
|
|
112
118
|
async function sendChunked(target, text) {
|
|
119
|
+
if (!text || !text.trim())
|
|
120
|
+
return;
|
|
113
121
|
const chunks = splitToChunks(text, 1900);
|
|
114
122
|
for (const chunk of chunks) {
|
|
115
123
|
await target.send(chunk);
|
|
116
124
|
}
|
|
117
125
|
}
|
|
126
|
+
function buildDiscordAttachments(files) {
|
|
127
|
+
return files
|
|
128
|
+
.filter(file => file.content.trim())
|
|
129
|
+
.map(file => new AttachmentBuilder(Buffer.from(file.content, 'utf-8'), {
|
|
130
|
+
name: file.name,
|
|
131
|
+
description: file.description,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
118
134
|
export async function sendDiscordProactiveMessage(target, message) {
|
|
119
135
|
if (!client || isDiscordSilenced())
|
|
120
136
|
return;
|
|
@@ -129,11 +145,33 @@ export async function sendDiscordProactiveMessage(target, message) {
|
|
|
129
145
|
await sendChunked(user, message);
|
|
130
146
|
}
|
|
131
147
|
}
|
|
148
|
+
export async function sendDiscordProactiveMessageWithAttachments(target, message, attachments) {
|
|
149
|
+
if (!client || isDiscordSilenced())
|
|
150
|
+
return;
|
|
151
|
+
const targetId = String(target);
|
|
152
|
+
const chunks = splitToChunks(message || '(No summary generated.)', 1900);
|
|
153
|
+
const files = buildDiscordAttachments(attachments);
|
|
154
|
+
const channel = await client.channels.fetch(targetId).catch(() => null);
|
|
155
|
+
if (channel && 'send' in channel && typeof channel.send === 'function') {
|
|
156
|
+
await channel.send({ content: chunks[0], files });
|
|
157
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
158
|
+
await channel.send(chunks[i]);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const user = await client.users.fetch(targetId).catch(() => null);
|
|
163
|
+
if (user) {
|
|
164
|
+
await user.send({ content: chunks[0], files });
|
|
165
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
166
|
+
await user.send(chunks[i]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
132
170
|
export async function sendDiscordProactiveVoice(target, buffer, format) {
|
|
133
171
|
if (!client || isDiscordSilenced())
|
|
134
172
|
return;
|
|
135
173
|
const targetId = String(target);
|
|
136
|
-
const attachment = new AttachmentBuilder(buffer, {
|
|
174
|
+
const attachment = new AttachmentBuilder(Buffer.from(buffer), {
|
|
137
175
|
name: `voice.${format}`,
|
|
138
176
|
description: 'Voice message',
|
|
139
177
|
});
|
|
@@ -147,3 +185,28 @@ export async function sendDiscordProactiveVoice(target, buffer, format) {
|
|
|
147
185
|
await user.send({ files: [attachment] });
|
|
148
186
|
}
|
|
149
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Send a message to a Discord thread by ID.
|
|
190
|
+
* Used by code-agent notifications to route updates to task-specific threads.
|
|
191
|
+
* Returns true if sent successfully, false if client unavailable or thread not found.
|
|
192
|
+
*/
|
|
193
|
+
export async function sendToDiscordThread(threadId, message) {
|
|
194
|
+
if (!client)
|
|
195
|
+
return false;
|
|
196
|
+
return sendToThread(client, threadId, message);
|
|
197
|
+
}
|
|
198
|
+
export async function sendToDiscordThreadWithAttachments(threadId, message, attachments) {
|
|
199
|
+
if (!client)
|
|
200
|
+
return false;
|
|
201
|
+
return sendToThreadWithAttachments(client, threadId, message, attachments);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Send a message with optional voice attachment to a Discord thread by ID.
|
|
205
|
+
* Used by cron jobs to route both text and voice output to the same thread.
|
|
206
|
+
* Returns true if sent successfully, false if client unavailable or thread not found.
|
|
207
|
+
*/
|
|
208
|
+
export async function sendToDiscordThreadWithVoice(threadId, message, voiceBuffer, voiceFormat) {
|
|
209
|
+
if (!client)
|
|
210
|
+
return false;
|
|
211
|
+
return sendToThreadWithVoice(client, threadId, message, voiceBuffer, voiceFormat);
|
|
212
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ThinkingLevel } from '../../types.js';
|
|
2
|
+
export interface DiscordAgentProfile {
|
|
3
|
+
alias: string;
|
|
4
|
+
agentId: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
thinking?: ThinkingLevel;
|
|
7
|
+
promptOverlay?: string;
|
|
8
|
+
createdBy: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DiscordThreadAgentBinding {
|
|
13
|
+
threadId: string;
|
|
14
|
+
profileAlias: string;
|
|
15
|
+
guildId?: string;
|
|
16
|
+
channelId?: string;
|
|
17
|
+
createdBy: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DiscordThreadAgent extends DiscordAgentProfile {
|
|
22
|
+
threadId: string;
|
|
23
|
+
guildId?: string;
|
|
24
|
+
channelId?: string;
|
|
25
|
+
profileAlias: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DiscordAgentMentionInvocation {
|
|
28
|
+
alias: string;
|
|
29
|
+
prompt: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function normalizeThreadAgentAlias(value: string | undefined): string | null;
|
|
32
|
+
export declare function parseDiscordAgentMention(value: string): DiscordAgentMentionInvocation | null;
|
|
33
|
+
export declare function listAgentProfiles(): DiscordAgentProfile[];
|
|
34
|
+
export declare function listThreadAgentBindings(): DiscordThreadAgentBinding[];
|
|
35
|
+
export declare function getAgentProfileByAlias(alias: string | undefined): DiscordAgentProfile | null;
|
|
36
|
+
export declare function getThreadAgentByThreadId(threadId: string | undefined): DiscordThreadAgent | null;
|
|
37
|
+
export declare function getThreadAgentBinding(threadId: string | undefined): DiscordThreadAgentBinding | null;
|
|
38
|
+
export declare function upsertAgentProfile(input: {
|
|
39
|
+
alias: string;
|
|
40
|
+
agentId: string;
|
|
41
|
+
createdBy: string;
|
|
42
|
+
}): DiscordAgentProfile;
|
|
43
|
+
export declare function bindThreadAgent(input: {
|
|
44
|
+
threadId: string;
|
|
45
|
+
alias: string;
|
|
46
|
+
createdBy: string;
|
|
47
|
+
guildId?: string | null;
|
|
48
|
+
channelId?: string | null;
|
|
49
|
+
}): DiscordThreadAgent;
|
|
50
|
+
export declare function setAgentProfilePrompt(alias: string, promptOverlay: string | undefined): DiscordAgentProfile | null;
|
|
51
|
+
export declare function setAgentProfileModel(alias: string, model: string | undefined): DiscordAgentProfile | null;
|
|
52
|
+
export declare function setAgentProfileThinking(alias: string, thinking: ThinkingLevel | undefined): DiscordAgentProfile | null;
|
|
53
|
+
export declare function removeAgentProfile(alias: string): boolean;
|
|
54
|
+
export declare function _setThreadAgentStorePathForTesting(path: string | null): void;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
const DEFAULT_STORE_PATH = join(homedir(), '.skimpyclaw', 'discord-thread-agents.json');
|
|
5
|
+
const ALIAS_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
6
|
+
const THINKING_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh']);
|
|
7
|
+
let storePathOverride = null;
|
|
8
|
+
let loaded = false;
|
|
9
|
+
const profilesByAlias = new Map();
|
|
10
|
+
const bindingsByThreadId = new Map();
|
|
11
|
+
function getStorePath() {
|
|
12
|
+
return storePathOverride || DEFAULT_STORE_PATH;
|
|
13
|
+
}
|
|
14
|
+
export function normalizeThreadAgentAlias(value) {
|
|
15
|
+
const normalized = (value || '').trim().replace(/^@/, '').toLowerCase();
|
|
16
|
+
if (!ALIAS_RE.test(normalized))
|
|
17
|
+
return null;
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
export function parseDiscordAgentMention(value) {
|
|
21
|
+
const match = value.trim().match(/^@([a-z][a-z0-9_-]{0,63})(?:\s+([\s\S]*))?$/i);
|
|
22
|
+
if (!match)
|
|
23
|
+
return null;
|
|
24
|
+
const alias = normalizeThreadAgentAlias(match[1]);
|
|
25
|
+
if (!alias)
|
|
26
|
+
return null;
|
|
27
|
+
return {
|
|
28
|
+
alias,
|
|
29
|
+
prompt: (match[2] || '').trim(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function normalizeThinking(value) {
|
|
33
|
+
return typeof value === 'string' && THINKING_LEVELS.has(value)
|
|
34
|
+
? value
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
37
|
+
function normalizeProfile(value) {
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return null;
|
|
40
|
+
const raw = value;
|
|
41
|
+
const alias = normalizeThreadAgentAlias(raw.alias);
|
|
42
|
+
const agentId = typeof raw.agentId === 'string' ? raw.agentId.trim() : '';
|
|
43
|
+
const createdBy = typeof raw.createdBy === 'string' ? raw.createdBy.trim() : '';
|
|
44
|
+
const createdAt = typeof raw.createdAt === 'string' ? raw.createdAt : new Date().toISOString();
|
|
45
|
+
const updatedAt = typeof raw.updatedAt === 'string' ? raw.updatedAt : createdAt;
|
|
46
|
+
if (!alias || !agentId || !createdBy)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
alias,
|
|
50
|
+
agentId,
|
|
51
|
+
model: typeof raw.model === 'string' && raw.model.trim() ? raw.model.trim() : undefined,
|
|
52
|
+
thinking: normalizeThinking(raw.thinking),
|
|
53
|
+
promptOverlay: typeof raw.promptOverlay === 'string' && raw.promptOverlay.trim()
|
|
54
|
+
? raw.promptOverlay.trim()
|
|
55
|
+
: undefined,
|
|
56
|
+
createdBy,
|
|
57
|
+
createdAt,
|
|
58
|
+
updatedAt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function normalizeBinding(value) {
|
|
62
|
+
if (!value || typeof value !== 'object')
|
|
63
|
+
return null;
|
|
64
|
+
const raw = value;
|
|
65
|
+
const threadId = typeof raw.threadId === 'string' ? raw.threadId.trim() : '';
|
|
66
|
+
const profileAlias = normalizeThreadAgentAlias(raw.profileAlias);
|
|
67
|
+
const createdBy = typeof raw.createdBy === 'string' ? raw.createdBy.trim() : '';
|
|
68
|
+
const createdAt = typeof raw.createdAt === 'string' ? raw.createdAt : new Date().toISOString();
|
|
69
|
+
const updatedAt = typeof raw.updatedAt === 'string' ? raw.updatedAt : createdAt;
|
|
70
|
+
if (!threadId || !profileAlias || !createdBy)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
threadId,
|
|
74
|
+
profileAlias,
|
|
75
|
+
guildId: typeof raw.guildId === 'string' && raw.guildId.trim() ? raw.guildId.trim() : undefined,
|
|
76
|
+
channelId: typeof raw.channelId === 'string' && raw.channelId.trim() ? raw.channelId.trim() : undefined,
|
|
77
|
+
createdBy,
|
|
78
|
+
createdAt,
|
|
79
|
+
updatedAt,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function normalizeLegacyRecord(value) {
|
|
83
|
+
if (!value || typeof value !== 'object')
|
|
84
|
+
return null;
|
|
85
|
+
const raw = value;
|
|
86
|
+
const alias = normalizeThreadAgentAlias(raw.alias);
|
|
87
|
+
const threadId = typeof raw.threadId === 'string' ? raw.threadId.trim() : '';
|
|
88
|
+
const agentId = typeof raw.agentId === 'string' ? raw.agentId.trim() : '';
|
|
89
|
+
const createdBy = typeof raw.createdBy === 'string' ? raw.createdBy.trim() : '';
|
|
90
|
+
const createdAt = typeof raw.createdAt === 'string' ? raw.createdAt : new Date().toISOString();
|
|
91
|
+
const updatedAt = typeof raw.updatedAt === 'string' ? raw.updatedAt : createdAt;
|
|
92
|
+
if (!alias || !threadId || !agentId || !createdBy)
|
|
93
|
+
return null;
|
|
94
|
+
return {
|
|
95
|
+
profile: {
|
|
96
|
+
alias,
|
|
97
|
+
agentId,
|
|
98
|
+
model: typeof raw.model === 'string' && raw.model.trim() ? raw.model.trim() : undefined,
|
|
99
|
+
thinking: normalizeThinking(raw.thinking),
|
|
100
|
+
promptOverlay: typeof raw.promptOverlay === 'string' && raw.promptOverlay.trim()
|
|
101
|
+
? raw.promptOverlay.trim()
|
|
102
|
+
: undefined,
|
|
103
|
+
createdBy,
|
|
104
|
+
createdAt,
|
|
105
|
+
updatedAt,
|
|
106
|
+
},
|
|
107
|
+
binding: {
|
|
108
|
+
threadId,
|
|
109
|
+
profileAlias: alias,
|
|
110
|
+
guildId: typeof raw.guildId === 'string' && raw.guildId.trim() ? raw.guildId.trim() : undefined,
|
|
111
|
+
channelId: typeof raw.channelId === 'string' && raw.channelId.trim() ? raw.channelId.trim() : undefined,
|
|
112
|
+
createdBy,
|
|
113
|
+
createdAt,
|
|
114
|
+
updatedAt,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function resolveBinding(binding) {
|
|
119
|
+
if (!binding)
|
|
120
|
+
return null;
|
|
121
|
+
const profile = profilesByAlias.get(binding.profileAlias);
|
|
122
|
+
if (!profile)
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
...profile,
|
|
126
|
+
threadId: binding.threadId,
|
|
127
|
+
profileAlias: binding.profileAlias,
|
|
128
|
+
guildId: binding.guildId,
|
|
129
|
+
channelId: binding.channelId,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function ensureLoaded() {
|
|
133
|
+
if (loaded)
|
|
134
|
+
return;
|
|
135
|
+
loaded = true;
|
|
136
|
+
const storePath = getStorePath();
|
|
137
|
+
if (!existsSync(storePath))
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(readFileSync(storePath, 'utf-8'));
|
|
141
|
+
if (Array.isArray(parsed)) {
|
|
142
|
+
for (const entry of parsed) {
|
|
143
|
+
const normalized = normalizeLegacyRecord(entry);
|
|
144
|
+
if (!normalized)
|
|
145
|
+
continue;
|
|
146
|
+
profilesByAlias.set(normalized.profile.alias, normalized.profile);
|
|
147
|
+
bindingsByThreadId.set(normalized.binding.threadId, normalized.binding);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const profiles = Array.isArray(parsed?.profiles) ? parsed.profiles : [];
|
|
152
|
+
const bindings = Array.isArray(parsed?.bindings) ? parsed.bindings : [];
|
|
153
|
+
for (const entry of profiles) {
|
|
154
|
+
const normalized = normalizeProfile(entry);
|
|
155
|
+
if (normalized)
|
|
156
|
+
profilesByAlias.set(normalized.alias, normalized);
|
|
157
|
+
}
|
|
158
|
+
for (const entry of bindings) {
|
|
159
|
+
const normalized = normalizeBinding(entry);
|
|
160
|
+
if (normalized)
|
|
161
|
+
bindingsByThreadId.set(normalized.threadId, normalized);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.error('[discord-thread-agents] Failed to load store:', err);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function persist() {
|
|
169
|
+
try {
|
|
170
|
+
const storePath = getStorePath();
|
|
171
|
+
mkdirSync(dirname(storePath), { recursive: true });
|
|
172
|
+
const store = {
|
|
173
|
+
version: 2,
|
|
174
|
+
profiles: listAgentProfiles(),
|
|
175
|
+
bindings: listThreadAgentBindings(),
|
|
176
|
+
};
|
|
177
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
178
|
+
chmodSync(storePath, 0o600);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error('[discord-thread-agents] Failed to persist store:', err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function listAgentProfiles() {
|
|
185
|
+
ensureLoaded();
|
|
186
|
+
return Array.from(profilesByAlias.values()).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
187
|
+
}
|
|
188
|
+
export function listThreadAgentBindings() {
|
|
189
|
+
ensureLoaded();
|
|
190
|
+
return Array.from(bindingsByThreadId.values()).sort((a, b) => a.threadId.localeCompare(b.threadId));
|
|
191
|
+
}
|
|
192
|
+
export function getAgentProfileByAlias(alias) {
|
|
193
|
+
ensureLoaded();
|
|
194
|
+
const normalized = normalizeThreadAgentAlias(alias);
|
|
195
|
+
if (!normalized)
|
|
196
|
+
return null;
|
|
197
|
+
return profilesByAlias.get(normalized) || null;
|
|
198
|
+
}
|
|
199
|
+
export function getThreadAgentByThreadId(threadId) {
|
|
200
|
+
ensureLoaded();
|
|
201
|
+
const id = (threadId || '').trim();
|
|
202
|
+
if (!id)
|
|
203
|
+
return null;
|
|
204
|
+
return resolveBinding(bindingsByThreadId.get(id));
|
|
205
|
+
}
|
|
206
|
+
export function getThreadAgentBinding(threadId) {
|
|
207
|
+
ensureLoaded();
|
|
208
|
+
const id = (threadId || '').trim();
|
|
209
|
+
if (!id)
|
|
210
|
+
return null;
|
|
211
|
+
return bindingsByThreadId.get(id) || null;
|
|
212
|
+
}
|
|
213
|
+
export function upsertAgentProfile(input) {
|
|
214
|
+
ensureLoaded();
|
|
215
|
+
const alias = normalizeThreadAgentAlias(input.alias);
|
|
216
|
+
const agentId = input.agentId.trim();
|
|
217
|
+
const createdBy = input.createdBy.trim();
|
|
218
|
+
if (!alias)
|
|
219
|
+
throw new Error('Alias must start with a letter and use only letters, numbers, underscore, or dash.');
|
|
220
|
+
if (!agentId)
|
|
221
|
+
throw new Error('Agent ID is required.');
|
|
222
|
+
if (!createdBy)
|
|
223
|
+
throw new Error('Creator ID is required.');
|
|
224
|
+
const existing = profilesByAlias.get(alias);
|
|
225
|
+
const now = new Date().toISOString();
|
|
226
|
+
const profile = {
|
|
227
|
+
alias,
|
|
228
|
+
agentId,
|
|
229
|
+
createdBy: existing?.createdBy || createdBy,
|
|
230
|
+
createdAt: existing?.createdAt || now,
|
|
231
|
+
updatedAt: now,
|
|
232
|
+
model: existing?.model,
|
|
233
|
+
thinking: existing?.thinking,
|
|
234
|
+
promptOverlay: existing?.promptOverlay,
|
|
235
|
+
};
|
|
236
|
+
profilesByAlias.set(alias, profile);
|
|
237
|
+
persist();
|
|
238
|
+
return profile;
|
|
239
|
+
}
|
|
240
|
+
export function bindThreadAgent(input) {
|
|
241
|
+
ensureLoaded();
|
|
242
|
+
const threadId = input.threadId.trim();
|
|
243
|
+
const profileAlias = normalizeThreadAgentAlias(input.alias);
|
|
244
|
+
const createdBy = input.createdBy.trim();
|
|
245
|
+
if (!threadId)
|
|
246
|
+
throw new Error('Thread ID is required.');
|
|
247
|
+
if (!profileAlias)
|
|
248
|
+
throw new Error('Agent alias is required.');
|
|
249
|
+
if (!createdBy)
|
|
250
|
+
throw new Error('Creator ID is required.');
|
|
251
|
+
if (!profilesByAlias.has(profileAlias))
|
|
252
|
+
throw new Error(`Agent profile "${profileAlias}" does not exist.`);
|
|
253
|
+
const existing = bindingsByThreadId.get(threadId);
|
|
254
|
+
const now = new Date().toISOString();
|
|
255
|
+
const binding = {
|
|
256
|
+
threadId,
|
|
257
|
+
profileAlias,
|
|
258
|
+
createdBy: existing?.createdBy || createdBy,
|
|
259
|
+
createdAt: existing?.createdAt || now,
|
|
260
|
+
updatedAt: now,
|
|
261
|
+
guildId: input.guildId?.trim() || existing?.guildId,
|
|
262
|
+
channelId: input.channelId?.trim() || existing?.channelId,
|
|
263
|
+
};
|
|
264
|
+
bindingsByThreadId.set(threadId, binding);
|
|
265
|
+
persist();
|
|
266
|
+
const resolved = resolveBinding(binding);
|
|
267
|
+
if (!resolved)
|
|
268
|
+
throw new Error(`Agent profile "${profileAlias}" does not exist.`);
|
|
269
|
+
return resolved;
|
|
270
|
+
}
|
|
271
|
+
export function setAgentProfilePrompt(alias, promptOverlay) {
|
|
272
|
+
ensureLoaded();
|
|
273
|
+
const profile = getAgentProfileByAlias(alias);
|
|
274
|
+
if (!profile)
|
|
275
|
+
return null;
|
|
276
|
+
const nextPrompt = promptOverlay?.trim();
|
|
277
|
+
profile.promptOverlay = nextPrompt || undefined;
|
|
278
|
+
profile.updatedAt = new Date().toISOString();
|
|
279
|
+
persist();
|
|
280
|
+
return profile;
|
|
281
|
+
}
|
|
282
|
+
export function setAgentProfileModel(alias, model) {
|
|
283
|
+
ensureLoaded();
|
|
284
|
+
const profile = getAgentProfileByAlias(alias);
|
|
285
|
+
if (!profile)
|
|
286
|
+
return null;
|
|
287
|
+
const nextModel = model?.trim();
|
|
288
|
+
profile.model = nextModel || undefined;
|
|
289
|
+
profile.updatedAt = new Date().toISOString();
|
|
290
|
+
persist();
|
|
291
|
+
return profile;
|
|
292
|
+
}
|
|
293
|
+
export function setAgentProfileThinking(alias, thinking) {
|
|
294
|
+
ensureLoaded();
|
|
295
|
+
const profile = getAgentProfileByAlias(alias);
|
|
296
|
+
if (!profile)
|
|
297
|
+
return null;
|
|
298
|
+
profile.thinking = thinking;
|
|
299
|
+
profile.updatedAt = new Date().toISOString();
|
|
300
|
+
persist();
|
|
301
|
+
return profile;
|
|
302
|
+
}
|
|
303
|
+
export function removeAgentProfile(alias) {
|
|
304
|
+
ensureLoaded();
|
|
305
|
+
const normalized = normalizeThreadAgentAlias(alias);
|
|
306
|
+
if (!normalized)
|
|
307
|
+
return false;
|
|
308
|
+
const deleted = profilesByAlias.delete(normalized);
|
|
309
|
+
if (deleted) {
|
|
310
|
+
for (const [threadId, binding] of bindingsByThreadId) {
|
|
311
|
+
if (binding.profileAlias === normalized)
|
|
312
|
+
bindingsByThreadId.delete(threadId);
|
|
313
|
+
}
|
|
314
|
+
persist();
|
|
315
|
+
}
|
|
316
|
+
return deleted;
|
|
317
|
+
}
|
|
318
|
+
export function _setThreadAgentStorePathForTesting(path) {
|
|
319
|
+
storePathOverride = path;
|
|
320
|
+
profilesByAlias.clear();
|
|
321
|
+
bindingsByThreadId.clear();
|
|
322
|
+
loaded = false;
|
|
323
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord thread management for coding agent task updates.
|
|
3
|
+
*
|
|
4
|
+
* Creates threads from triggering messages and routes status updates
|
|
5
|
+
* to those threads instead of the main channel.
|
|
6
|
+
*/
|
|
7
|
+
import { type Client, type Message } from 'discord.js';
|
|
8
|
+
export interface DiscordTextAttachment {
|
|
9
|
+
name: string;
|
|
10
|
+
content: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Detect if a response contains a coding agent start message.
|
|
15
|
+
* Returns the task ID if found, null otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectCodeAgentStart(response: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Create a thread from the user's message for a coding agent task.
|
|
20
|
+
* Returns the thread ID, or null if thread creation fails.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createTaskThread(message: Message, taskId: string, taskPreview: string): Promise<string | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Build a user-facing Discord URL for a thread, suitable for posting as a clickable link.
|
|
25
|
+
* Returns undefined if we can't construct one (e.g. missing guild context).
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildThreadUrl(guildId: string | null | undefined, threadId: string): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Register an existing thread for a task (e.g. restored from disk).
|
|
30
|
+
*/
|
|
31
|
+
export declare function registerTaskThread(taskId: string, threadId: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get the thread ID for a task, if one exists.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getTaskThreadId(taskId: string): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Send a message to a Discord thread. Handles chunking for long messages.
|
|
38
|
+
* Returns true if sent successfully.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sendToThread(client: Client, threadId: string, text: string): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Send text plus one or more UTF-8 attachments to a Discord thread.
|
|
43
|
+
* Keeps the visible message compact while preserving full reports.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sendToThreadWithAttachments(client: Client, threadId: string, text: string, attachments?: DiscordTextAttachment[]): Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Send a message with optional voice attachment to a Discord thread.
|
|
48
|
+
* Handles chunking for long text messages.
|
|
49
|
+
* Returns true if sent successfully.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sendToThreadWithVoice(client: Client, threadId: string, text: string, voiceBuffer?: Uint8Array, voiceFormat?: string): Promise<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* Clean up thread mapping for a task (e.g. after completion).
|
|
54
|
+
* We keep the mapping around for a while since late notifications may arrive.
|
|
55
|
+
*/
|
|
56
|
+
export declare function clearTaskThread(taskId: string): void;
|
|
57
|
+
/** Export for testing. */
|
|
58
|
+
export declare function _getTaskThreadsMap(): Map<string, string>;
|