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
package/dist/api.js
CHANGED
|
@@ -11,6 +11,7 @@ import { redactSecrets } from './security.js';
|
|
|
11
11
|
import { readAuditTraces } from './audit.js';
|
|
12
12
|
import { getUsageSummary, readUsageRecords } from './usage.js';
|
|
13
13
|
import { getAllCodeAgents, getCodeAgent, cancelCodeAgent } from './tools.js';
|
|
14
|
+
import { withCodeAgentModelLabel } from './code-agents/utils.js';
|
|
14
15
|
import { listApprovals, getApproval, approveRequest, denyRequest } from './exec-approval.js';
|
|
15
16
|
import { getDigests, getDigest, deleteDigest, updateArticleReadStatus } from './digests.js';
|
|
16
17
|
import { loadSkills } from './skills.js';
|
|
@@ -22,15 +23,13 @@ import { initHeartbeat, stopHeartbeat } from './heartbeat.js';
|
|
|
22
23
|
import { initActiveChannel, stopActiveChannel, startActiveChannel } from './channels.js';
|
|
23
24
|
import { setCodeAgentConfig } from './tools.js';
|
|
24
25
|
import { resolveModelSelection } from './model-selection.js';
|
|
26
|
+
import { readSessionEntriesFromFile } from './sessions.js';
|
|
27
|
+
import { getAgentProfileByAlias, listAgentProfiles, listThreadAgentBindings, normalizeThreadAgentAlias, removeAgentProfile, setAgentProfileModel, setAgentProfilePrompt, setAgentProfileThinking, upsertAgentProfile, } from './channels/discord/thread-agents.js';
|
|
25
28
|
const DEFAULT_MODEL_ALIASES = {
|
|
26
|
-
'claude-fast': 'anthropic/claude-haiku-4-5',
|
|
27
|
-
'claude-think': 'anthropic/claude-sonnet-4-6',
|
|
28
|
-
'claude-opus': 'anthropic/claude-opus-4-6',
|
|
29
29
|
'codex5.1': 'codex/gpt-5.1-codex',
|
|
30
30
|
'codex5.2': 'codex/gpt-5.2-codex',
|
|
31
31
|
'codex5.3': 'codex/gpt-5.3-codex',
|
|
32
|
-
|
|
33
|
-
kimi: 'kimi/kimi-for-coding',
|
|
32
|
+
'codex5.5': 'codex/gpt-5.5',
|
|
34
33
|
};
|
|
35
34
|
function validateFilename(filename) {
|
|
36
35
|
return !filename.includes('..') && filename === basename(filename);
|
|
@@ -42,6 +41,23 @@ function validateSkillName(name) {
|
|
|
42
41
|
function getSkillsDir(cfg) {
|
|
43
42
|
return cfg.skills?.directory || join(homedir(), '.skimpyclaw', 'skills');
|
|
44
43
|
}
|
|
44
|
+
const THINKING_LEVELS = new Set(['none', 'low', 'medium', 'high', 'xhigh']);
|
|
45
|
+
function parseThinkingLevel(value) {
|
|
46
|
+
if (typeof value !== 'string')
|
|
47
|
+
return undefined;
|
|
48
|
+
const normalized = value.trim().toLowerCase().replace(/^x[-_ ]?high$/, 'xhigh');
|
|
49
|
+
if (normalized === 'off')
|
|
50
|
+
return 'none';
|
|
51
|
+
return THINKING_LEVELS.has(normalized) ? normalized : undefined;
|
|
52
|
+
}
|
|
53
|
+
function configuredAgentsSummary(config) {
|
|
54
|
+
return Object.fromEntries(Object.entries(config.agents.list).map(([id, agent]) => [id, {
|
|
55
|
+
name: agent.identity?.name || id,
|
|
56
|
+
emoji: agent.identity?.emoji || '',
|
|
57
|
+
model: agent.model,
|
|
58
|
+
thinking: agent.thinking,
|
|
59
|
+
}]));
|
|
60
|
+
}
|
|
45
61
|
function resolveCronPromptPath(inputPath) {
|
|
46
62
|
const trimmed = inputPath.trim();
|
|
47
63
|
if (!trimmed || !trimmed.endsWith('.md') || trimmed.includes('\0'))
|
|
@@ -160,18 +176,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
160
176
|
if (channelFilter && channel !== channelFilter)
|
|
161
177
|
return null;
|
|
162
178
|
try {
|
|
163
|
-
const
|
|
164
|
-
const lines = content.split('\n').filter(Boolean);
|
|
165
|
-
const entries = lines
|
|
166
|
-
.map((line) => {
|
|
167
|
-
try {
|
|
168
|
-
return JSON.parse(line);
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
})
|
|
174
|
-
.filter((v) => Boolean(v));
|
|
179
|
+
const entries = readSessionEntriesFromFile(join(sessionsDir, file));
|
|
175
180
|
const last = entries[entries.length - 1];
|
|
176
181
|
const preview = last?.user || last?.assistant || '';
|
|
177
182
|
return {
|
|
@@ -205,21 +210,14 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
205
210
|
const offsetRaw = Number.parseInt(request.query.offset || '0', 10);
|
|
206
211
|
const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 500) : 80;
|
|
207
212
|
const offset = Number.isFinite(offsetRaw) ? Math.max(offsetRaw, 0) : 0;
|
|
208
|
-
const
|
|
209
|
-
const lines = content.split('\n').filter(Boolean);
|
|
213
|
+
const entries = readSessionEntriesFromFile(filePath);
|
|
210
214
|
const messages = [];
|
|
211
|
-
for (const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (entry.assistant)
|
|
218
|
-
messages.push({ ts, role: 'assistant', content: entry.assistant });
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
// Ignore malformed lines
|
|
222
|
-
}
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
const ts = entry.ts || new Date().toISOString();
|
|
217
|
+
if (entry.user)
|
|
218
|
+
messages.push({ ts, role: 'user', content: entry.user });
|
|
219
|
+
if (entry.assistant)
|
|
220
|
+
messages.push({ ts, role: 'assistant', content: entry.assistant });
|
|
223
221
|
}
|
|
224
222
|
const total = messages.length;
|
|
225
223
|
const end = Math.max(0, total - offset);
|
|
@@ -385,7 +383,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
385
383
|
: DEFAULT_MODEL_ALIASES;
|
|
386
384
|
const currentModel = getCurrentModel()
|
|
387
385
|
|| runtimeConfig.agents?.list?.[runtimeConfig.agents?.default]?.model
|
|
388
|
-
|| 'claude-opus';
|
|
386
|
+
|| 'anthropic/claude-opus-4-7';
|
|
389
387
|
return {
|
|
390
388
|
current: currentModel,
|
|
391
389
|
aliases: mergedAliases,
|
|
@@ -404,6 +402,87 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
404
402
|
setCurrentModel(selection.resolved);
|
|
405
403
|
return { model: selection.resolved };
|
|
406
404
|
});
|
|
405
|
+
// --- Discord Agent Profiles ---
|
|
406
|
+
fastify.get('/api/dashboard/agent-profiles', async () => {
|
|
407
|
+
return {
|
|
408
|
+
profiles: listAgentProfiles(),
|
|
409
|
+
bindings: listThreadAgentBindings(),
|
|
410
|
+
configuredAgents: configuredAgentsSummary(runtimeConfig),
|
|
411
|
+
modelAliases: runtimeConfig.models?.aliases || DEFAULT_MODEL_ALIASES,
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
fastify.post('/api/dashboard/agent-profiles', async (request, reply) => {
|
|
415
|
+
const alias = normalizeThreadAgentAlias(request.body?.alias);
|
|
416
|
+
const agentId = request.body?.agentId?.trim();
|
|
417
|
+
if (!alias)
|
|
418
|
+
return reply.code(400).send({ error: 'Invalid alias' });
|
|
419
|
+
if (!agentId || !runtimeConfig.agents.list[agentId]) {
|
|
420
|
+
return reply.code(400).send({ error: 'Unknown configured agent' });
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const profile = upsertAgentProfile({ alias, agentId, createdBy: 'dashboard' });
|
|
424
|
+
return { profile };
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
428
|
+
return reply.code(400).send({ error: msg });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
fastify.put('/api/dashboard/agent-profiles/:alias', async (request, reply) => {
|
|
432
|
+
const alias = normalizeThreadAgentAlias(request.params.alias);
|
|
433
|
+
if (!alias)
|
|
434
|
+
return reply.code(400).send({ error: 'Invalid alias' });
|
|
435
|
+
const existing = getAgentProfileByAlias(alias);
|
|
436
|
+
if (!existing)
|
|
437
|
+
return reply.code(404).send({ error: 'Agent profile not found' });
|
|
438
|
+
const body = request.body || {};
|
|
439
|
+
if (typeof body.agentId === 'string') {
|
|
440
|
+
const agentId = body.agentId.trim();
|
|
441
|
+
if (!agentId || !runtimeConfig.agents.list[agentId]) {
|
|
442
|
+
return reply.code(400).send({ error: 'Unknown configured agent' });
|
|
443
|
+
}
|
|
444
|
+
upsertAgentProfile({ alias, agentId, createdBy: existing.createdBy || 'dashboard' });
|
|
445
|
+
}
|
|
446
|
+
if ('model' in body) {
|
|
447
|
+
const modelInput = typeof body.model === 'string' ? body.model.trim() : '';
|
|
448
|
+
if (modelInput) {
|
|
449
|
+
const selection = resolveModelSelection(modelInput, runtimeConfig);
|
|
450
|
+
if (!selection.ok || !selection.resolved) {
|
|
451
|
+
return reply.code(400).send({ error: selection.error || 'Invalid model selection' });
|
|
452
|
+
}
|
|
453
|
+
setAgentProfileModel(alias, selection.resolved);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
setAgentProfileModel(alias, undefined);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if ('thinking' in body) {
|
|
460
|
+
const thinkingInput = typeof body.thinking === 'string' ? body.thinking.trim() : '';
|
|
461
|
+
if (thinkingInput) {
|
|
462
|
+
const thinking = parseThinkingLevel(thinkingInput);
|
|
463
|
+
if (!thinking)
|
|
464
|
+
return reply.code(400).send({ error: 'Invalid effort' });
|
|
465
|
+
setAgentProfileThinking(alias, thinking);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
setAgentProfileThinking(alias, undefined);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if ('promptOverlay' in body) {
|
|
472
|
+
const promptOverlay = typeof body.promptOverlay === 'string' ? body.promptOverlay : '';
|
|
473
|
+
setAgentProfilePrompt(alias, promptOverlay);
|
|
474
|
+
}
|
|
475
|
+
return { profile: getAgentProfileByAlias(alias) };
|
|
476
|
+
});
|
|
477
|
+
fastify.delete('/api/dashboard/agent-profiles/:alias', async (request, reply) => {
|
|
478
|
+
const alias = normalizeThreadAgentAlias(request.params.alias);
|
|
479
|
+
if (!alias)
|
|
480
|
+
return reply.code(400).send({ error: 'Invalid alias' });
|
|
481
|
+
const deleted = removeAgentProfile(alias);
|
|
482
|
+
if (!deleted)
|
|
483
|
+
return reply.code(404).send({ error: 'Agent profile not found' });
|
|
484
|
+
return { deleted: true };
|
|
485
|
+
});
|
|
407
486
|
// --- Templates ---
|
|
408
487
|
fastify.get('/api/dashboard/templates/:agentId', async (request, reply) => {
|
|
409
488
|
const { agentId } = request.params;
|
|
@@ -651,9 +730,6 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
651
730
|
const features = {
|
|
652
731
|
telegram: runtimeConfig.channels.telegram?.enabled ?? false,
|
|
653
732
|
discord: runtimeConfig.channels.discord?.enabled ?? false,
|
|
654
|
-
browser: Boolean(runtimeConfig.channels.telegram?.tools?.browser?.enabled
|
|
655
|
-
|| runtimeConfig.channels.discord?.tools?.browser?.enabled
|
|
656
|
-
|| runtimeConfig.heartbeat?.tools?.browser?.enabled),
|
|
657
733
|
voice: Boolean(runtimeConfig.voice?.enabled),
|
|
658
734
|
};
|
|
659
735
|
// Check which env vars are set vs missing by reading raw config for ${VAR} refs
|
|
@@ -687,7 +763,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
687
763
|
});
|
|
688
764
|
// --- Code Agents (Multi-Agent) ---
|
|
689
765
|
fastify.get('/api/dashboard/code-agents', async () => {
|
|
690
|
-
const agents = getAllCodeAgents();
|
|
766
|
+
const agents = getAllCodeAgents().map(withCodeAgentModelLabel);
|
|
691
767
|
return { agents };
|
|
692
768
|
});
|
|
693
769
|
fastify.get('/api/dashboard/code-agents/:id', async (request, reply) => {
|
|
@@ -696,7 +772,7 @@ export function registerDashboardAPI(fastify, config) {
|
|
|
696
772
|
if (!agent) {
|
|
697
773
|
return reply.code(404).send({ error: 'Code agent not found' });
|
|
698
774
|
}
|
|
699
|
-
return agent;
|
|
775
|
+
return withCodeAgentModelLabel(agent);
|
|
700
776
|
});
|
|
701
777
|
fastify.post('/api/dashboard/code-agents/:id/cancel', async (request, reply) => {
|
|
702
778
|
const { id } = request.params;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord attachment processing — extensible framework for extracting text
|
|
3
|
+
* content from user-uploaded files (txt, pdf, etc.) and injecting it into
|
|
4
|
+
* the model context.
|
|
5
|
+
*/
|
|
6
|
+
import type { Attachment } from 'discord.js';
|
|
7
|
+
/** Maximum attachment size in bytes (2 MB) */
|
|
8
|
+
export declare const MAX_ATTACHMENT_BYTES: number;
|
|
9
|
+
/** Maximum extracted text length in characters (100 KB) */
|
|
10
|
+
export declare const MAX_TEXT_CHARS = 100000;
|
|
11
|
+
export interface AttachmentResult {
|
|
12
|
+
/** Whether extraction succeeded */
|
|
13
|
+
ok: boolean;
|
|
14
|
+
/** Extracted text (present when ok=true) */
|
|
15
|
+
text?: string;
|
|
16
|
+
/** Human-readable error (present when ok=false) */
|
|
17
|
+
error?: string;
|
|
18
|
+
/** Original filename */
|
|
19
|
+
filename: string;
|
|
20
|
+
}
|
|
21
|
+
/** Handler for a specific file type */
|
|
22
|
+
export interface AttachmentHandler {
|
|
23
|
+
/** File extensions this handler supports (lowercase, without dot) */
|
|
24
|
+
extensions: string[];
|
|
25
|
+
/** MIME type prefixes this handler supports */
|
|
26
|
+
mimeTypes: string[];
|
|
27
|
+
/** Extract text from the attachment buffer */
|
|
28
|
+
extract(buffer: Buffer, filename: string): Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register a custom attachment handler. Handlers added later take priority
|
|
32
|
+
* over earlier ones when extensions overlap.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerHandler(handler: AttachmentHandler): void;
|
|
35
|
+
/** List supported file extensions across all registered handlers. */
|
|
36
|
+
export declare function supportedExtensions(): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Check if a Discord attachment is a "document" type we can extract text from
|
|
39
|
+
* (i.e. not an image or audio, which have their own handling paths).
|
|
40
|
+
*/
|
|
41
|
+
export declare function isDocumentAttachment(attachment: Attachment): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Process a Discord attachment: fetch, validate, extract text.
|
|
44
|
+
* Returns a result object — never throws.
|
|
45
|
+
*/
|
|
46
|
+
export declare function processAttachment(attachment: Attachment): Promise<AttachmentResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Process multiple attachments, returning results for each.
|
|
49
|
+
*/
|
|
50
|
+
export declare function processAttachments(attachments: Attachment[]): Promise<AttachmentResult[]>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord attachment processing — extensible framework for extracting text
|
|
3
|
+
* content from user-uploaded files (txt, pdf, etc.) and injecting it into
|
|
4
|
+
* the model context.
|
|
5
|
+
*/
|
|
6
|
+
import { PDFParse } from 'pdf-parse';
|
|
7
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
8
|
+
/** Maximum attachment size in bytes (2 MB) */
|
|
9
|
+
export const MAX_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
/** Maximum extracted text length in characters (100 KB) */
|
|
11
|
+
export const MAX_TEXT_CHARS = 100_000;
|
|
12
|
+
/** Fetch timeout in milliseconds */
|
|
13
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
14
|
+
// ── Built-in handlers ────────────────────────────────────────────────
|
|
15
|
+
const textHandler = {
|
|
16
|
+
extensions: ['txt', 'md', 'csv', 'json', 'xml', 'yaml', 'yml', 'log', 'ini', 'cfg', 'conf', 'toml', 'env', 'sh', 'bash', 'zsh', 'py', 'js', 'ts', 'jsx', 'tsx', 'html', 'css', 'sql', 'rb', 'go', 'rs', 'java', 'kt', 'swift', 'c', 'cpp', 'h', 'hpp'],
|
|
17
|
+
mimeTypes: ['text/'],
|
|
18
|
+
extract: async (buffer) => buffer.toString('utf-8'),
|
|
19
|
+
};
|
|
20
|
+
const pdfHandler = {
|
|
21
|
+
extensions: ['pdf'],
|
|
22
|
+
mimeTypes: ['application/pdf'],
|
|
23
|
+
extract: async (buffer, filename) => {
|
|
24
|
+
const parser = new PDFParse({ data: buffer });
|
|
25
|
+
try {
|
|
26
|
+
const result = await parser.getText();
|
|
27
|
+
const text = result.text?.trim();
|
|
28
|
+
if (!text) {
|
|
29
|
+
throw new Error(`PDF "${filename}" contains no extractable text (may be image-only).`);
|
|
30
|
+
}
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await parser.destroy().catch(() => { });
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
// ── Registry ─────────────────────────────────────────────────────────
|
|
39
|
+
const handlers = [textHandler, pdfHandler];
|
|
40
|
+
/**
|
|
41
|
+
* Register a custom attachment handler. Handlers added later take priority
|
|
42
|
+
* over earlier ones when extensions overlap.
|
|
43
|
+
*/
|
|
44
|
+
export function registerHandler(handler) {
|
|
45
|
+
handlers.push(handler);
|
|
46
|
+
}
|
|
47
|
+
/** List supported file extensions across all registered handlers. */
|
|
48
|
+
export function supportedExtensions() {
|
|
49
|
+
const exts = new Set();
|
|
50
|
+
for (const h of handlers) {
|
|
51
|
+
for (const ext of h.extensions)
|
|
52
|
+
exts.add(ext);
|
|
53
|
+
}
|
|
54
|
+
return [...exts].sort();
|
|
55
|
+
}
|
|
56
|
+
function findHandler(filename, contentType) {
|
|
57
|
+
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
|
58
|
+
// Search in reverse so later-registered handlers win
|
|
59
|
+
for (let i = handlers.length - 1; i >= 0; i--) {
|
|
60
|
+
const h = handlers[i];
|
|
61
|
+
if (h.extensions.includes(ext))
|
|
62
|
+
return h;
|
|
63
|
+
if (contentType && h.mimeTypes.some(prefix => contentType.startsWith(prefix)))
|
|
64
|
+
return h;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* Check if a Discord attachment is a "document" type we can extract text from
|
|
71
|
+
* (i.e. not an image or audio, which have their own handling paths).
|
|
72
|
+
*/
|
|
73
|
+
export function isDocumentAttachment(attachment) {
|
|
74
|
+
const contentType = attachment.contentType ?? '';
|
|
75
|
+
if (contentType.startsWith('image/') || contentType.startsWith('audio/') || contentType.startsWith('voice/')) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const filename = attachment.name ?? 'unknown';
|
|
79
|
+
return findHandler(filename, contentType) !== undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Process a Discord attachment: fetch, validate, extract text.
|
|
83
|
+
* Returns a result object — never throws.
|
|
84
|
+
*/
|
|
85
|
+
export async function processAttachment(attachment) {
|
|
86
|
+
const filename = attachment.name ?? 'unknown';
|
|
87
|
+
try {
|
|
88
|
+
// Size check
|
|
89
|
+
if (attachment.size > MAX_ATTACHMENT_BYTES) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: `File "${filename}" is too large (${(attachment.size / 1024 / 1024).toFixed(1)} MB). Maximum is ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB.`,
|
|
93
|
+
filename,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Find handler
|
|
97
|
+
const handler = findHandler(filename, attachment.contentType ?? null);
|
|
98
|
+
if (!handler) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: `Unsupported file type: "${filename}". Supported extensions: ${supportedExtensions().map(e => `.${e}`).join(', ')}`,
|
|
102
|
+
filename,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Fetch with timeout
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
108
|
+
let buffer;
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(attachment.url, { signal: controller.signal });
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
return { ok: false, error: `Failed to download "${filename}" (HTTP ${response.status}).`, filename };
|
|
113
|
+
}
|
|
114
|
+
buffer = Buffer.from(await response.arrayBuffer());
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
// Extract text
|
|
120
|
+
let text = await handler.extract(buffer, filename);
|
|
121
|
+
// Truncate if needed
|
|
122
|
+
if (text.length > MAX_TEXT_CHARS) {
|
|
123
|
+
text = text.slice(0, MAX_TEXT_CHARS) + `\n\n[... truncated at ${MAX_TEXT_CHARS.toLocaleString()} characters]`;
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, text, filename };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
129
|
+
return { ok: false, error: `Error processing "${filename}": ${msg}`, filename };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Process multiple attachments, returning results for each.
|
|
134
|
+
*/
|
|
135
|
+
export async function processAttachments(attachments) {
|
|
136
|
+
return Promise.all(attachments.map(processAttachment));
|
|
137
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Client } from 'discord.js';
|
|
2
|
+
import type { Config } from '../../types.js';
|
|
3
|
+
import type { ExecuteToolContext } from '../../tools/execute-context.js';
|
|
4
|
+
import type { NormalizedDelegateToAgentInput } from '../../tools/agent-delegation.js';
|
|
5
|
+
export declare function createDiscordAgentDelegateHandler(getClient: () => Client | null): ((input: NormalizedDelegateToAgentInput, config: Config, context?: ExecuteToolContext) => Promise<string>);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ChannelType } from 'discord.js';
|
|
2
|
+
import { runAgentTurn } from '../../agent.js';
|
|
3
|
+
import { getCurrentModel, getCurrentThinking } from '../../gateway.js';
|
|
4
|
+
import { bindThreadAgent, getAgentProfileByAlias, } from './thread-agents.js';
|
|
5
|
+
import { addToHistory, getDiscordToolConfig, getHistory, sendLongTextToChannel, startTypingIndicatorForChannel, } from './utils.js';
|
|
6
|
+
import { buildThreadUrl } from './threads.js';
|
|
7
|
+
function formatThreadAgentThreadName(alias, taskText) {
|
|
8
|
+
const task = (taskText || '')
|
|
9
|
+
.replace(/https?:\/\/\S+\/pull\/(\d+)\S*/g, '#$1')
|
|
10
|
+
.replace(/https?:\/\/\S+\/issues\/(\d+)\S*/g, '#$1')
|
|
11
|
+
.replace(/https?:\/\/\S+/g, '')
|
|
12
|
+
.replace(/\s+/g, ' ')
|
|
13
|
+
.trim();
|
|
14
|
+
const base = task || 'agent';
|
|
15
|
+
const maxTaskLength = Math.max(10, 96 - alias.length);
|
|
16
|
+
const suffix = base.length > maxTaskLength ? `${base.slice(0, maxTaskLength - 3).trim()}...` : base;
|
|
17
|
+
return `${alias}: ${suffix}`.slice(0, 100);
|
|
18
|
+
}
|
|
19
|
+
function isThreadLike(channel) {
|
|
20
|
+
return Boolean(channel.isThread?.());
|
|
21
|
+
}
|
|
22
|
+
function canStartThreadFromMessage(message) {
|
|
23
|
+
return typeof message.startThread === 'function';
|
|
24
|
+
}
|
|
25
|
+
function truncateTask(value, maxChars) {
|
|
26
|
+
return value.length <= maxChars ? value : `${value.slice(0, maxChars - 3).trim()}...`;
|
|
27
|
+
}
|
|
28
|
+
function buildRunContext(context, threadAgent, parentChannelId) {
|
|
29
|
+
const depth = (context?.delegationDepth ?? 0) + 1;
|
|
30
|
+
return {
|
|
31
|
+
userId: context?.approverUserId,
|
|
32
|
+
sessionId: threadAgent.threadId,
|
|
33
|
+
channel: 'discord',
|
|
34
|
+
trigger: 'discord',
|
|
35
|
+
metadata: {
|
|
36
|
+
username: context?.approverUsername,
|
|
37
|
+
isDm: false,
|
|
38
|
+
discordThreadId: threadAgent.threadId,
|
|
39
|
+
discordChannelId: parentChannelId,
|
|
40
|
+
threadAgentAlias: threadAgent.alias,
|
|
41
|
+
threadAgentId: threadAgent.agentId,
|
|
42
|
+
threadAgentModel: threadAgent.model,
|
|
43
|
+
threadAgentThinking: threadAgent.thinking,
|
|
44
|
+
thinkingOverride: threadAgent.thinking ?? getCurrentThinking(),
|
|
45
|
+
threadAgentPromptOverlay: threadAgent.promptOverlay,
|
|
46
|
+
delegationDepth: depth,
|
|
47
|
+
delegatedFromAgentAlias: context?.threadAgentAlias,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function runDelegatedAgent(thread, parentChannelId, threadAgent, task, config, context) {
|
|
52
|
+
await sendLongTextToChannel(thread, `Task delegated to @${threadAgent.alias}:\n${task}`);
|
|
53
|
+
const stopTyping = startTypingIndicatorForChannel(thread);
|
|
54
|
+
try {
|
|
55
|
+
const key = `channel:${thread.id}`;
|
|
56
|
+
const history = await getHistory(key);
|
|
57
|
+
const response = await runAgentTurn(threadAgent.agentId, task, config, threadAgent.model || getCurrentModel(), getDiscordToolConfig(config), history, buildRunContext(context, threadAgent, parentChannelId));
|
|
58
|
+
await addToHistory(key, task, response);
|
|
59
|
+
await sendLongTextToChannel(thread, response);
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
64
|
+
await sendLongTextToChannel(thread, `Error: ${msg}`);
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
stopTyping();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function resolveDelegationParentChannel(client, context) {
|
|
72
|
+
const sourceChannelId = context?.discordChannelId || context?.channelTargetId || context?.sessionId;
|
|
73
|
+
if (!sourceChannelId)
|
|
74
|
+
return null;
|
|
75
|
+
const channel = await client.channels.fetch(String(sourceChannelId)).catch(() => null);
|
|
76
|
+
if (!channel)
|
|
77
|
+
return null;
|
|
78
|
+
if (isThreadLike(channel)) {
|
|
79
|
+
const parentId = channel.parentId;
|
|
80
|
+
if (!parentId)
|
|
81
|
+
return null;
|
|
82
|
+
const parent = await client.channels.fetch(parentId).catch(() => null);
|
|
83
|
+
return parent && 'send' in parent ? parent : null;
|
|
84
|
+
}
|
|
85
|
+
const channelType = channel.type;
|
|
86
|
+
const supportsThreads = channelType === ChannelType.GuildText || channelType === ChannelType.GuildAnnouncement;
|
|
87
|
+
if (!supportsThreads || !('send' in channel))
|
|
88
|
+
return null;
|
|
89
|
+
return channel;
|
|
90
|
+
}
|
|
91
|
+
export function createDiscordAgentDelegateHandler(getClient) {
|
|
92
|
+
return async (input, config, context) => {
|
|
93
|
+
const client = getClient();
|
|
94
|
+
if (!client)
|
|
95
|
+
return 'Error: Discord client is not available.';
|
|
96
|
+
const profile = getAgentProfileByAlias(input.alias);
|
|
97
|
+
if (!profile)
|
|
98
|
+
return `Error: No Discord agent profile found for @${input.alias}.`;
|
|
99
|
+
if (!config.agents.list[profile.agentId]) {
|
|
100
|
+
return `Error: Agent profile @${profile.alias} points to missing configured agent "${profile.agentId}".`;
|
|
101
|
+
}
|
|
102
|
+
const parentChannel = await resolveDelegationParentChannel(client, context);
|
|
103
|
+
if (!parentChannel) {
|
|
104
|
+
return 'Error: Could not find a Discord server channel where I can create a delegated agent thread.';
|
|
105
|
+
}
|
|
106
|
+
const preview = truncateTask(input.task, 300);
|
|
107
|
+
const starter = await parentChannel.send(`Delegating to @${profile.alias}:\n${preview}`);
|
|
108
|
+
if (!canStartThreadFromMessage(starter)) {
|
|
109
|
+
return 'Error: Discord did not allow creating a thread for this delegated agent.';
|
|
110
|
+
}
|
|
111
|
+
const thread = await starter.startThread({
|
|
112
|
+
name: formatThreadAgentThreadName(profile.alias, input.task),
|
|
113
|
+
autoArchiveDuration: 1440,
|
|
114
|
+
});
|
|
115
|
+
const threadAgent = bindThreadAgent({
|
|
116
|
+
threadId: thread.id,
|
|
117
|
+
alias: profile.alias,
|
|
118
|
+
createdBy: context?.approverUserId || 'agent-delegation',
|
|
119
|
+
guildId: thread.guildId,
|
|
120
|
+
channelId: thread.parentId ?? parentChannel.id,
|
|
121
|
+
});
|
|
122
|
+
const url = buildThreadUrl(thread.guildId, thread.id);
|
|
123
|
+
const runner = runDelegatedAgent(thread, thread.parentId ?? parentChannel.id, threadAgent, input.task, config, context);
|
|
124
|
+
if (input.wait) {
|
|
125
|
+
await runner;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
void runner.catch((err) => {
|
|
129
|
+
console.error('[discord-agent-delegation] Delegated agent failed:', err);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return url
|
|
133
|
+
? `Delegated to @${profile.alias}: ${url}`
|
|
134
|
+
: `Delegated to @${profile.alias} in thread ${thread.id}.`;
|
|
135
|
+
};
|
|
136
|
+
}
|