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
@@ -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 sendDiscordProactiveVoice(target: string | number, buffer: Buffer, format: string): Promise<void>;
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>;