claudity 1.0.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.
@@ -0,0 +1,353 @@
1
+ const { spawn } = require('child_process');
2
+ const { randomUUID } = require('crypto');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const auth = require('./auth');
7
+ const { stmts } = require('../db');
8
+
9
+ const API_URL = 'https://api.anthropic.com/v1/messages';
10
+ const MODEL = 'claude-opus-4-6';
11
+
12
+ function hashPrompt(str) {
13
+ let h = 0;
14
+ for (let i = 0; i < str.length; i++) {
15
+ h = ((h << 5) - h + str.charCodeAt(i)) | 0;
16
+ }
17
+ return h.toString(36);
18
+ }
19
+
20
+ function invalidateSession(agentId) {
21
+ stmts.deleteSession.run(agentId);
22
+ }
23
+
24
+ function isContextOverflow(err) {
25
+ const msg = (err.message || '').toLowerCase();
26
+ return msg.includes('context') || msg.includes('overflow') || msg.includes('too long') || msg.includes('token limit');
27
+ }
28
+
29
+ async function sendMessage({ system, messages, tools, maxTokens = 4096, agentId, model = 'opus', thinking = 'high', noBuiltinTools = false }) {
30
+ const status = auth.getAuthStatus();
31
+ if (!status.authenticated) throw new Error('not authenticated — run claude setup-token');
32
+
33
+ if (status.mode === 'api_key') {
34
+ return sendViaApi({ system, messages, tools, maxTokens });
35
+ }
36
+
37
+ return sendViaCli({ system, messages, tools, maxTokens, agentId, model, thinking, noBuiltinTools });
38
+ }
39
+
40
+ async function sendViaApi({ system, messages, tools, maxTokens }) {
41
+ const headers = auth.getHeaders();
42
+
43
+ const body = {
44
+ model: MODEL,
45
+ max_tokens: maxTokens,
46
+ system,
47
+ messages
48
+ };
49
+
50
+ if (tools && tools.length > 0) {
51
+ body.tools = tools;
52
+ }
53
+
54
+ const res = await fetch(API_URL, {
55
+ method: 'POST',
56
+ headers,
57
+ body: JSON.stringify(body)
58
+ });
59
+
60
+ if (!res.ok) {
61
+ const text = await res.text();
62
+ throw new Error(`claude api error ${res.status}: ${text}`);
63
+ }
64
+
65
+ return await res.json();
66
+ }
67
+
68
+ function buildFullSysPrompt(system, tools) {
69
+ let sysPrompt = system || '';
70
+ if (tools && tools.length > 0) {
71
+ sysPrompt += '\n\nyou have these tools available. to use a tool, include a json block in your response:\n```json\n{"tool_use": {"name": "tool_name", "input": {...}}}\n```\n\navailable tools:\n' +
72
+ tools.map(t => `- ${t.name}: ${t.description}\n parameters: ${JSON.stringify(t.input_schema)}`).join('\n');
73
+ }
74
+ return sysPrompt;
75
+ }
76
+
77
+ function buildPromptText(lastUserMsg) {
78
+ let promptText = '';
79
+ if (typeof lastUserMsg.content === 'string') {
80
+ promptText = lastUserMsg.content;
81
+ } else if (Array.isArray(lastUserMsg.content)) {
82
+ const textParts = lastUserMsg.content.filter(b => typeof b === 'string' || b.type === 'text');
83
+ promptText = textParts.map(b => typeof b === 'string' ? b : b.text).join('\n');
84
+
85
+ const toolResults = lastUserMsg.content.filter(b => b.type === 'tool_result');
86
+ if (toolResults.length > 0) {
87
+ promptText += '\n\ntool results:\n' + toolResults.map(r =>
88
+ `[${r.tool_use_id}]: ${r.content}`
89
+ ).join('\n');
90
+ }
91
+ }
92
+ return promptText;
93
+ }
94
+
95
+ function buildContext(messages) {
96
+ return messages.slice(0, -1).map(m => {
97
+ if (m.role === 'user' && typeof m.content === 'string') return `user: ${m.content}`;
98
+ if (m.role === 'assistant') {
99
+ if (Array.isArray(m.content)) {
100
+ const parts = m.content.map(b => {
101
+ if (b.type === 'text') return b.text;
102
+ if (b.type === 'tool_use') return `[tool call: ${b.name}(${JSON.stringify(b.input)})]`;
103
+ return '';
104
+ }).filter(Boolean);
105
+ return `assistant: ${parts.join('\n')}`;
106
+ }
107
+ return `assistant: ${m.content}`;
108
+ }
109
+ return '';
110
+ }).filter(Boolean).join('\n\n');
111
+ }
112
+
113
+ function runCli(args, input, timeoutMs = 300000, extraEnv = {}) {
114
+ return new Promise((resolve, reject) => {
115
+ let done = false;
116
+ const env = { ...process.env, ...extraEnv };
117
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'claudity-'));
118
+ const proc = spawn('claude', args, {
119
+ stdio: ['pipe', 'pipe', 'pipe'],
120
+ timeout: timeoutMs,
121
+ cwd,
122
+ env
123
+ });
124
+
125
+ const fallback = setTimeout(() => {
126
+ if (!done) {
127
+ done = true;
128
+ proc.kill();
129
+ reject(new Error('claude cli timed out'));
130
+ }
131
+ }, timeoutMs + 10000);
132
+
133
+ let stdout = '';
134
+ let stderr = '';
135
+
136
+ proc.stdout.on('data', d => stdout += d);
137
+ proc.stderr.on('data', d => stderr += d);
138
+
139
+ proc.on('close', code => {
140
+ if (done) return;
141
+ done = true;
142
+ clearTimeout(fallback);
143
+ if (stdout.trim()) {
144
+ resolve(stdout.trim());
145
+ } else if (code !== 0) {
146
+ reject(new Error(`claude cli exited ${code}: ${stderr.trim()}`));
147
+ } else {
148
+ resolve('');
149
+ }
150
+ });
151
+
152
+ proc.on('error', err => {
153
+ if (done) return;
154
+ done = true;
155
+ clearTimeout(fallback);
156
+ reject(err);
157
+ });
158
+
159
+ proc.stdin.write(input);
160
+ proc.stdin.end();
161
+ });
162
+ }
163
+
164
+ async function sendViaCli({ system, messages, tools, maxTokens, agentId, model = 'opus', thinking = 'high', noBuiltinTools = false }) {
165
+ const lastUserMsg = messages.filter(m => m.role === 'user').pop();
166
+ const promptText = buildPromptText(lastUserMsg);
167
+ const sysPrompt = buildFullSysPrompt(system, tools);
168
+ const currentHash = hashPrompt(sysPrompt);
169
+ const thinkingTokens = { low: '0', medium: '16000', high: '31999' };
170
+ const extraEnv = { MAX_THINKING_TOKENS: thinkingTokens[thinking] || '31999' };
171
+
172
+ const session = agentId ? stmts.getSession.get(agentId) : null;
173
+ const canResume = session && session.prompt_hash === currentHash;
174
+
175
+ let output;
176
+
177
+ if (canResume) {
178
+ const args = ['-p', '--output-format', 'json', '--dangerously-skip-permissions', '--setting-sources', '', '--resume', session.session_id];
179
+ try {
180
+ output = await runCli(args, promptText, 300000, extraEnv);
181
+ } catch (err) {
182
+ stmts.deleteSession.run(agentId);
183
+ if (isContextOverflow(err)) {
184
+ return sendCliFresh({ sysPrompt, messages: [messages[messages.length - 1]], promptText, tools, agentId, currentHash, model, extraEnv, noBuiltinTools });
185
+ }
186
+ return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, extraEnv, noBuiltinTools });
187
+ }
188
+ } else {
189
+ return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, extraEnv, noBuiltinTools });
190
+ }
191
+
192
+ return processCliOutput(output, tools);
193
+ }
194
+
195
+ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model = 'opus', extraEnv = {}, noBuiltinTools = false }) {
196
+ const sessionId = randomUUID();
197
+ const context = buildContext(messages);
198
+
199
+ let fullPrompt = '';
200
+ if (context) fullPrompt += `previous conversation:\n${context}\n\n`;
201
+ fullPrompt += promptText;
202
+
203
+ const args = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', sessionId];
204
+ if (noBuiltinTools) args.push('--tools', '');
205
+ if (sysPrompt) {
206
+ args.push('--system-prompt', sysPrompt);
207
+ }
208
+
209
+ try {
210
+ const output = await runCli(args, fullPrompt, 300000, extraEnv);
211
+
212
+ if (agentId) {
213
+ stmts.upsertSession.run(agentId, sessionId, currentHash);
214
+ }
215
+
216
+ return processCliOutput(output, tools);
217
+ } catch (err) {
218
+ if (isContextOverflow(err) && context) {
219
+ const retryArgs = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', randomUUID()];
220
+ if (sysPrompt) retryArgs.push('--system-prompt', sysPrompt);
221
+ const output = await runCli(retryArgs, promptText, 300000, extraEnv);
222
+ return processCliOutput(output, tools);
223
+ }
224
+ throw err;
225
+ }
226
+ }
227
+
228
+ function processCliOutput(output, tools) {
229
+ const parsed = parseCliOutput(output);
230
+ if (!parsed) throw new Error('claude cli returned empty response');
231
+
232
+ if (parsed.is_error || parsed.subtype === 'error_max_turns') {
233
+ const text = typeof parsed.result === 'string' && parsed.result.length > 0
234
+ ? parsed.result
235
+ : '';
236
+ return buildResponse(text, tools);
237
+ }
238
+
239
+ if (parsed.result !== undefined && parsed.result !== null && parsed.result !== '') {
240
+ const text = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
241
+ return buildResponse(text, tools);
242
+ }
243
+
244
+ if (parsed.content) {
245
+ return parsed;
246
+ }
247
+
248
+ if (parsed.type === 'result') {
249
+ return buildResponse('', tools);
250
+ }
251
+
252
+ return buildResponse(output, tools);
253
+ }
254
+
255
+ function parseCliOutput(output) {
256
+ if (!output) return null;
257
+ try { return JSON.parse(output); } catch {}
258
+ const lines = output.split('\n').filter(l => l.trim());
259
+ for (let i = lines.length - 1; i >= 0; i--) {
260
+ try { return JSON.parse(lines[i]); } catch {}
261
+ }
262
+ return null;
263
+ }
264
+
265
+ function extractJsonObjects(str) {
266
+ const results = [];
267
+ let i = 0;
268
+ while (i < str.length) {
269
+ if (str[i] === '{') {
270
+ let depth = 0;
271
+ let start = i;
272
+ let inString = false;
273
+ let escaped = false;
274
+ for (let j = i; j < str.length; j++) {
275
+ const ch = str[j];
276
+ if (escaped) { escaped = false; continue; }
277
+ if (ch === '\\' && inString) { escaped = true; continue; }
278
+ if (ch === '"' && !escaped) { inString = !inString; continue; }
279
+ if (inString) continue;
280
+ if (ch === '{') depth++;
281
+ else if (ch === '}') {
282
+ depth--;
283
+ if (depth === 0) {
284
+ const candidate = str.slice(start, j + 1);
285
+ try {
286
+ results.push(JSON.parse(candidate));
287
+ } catch {}
288
+ i = j + 1;
289
+ break;
290
+ }
291
+ }
292
+ }
293
+ if (depth !== 0) i++;
294
+ } else {
295
+ i++;
296
+ }
297
+ }
298
+ return results;
299
+ }
300
+
301
+ function buildResponse(text, tools) {
302
+ const content = [{ type: 'text', text }];
303
+ let stopReason = 'end_turn';
304
+
305
+ if (tools && tools.length > 0) {
306
+ const jsonBlockPattern = /```json\s*\n?\s*([\s\S]*?)\s*\n?\s*```/g;
307
+ let match;
308
+ while ((match = jsonBlockPattern.exec(text)) !== null) {
309
+ const objects = extractJsonObjects(match[1]);
310
+ for (const obj of objects) {
311
+ if (obj.tool_use && obj.tool_use.name) {
312
+ content.push({
313
+ type: 'tool_use',
314
+ id: `toolu_cli_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
315
+ name: obj.tool_use.name,
316
+ input: obj.tool_use.input || {}
317
+ });
318
+ stopReason = 'tool_use';
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ return { content, stop_reason: stopReason };
325
+ }
326
+
327
+ function extractText(response) {
328
+ const textBlocks = response.content.filter(b => b.type === 'text');
329
+ return textBlocks.map(b => b.text).join('\n');
330
+ }
331
+
332
+ function extractToolUse(response) {
333
+ return response.content.filter(b => b.type === 'tool_use');
334
+ }
335
+
336
+ function hasToolUse(response) {
337
+ return response.stop_reason === 'tool_use';
338
+ }
339
+
340
+ async function generateQuickAck(userContent, agentName) {
341
+ const args = ['-p', '--output-format', 'json', '--model', 'haiku', '--dangerously-skip-permissions', '--setting-sources', ''];
342
+ const prompt = `you are ${agentName}, an ai agent. the user just sent you this message:\n\n"${userContent}"\n\nyou are about to start working on this. generate a short casual acknowledgment (1 sentence, all lowercase) that shows you read their message and are about to get on it. DO NOT answer their question or attempt the task. just acknowledge it like "sounds good, let me look into that" or "ooh nice, give me a sec to work on that" — reference what they asked about naturally but don't provide any actual content or answers. just the acknowledgment, nothing else.`;
343
+ try {
344
+ const output = await runCli(args, prompt, 15000);
345
+ const parsed = parseCliOutput(output);
346
+ if (parsed && typeof parsed.result === 'string' && parsed.result.length > 0) {
347
+ return parsed.result.trim();
348
+ }
349
+ } catch {}
350
+ return null;
351
+ }
352
+
353
+ module.exports = { sendMessage, extractText, extractToolUse, hasToolUse, invalidateSession, generateQuickAck };
@@ -0,0 +1,97 @@
1
+ const { v4: uuid } = require('uuid');
2
+ const { stmts } = require('../db');
3
+
4
+ const services = {
5
+ imessage: () => require('./imessage'),
6
+ discord: () => require('./discord'),
7
+ telegram: () => require('./telegram'),
8
+ slack: () => require('./slack'),
9
+ whatsapp: () => require('./whatsapp'),
10
+ signal: () => require('./signal'),
11
+ };
12
+
13
+ const running = new Map();
14
+
15
+ function log(msg) {
16
+ console.log(`[connections] ${msg}`);
17
+ }
18
+
19
+ function onStatus(platform) {
20
+ return (status, detail) => {
21
+ stmts.updateConnectionStatus.run(status, detail || null, platform);
22
+ log(`${platform}: ${status}${detail ? ' — ' + detail : ''}`);
23
+ };
24
+ }
25
+
26
+ async function startService(platform, config) {
27
+ if (running.has(platform)) {
28
+ await stopService(platform);
29
+ }
30
+
31
+ const factory = services[platform];
32
+ if (!factory) {
33
+ log(`unknown platform: ${platform}`);
34
+ return;
35
+ }
36
+
37
+ try {
38
+ stmts.updateConnectionStatus.run('connecting', null, platform);
39
+ const svc = factory();
40
+ running.set(platform, svc);
41
+ await svc.start(config, { onStatus: onStatus(platform) });
42
+ } catch (err) {
43
+ stmts.updateConnectionStatus.run('error', err.message, platform);
44
+ log(`${platform} start failed: ${err.message}`);
45
+ }
46
+ }
47
+
48
+ async function stopService(platform) {
49
+ const svc = running.get(platform);
50
+ if (svc) {
51
+ svc.stop();
52
+ running.delete(platform);
53
+ }
54
+ stmts.updateConnectionStatus.run('disconnected', null, platform);
55
+ }
56
+
57
+ async function start() {
58
+ const rows = stmts.enabledConnections.all();
59
+ for (const row of rows) {
60
+ let config = {};
61
+ try { config = JSON.parse(row.config); } catch {}
62
+ await startService(row.platform, config);
63
+ }
64
+ log(`started ${rows.length} connection(s)`);
65
+ }
66
+
67
+ async function stop() {
68
+ for (const platform of running.keys()) {
69
+ await stopService(platform);
70
+ }
71
+ }
72
+
73
+ async function enable(platform, config) {
74
+ const existing = stmts.getConnectionByPlatform.get(platform);
75
+ const id = existing ? existing.id : uuid();
76
+ stmts.upsertConnection.run(id, platform, JSON.stringify(config), 1, 'disconnected', null);
77
+ await startService(platform, config);
78
+ }
79
+
80
+ async function disable(platform) {
81
+ stmts.updateConnectionEnabled.run(0, platform);
82
+ await stopService(platform);
83
+ }
84
+
85
+ async function remove(platform) {
86
+ await stopService(platform);
87
+ stmts.deleteConnection.run(platform);
88
+ }
89
+
90
+ function list() {
91
+ return stmts.listConnections.all().map(row => ({
92
+ ...row,
93
+ config: (() => { try { return JSON.parse(row.config); } catch { return {}; } })(),
94
+ }));
95
+ }
96
+
97
+ module.exports = { start, stop, enable, disable, remove, list };
@@ -0,0 +1,126 @@
1
+ const { Client, GatewayIntentBits, Partials } = require('discord.js');
2
+
3
+ const MAX_RESPONSE_LENGTH = 1900;
4
+
5
+ let client = null;
6
+ let typingTimer = null;
7
+
8
+ function log(msg) {
9
+ console.log(`[discord] ${msg}`);
10
+ }
11
+
12
+ function parseMessage(text, botId) {
13
+ if (!text) return null;
14
+ let cleaned = text.replace(new RegExp(`<@!?${botId}>\\s*`, 'g'), '').trim();
15
+ const match = cleaned.match(/^(\w+):\s*(.+)$/s);
16
+ if (!match) return null;
17
+ return { agent: match[1].toLowerCase(), command: match[2].trim() };
18
+ }
19
+
20
+ function startTyping(channel) {
21
+ channel.sendTyping().catch(() => {});
22
+ typingTimer = setInterval(() => {
23
+ channel.sendTyping().catch(() => {});
24
+ }, 5000);
25
+ }
26
+
27
+ function stopTyping() {
28
+ if (typingTimer) {
29
+ clearInterval(typingTimer);
30
+ typingTimer = null;
31
+ }
32
+ }
33
+
34
+ function start(config, callbacks) {
35
+ const { onStatus } = callbacks || {};
36
+ const { token } = config || {};
37
+
38
+ if (!token) {
39
+ if (onStatus) onStatus('error', 'no bot token configured');
40
+ return;
41
+ }
42
+
43
+ const chatModule = require('./chat');
44
+ const { stmts } = require('../db');
45
+
46
+ client = new Client({
47
+ intents: [
48
+ GatewayIntentBits.Guilds,
49
+ GatewayIntentBits.GuildMessages,
50
+ GatewayIntentBits.MessageContent,
51
+ GatewayIntentBits.DirectMessages,
52
+ ],
53
+ partials: [Partials.Channel, Partials.Message],
54
+ });
55
+
56
+ client.once('ready', () => {
57
+ const tag = client.user.tag;
58
+ const servers = client.guilds.cache.size;
59
+ log(`logged in as ${tag} on ${servers} server(s)`);
60
+ if (onStatus) onStatus('connected', `${tag} on ${servers} server(s)`);
61
+ });
62
+
63
+ client.on('messageCreate', async (message) => {
64
+ if (message.author.bot) return;
65
+
66
+ const isDM = !message.guild;
67
+ const isMention = message.mentions.has(client.user);
68
+
69
+ if (!isDM && !isMention) return;
70
+
71
+ let parsed = parseMessage(message.content, client.user.id);
72
+ let agent;
73
+ if (parsed) {
74
+ agent = stmts.getAgentByName.get(parsed.agent);
75
+ if (!agent) {
76
+ await message.reply(`no agent named "${parsed.agent}"`).catch(() => {});
77
+ return;
78
+ }
79
+ } else {
80
+ agent = stmts.getDefaultAgent.get();
81
+ if (!agent) return;
82
+ let cleaned = message.content.replace(new RegExp(`<@!?${client.user.id}>\\s*`, 'g'), '').trim();
83
+ parsed = { agent: agent.name, command: cleaned };
84
+ }
85
+
86
+ log(`${parsed.agent}: ${parsed.command}`);
87
+ startTyping(message.channel);
88
+
89
+ try {
90
+ const result = await chatModule.enqueueMessage(agent.id, parsed.command, {
91
+ onAck: (text) => {
92
+ stopTyping();
93
+ message.reply(text).catch(() => {});
94
+ startTyping(message.channel);
95
+ }
96
+ });
97
+ stopTyping();
98
+ if (result && result.content) {
99
+ let text = result.content;
100
+ if (text.length > MAX_RESPONSE_LENGTH) {
101
+ text = text.slice(0, MAX_RESPONSE_LENGTH) + '...';
102
+ }
103
+ await message.reply(text);
104
+ }
105
+ } catch (err) {
106
+ stopTyping();
107
+ await message.reply(`error: ${err.message}`).catch(() => {});
108
+ }
109
+ });
110
+
111
+ client.login(token).catch((err) => {
112
+ log('login failed: ' + err.message);
113
+ if (onStatus) onStatus('error', err.message);
114
+ });
115
+ }
116
+
117
+ function stop() {
118
+ stopTyping();
119
+ if (client) {
120
+ client.destroy();
121
+ client = null;
122
+ }
123
+ log('disconnected');
124
+ }
125
+
126
+ module.exports = { start, stop };
@@ -0,0 +1,106 @@
1
+ const { stmts } = require('../db');
2
+ const workspace = require('./workspace');
3
+
4
+ const MIN_INTERVAL = 5 * 60 * 1000;
5
+ const timers = new Map();
6
+ const delays = new Map();
7
+ const processing = new Set();
8
+ const queues = new Map();
9
+
10
+ function start() {
11
+ const agents = stmts.agentsWithHeartbeat.all();
12
+ let offset = 0;
13
+ for (const agent of agents) {
14
+ addAgent(agent, offset);
15
+ offset += 30000;
16
+ }
17
+ if (agents.length > 0) {
18
+ console.log(`[heartbeat] started timers for ${agents.length} agent${agents.length === 1 ? '' : 's'}`);
19
+ }
20
+ }
21
+
22
+ function stop() {
23
+ for (const [id, timeout] of delays) clearTimeout(timeout);
24
+ delays.clear();
25
+ for (const [id, timer] of timers) clearInterval(timer);
26
+ timers.clear();
27
+ }
28
+
29
+ function addAgent(agent, initialDelay) {
30
+ if (!agent.heartbeat_interval) return;
31
+ removeAgent(agent.id);
32
+ const interval = Math.max(agent.heartbeat_interval, MIN_INTERVAL);
33
+ const delay = initialDelay !== undefined ? initialDelay : Math.floor(Math.random() * 30000);
34
+ const timeout = setTimeout(() => {
35
+ delays.delete(agent.id);
36
+ const timer = setInterval(() => scheduleHeartbeat(agent.id), interval);
37
+ timers.set(agent.id, timer);
38
+ scheduleHeartbeat(agent.id);
39
+ }, delay);
40
+ delays.set(agent.id, timeout);
41
+ }
42
+
43
+ function removeAgent(agentId) {
44
+ const delay = delays.get(agentId);
45
+ if (delay) {
46
+ clearTimeout(delay);
47
+ delays.delete(agentId);
48
+ }
49
+ const timer = timers.get(agentId);
50
+ if (timer) {
51
+ clearInterval(timer);
52
+ timers.delete(agentId);
53
+ }
54
+ }
55
+
56
+ function updateInterval(agentId, intervalMs) {
57
+ stmts.setHeartbeatInterval.run(intervalMs, agentId);
58
+ const agent = stmts.getAgent.get(agentId);
59
+ if (!agent) return;
60
+ if (intervalMs === null) {
61
+ removeAgent(agentId);
62
+ } else {
63
+ addAgent(agent);
64
+ }
65
+ }
66
+
67
+ function scheduleHeartbeat(agentId) {
68
+ if (processing.has(agentId)) return;
69
+ const prior = queues.get(agentId) || Promise.resolve();
70
+ const next = prior.catch(() => {}).then(() => handleHeartbeat(agentId));
71
+ queues.set(agentId, next);
72
+ }
73
+
74
+ async function handleHeartbeat(agentId) {
75
+ if (processing.has(agentId)) return;
76
+ processing.add(agentId);
77
+
78
+ const agent = stmts.getAgent.get(agentId);
79
+ if (!agent || !agent.heartbeat_interval) {
80
+ processing.delete(agentId);
81
+ removeAgent(agentId);
82
+ return;
83
+ }
84
+
85
+ if (agent.bootstrapped === 0) {
86
+ processing.delete(agentId);
87
+ return;
88
+ }
89
+
90
+ const heartbeatMd = workspace.readFile(agent.name, 'HEARTBEAT.md') || 'check if anything needs follow-up from recent conversations.';
91
+
92
+ const prompt = `[heartbeat] review your heartbeat checklist and act on anything that needs attention. if nothing needs action, respond with exactly HEARTBEAT_OK.\n\n${heartbeatMd}`;
93
+
94
+ try {
95
+ console.log(`[heartbeat] running for ${agent.name}`);
96
+ const chat = require('./chat');
97
+ await chat.enqueueMessage(agentId, prompt, { heartbeat: true });
98
+ console.log(`[heartbeat] completed for ${agent.name}`);
99
+ } catch (err) {
100
+ console.error(`[heartbeat] error for ${agent.name}: ${err.message}`);
101
+ } finally {
102
+ processing.delete(agentId);
103
+ }
104
+ }
105
+
106
+ module.exports = { start, stop, addAgent, removeAgent, updateInterval };