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.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /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
- minimax: 'minimax/MiniMax-M2.5',
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 content = readFileSync(join(sessionsDir, file), 'utf-8');
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 content = readFileSync(filePath, 'utf-8');
209
- const lines = content.split('\n').filter(Boolean);
213
+ const entries = readSessionEntriesFromFile(filePath);
210
214
  const messages = [];
211
- for (const line of lines) {
212
- try {
213
- const entry = JSON.parse(line);
214
- const ts = entry.ts || new Date().toISOString();
215
- if (entry.user)
216
- messages.push({ ts, role: 'user', content: entry.user });
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
+ }