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.
- package/.env.example +2 -0
- package/README.md +32 -0
- package/install.sh +343 -0
- package/package.json +28 -0
- package/public/css/style.css +1672 -0
- package/public/favicon.svg +9 -0
- package/public/font/berkeley.woff2 +0 -0
- package/public/index.html +262 -0
- package/public/js/app.js +1240 -0
- package/setup.sh +134 -0
- package/src/db.js +162 -0
- package/src/index.js +61 -0
- package/src/routes/api.js +194 -0
- package/src/routes/connections.js +41 -0
- package/src/routes/relay.js +37 -0
- package/src/services/auth.js +88 -0
- package/src/services/chat.js +365 -0
- package/src/services/claude.js +353 -0
- package/src/services/connections.js +97 -0
- package/src/services/discord.js +126 -0
- package/src/services/heartbeat.js +106 -0
- package/src/services/imessage.js +200 -0
- package/src/services/memory.js +183 -0
- package/src/services/scheduler.js +32 -0
- package/src/services/signal.js +237 -0
- package/src/services/slack.js +113 -0
- package/src/services/telegram.js +94 -0
- package/src/services/tools.js +467 -0
- package/src/services/whatsapp.js +111 -0
- package/src/services/workspace.js +162 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const { App, LogLevel } = require('@slack/bolt');
|
|
2
|
+
const { WebClient } = require('@slack/web-api');
|
|
3
|
+
|
|
4
|
+
const MAX_RESPONSE_LENGTH = 3000;
|
|
5
|
+
|
|
6
|
+
let app = null;
|
|
7
|
+
|
|
8
|
+
function log(msg) {
|
|
9
|
+
console.log(`[slack] ${msg}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseMessage(text) {
|
|
13
|
+
if (!text) return null;
|
|
14
|
+
let cleaned = text.replace(/<@[A-Z0-9]+>/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 start(config, callbacks) {
|
|
21
|
+
const { onStatus } = callbacks || {};
|
|
22
|
+
const { bot_token, app_token } = config || {};
|
|
23
|
+
|
|
24
|
+
if (!bot_token || !app_token) {
|
|
25
|
+
if (onStatus) onStatus('error', 'bot token and app token required');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const client = new WebClient(bot_token);
|
|
32
|
+
const auth = await client.auth.test();
|
|
33
|
+
const team = auth.team || 'unknown workspace';
|
|
34
|
+
|
|
35
|
+
const chatModule = require('./chat');
|
|
36
|
+
const { stmts } = require('../db');
|
|
37
|
+
|
|
38
|
+
app = new App({
|
|
39
|
+
token: bot_token,
|
|
40
|
+
appToken: app_token,
|
|
41
|
+
socketMode: true,
|
|
42
|
+
logLevel: LogLevel.ERROR,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function handleMessage({ message, say }) {
|
|
46
|
+
if (message.subtype) return;
|
|
47
|
+
if (message.bot_id) return;
|
|
48
|
+
|
|
49
|
+
let parsed = parseMessage(message.text);
|
|
50
|
+
let agent;
|
|
51
|
+
if (parsed) {
|
|
52
|
+
agent = stmts.getAgentByName.get(parsed.agent);
|
|
53
|
+
if (!agent) {
|
|
54
|
+
await say(`no agent named "${parsed.agent}"`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
agent = stmts.getDefaultAgent.get();
|
|
59
|
+
if (!agent) return;
|
|
60
|
+
let cleaned = (message.text || '').replace(/<@[A-Z0-9]+>/g, '').trim();
|
|
61
|
+
parsed = { agent: agent.name, command: cleaned };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
log(`${parsed.agent}: ${parsed.command}`);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await chatModule.enqueueMessage(agent.id, parsed.command, {
|
|
68
|
+
onAck: (text) => say(text).catch(() => {})
|
|
69
|
+
});
|
|
70
|
+
if (result && result.content) {
|
|
71
|
+
let text = result.content;
|
|
72
|
+
if (text.length > MAX_RESPONSE_LENGTH) {
|
|
73
|
+
text = text.slice(0, MAX_RESPONSE_LENGTH) + '...';
|
|
74
|
+
}
|
|
75
|
+
await say(text);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
await say(`error: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
app.error(async (err) => {
|
|
83
|
+
log('runtime error: ' + err.message);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
app.message(handleMessage);
|
|
87
|
+
|
|
88
|
+
app.event('app_mention', async ({ event, say }) => {
|
|
89
|
+
handleMessage({ message: event, say });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await app.start();
|
|
93
|
+
log(`connected to ${team}`);
|
|
94
|
+
if (onStatus) onStatus('connected', auth.user + ' in ' + team);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log('start failed: ' + err.message);
|
|
97
|
+
if (onStatus) onStatus('error', err.message);
|
|
98
|
+
}
|
|
99
|
+
})().catch((err) => {
|
|
100
|
+
log('unhandled error: ' + err.message);
|
|
101
|
+
if (onStatus) onStatus('error', err.message);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stop() {
|
|
106
|
+
if (app) {
|
|
107
|
+
app.stop().catch(() => {});
|
|
108
|
+
app = null;
|
|
109
|
+
}
|
|
110
|
+
log('disconnected');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { start, stop };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const TelegramBot = require('node-telegram-bot-api');
|
|
2
|
+
|
|
3
|
+
const MAX_RESPONSE_LENGTH = 4000;
|
|
4
|
+
|
|
5
|
+
let bot = null;
|
|
6
|
+
|
|
7
|
+
function log(msg) {
|
|
8
|
+
console.log(`[telegram] ${msg}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseMessage(text) {
|
|
12
|
+
if (!text) return null;
|
|
13
|
+
const match = text.match(/^(\w+):\s*(.+)$/s);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
return { agent: match[1].toLowerCase(), command: match[2].trim() };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function start(config, callbacks) {
|
|
19
|
+
const { onStatus } = callbacks || {};
|
|
20
|
+
const { token } = config || {};
|
|
21
|
+
|
|
22
|
+
if (!token) {
|
|
23
|
+
if (onStatus) onStatus('error', 'no bot token configured');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const chatModule = require('./chat');
|
|
28
|
+
const { stmts } = require('../db');
|
|
29
|
+
|
|
30
|
+
bot = new TelegramBot(token, { polling: true });
|
|
31
|
+
|
|
32
|
+
bot.on('polling_error', (err) => {
|
|
33
|
+
log('polling error: ' + err.message);
|
|
34
|
+
if (onStatus) onStatus('error', err.message);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
bot.getMe().then((me) => {
|
|
38
|
+
log(`logged in as @${me.username}`);
|
|
39
|
+
if (onStatus) onStatus('connected', `@${me.username}`);
|
|
40
|
+
}).catch((err) => {
|
|
41
|
+
log('auth failed: ' + err.message);
|
|
42
|
+
if (onStatus) onStatus('error', err.message);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
bot.on('message', async (msg) => {
|
|
46
|
+
if (msg.from.is_bot) return;
|
|
47
|
+
if (!msg.text) return;
|
|
48
|
+
|
|
49
|
+
let parsed = parseMessage(msg.text);
|
|
50
|
+
let agent;
|
|
51
|
+
if (parsed) {
|
|
52
|
+
agent = stmts.getAgentByName.get(parsed.agent);
|
|
53
|
+
if (!agent) {
|
|
54
|
+
bot.sendMessage(msg.chat.id, `no agent named "${parsed.agent}"`).catch(() => {});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
agent = stmts.getDefaultAgent.get();
|
|
59
|
+
if (!agent) return;
|
|
60
|
+
parsed = { agent: agent.name, command: msg.text.trim() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log(`${parsed.agent}: ${parsed.command}`);
|
|
64
|
+
bot.sendChatAction(msg.chat.id, 'typing').catch(() => {});
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await chatModule.enqueueMessage(agent.id, parsed.command, {
|
|
68
|
+
onAck: (text) => {
|
|
69
|
+
bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id }).catch(() => {});
|
|
70
|
+
bot.sendChatAction(msg.chat.id, 'typing').catch(() => {});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (result && result.content) {
|
|
74
|
+
let text = result.content;
|
|
75
|
+
if (text.length > MAX_RESPONSE_LENGTH) {
|
|
76
|
+
text = text.slice(0, MAX_RESPONSE_LENGTH) + '...';
|
|
77
|
+
}
|
|
78
|
+
await bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id });
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
bot.sendMessage(msg.chat.id, `error: ${err.message}`, { reply_to_message_id: msg.message_id }).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stop() {
|
|
87
|
+
if (bot) {
|
|
88
|
+
bot.stopPolling();
|
|
89
|
+
bot = null;
|
|
90
|
+
}
|
|
91
|
+
log('disconnected');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { start, stop };
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const { v4: uuid } = require('uuid');
|
|
3
|
+
const { stmts } = require('../db');
|
|
4
|
+
const workspace = require('./workspace');
|
|
5
|
+
|
|
6
|
+
const registry = {
|
|
7
|
+
http_request: {
|
|
8
|
+
definition: {
|
|
9
|
+
name: 'http_request',
|
|
10
|
+
description: 'make an http request to any url. use this to interact with APIs, submit data, register accounts, etc. supports all http methods.',
|
|
11
|
+
input_schema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], description: 'http method' },
|
|
15
|
+
url: { type: 'string', description: 'full url including https://' },
|
|
16
|
+
headers: { type: 'object', description: 'request headers as key-value pairs' },
|
|
17
|
+
body: { description: 'request body — object will be sent as json, string sent as-is' }
|
|
18
|
+
},
|
|
19
|
+
required: ['method', 'url']
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
handler: httpRequest
|
|
23
|
+
},
|
|
24
|
+
read_url: {
|
|
25
|
+
definition: {
|
|
26
|
+
name: 'read_url',
|
|
27
|
+
description: 'fetch a url and return its content as readable text. use this to read web pages, documentation, api specs, skill files, etc.',
|
|
28
|
+
input_schema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
url: { type: 'string', description: 'url to fetch and read' }
|
|
32
|
+
},
|
|
33
|
+
required: ['url']
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
handler: readUrl
|
|
37
|
+
},
|
|
38
|
+
store_credential: {
|
|
39
|
+
definition: {
|
|
40
|
+
name: 'store_credential',
|
|
41
|
+
description: 'securely store a credential or secret for later use. credentials are scoped to this agent and persist across tasks.',
|
|
42
|
+
input_schema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
key: { type: 'string', description: 'credential name (e.g. moltbook_api_key, github_token)' },
|
|
46
|
+
value: { type: 'string', description: 'the secret value to store' }
|
|
47
|
+
},
|
|
48
|
+
required: ['key', 'value']
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
handler: storeCredential
|
|
52
|
+
},
|
|
53
|
+
get_credential: {
|
|
54
|
+
definition: {
|
|
55
|
+
name: 'get_credential',
|
|
56
|
+
description: 'retrieve a previously stored credential by key. returns null if not found.',
|
|
57
|
+
input_schema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
key: { type: 'string', description: 'credential name to look up' }
|
|
61
|
+
},
|
|
62
|
+
required: ['key']
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
handler: getCredential
|
|
66
|
+
},
|
|
67
|
+
remember: {
|
|
68
|
+
definition: {
|
|
69
|
+
name: 'remember',
|
|
70
|
+
description: 'store a memory or standing instruction that persists across conversations. use this for user preferences, recurring instructions, important context, etc.',
|
|
71
|
+
input_schema: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
summary: { type: 'string', description: 'what to remember' }
|
|
75
|
+
},
|
|
76
|
+
required: ['summary']
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
handler: remember
|
|
80
|
+
},
|
|
81
|
+
schedule_task: {
|
|
82
|
+
definition: {
|
|
83
|
+
name: 'schedule_task',
|
|
84
|
+
description: 'schedule a recurring task. a reminder with your description will be sent to you at the specified interval, triggering you to act. use this for periodic actions like posting, checking feeds, monitoring, etc.',
|
|
85
|
+
input_schema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
description: { type: 'string', description: 'what to do each time this fires (be specific — this is the prompt you will receive)' },
|
|
89
|
+
interval_minutes: { type: 'number', description: 'how often to run in minutes (minimum 1)' }
|
|
90
|
+
},
|
|
91
|
+
required: ['description', 'interval_minutes']
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
handler: scheduleTask
|
|
95
|
+
},
|
|
96
|
+
cancel_schedule: {
|
|
97
|
+
definition: {
|
|
98
|
+
name: 'cancel_schedule',
|
|
99
|
+
description: 'cancel a scheduled recurring task by its id.',
|
|
100
|
+
input_schema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
schedule_id: { type: 'string', description: 'id of the schedule to cancel' }
|
|
104
|
+
},
|
|
105
|
+
required: ['schedule_id']
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
handler: cancelSchedule
|
|
109
|
+
},
|
|
110
|
+
list_schedules: {
|
|
111
|
+
definition: {
|
|
112
|
+
name: 'list_schedules',
|
|
113
|
+
description: 'list all active scheduled tasks.',
|
|
114
|
+
input_schema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
handler: listSchedules
|
|
120
|
+
},
|
|
121
|
+
spawn_subagent: {
|
|
122
|
+
definition: {
|
|
123
|
+
name: 'spawn_subagent',
|
|
124
|
+
description: 'spawn an ephemeral claude subprocess to handle a complex task. the subagent has full machine access (bash, file read/write, etc) but no claudity tools or memory. use this to offload heavy work like writing code, analyzing files, running commands, etc.',
|
|
125
|
+
input_schema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
task: { type: 'string', description: 'detailed description of what the subagent should do' },
|
|
129
|
+
context: { type: 'string', description: 'optional additional context to include' }
|
|
130
|
+
},
|
|
131
|
+
required: ['task']
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
handler: spawnSubagent
|
|
135
|
+
},
|
|
136
|
+
delegate: {
|
|
137
|
+
definition: {
|
|
138
|
+
name: 'delegate',
|
|
139
|
+
description: 'send a message to another claudity agent by name and get their response. use this for cross-agent collaboration — asking another agent to handle something in their domain.',
|
|
140
|
+
input_schema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
agent_name: { type: 'string', description: 'name of the agent to delegate to' },
|
|
144
|
+
message: { type: 'string', description: 'message to send to the other agent' }
|
|
145
|
+
},
|
|
146
|
+
required: ['agent_name', 'message']
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
handler: delegateToAgent
|
|
150
|
+
},
|
|
151
|
+
read_workspace: {
|
|
152
|
+
definition: {
|
|
153
|
+
name: 'read_workspace',
|
|
154
|
+
description: 'read a file from your workspace. use relative paths like "SOUL.md" or "memory/2026-02-07.md".',
|
|
155
|
+
input_schema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
path: { type: 'string', description: 'relative path within your workspace' }
|
|
159
|
+
},
|
|
160
|
+
required: ['path']
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
handler: readWorkspace
|
|
164
|
+
},
|
|
165
|
+
write_workspace: {
|
|
166
|
+
definition: {
|
|
167
|
+
name: 'write_workspace',
|
|
168
|
+
description: 'write or overwrite a file in your workspace. use this to update your soul, identity, memory, heartbeat, or any other workspace files.',
|
|
169
|
+
input_schema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
path: { type: 'string', description: 'relative path within your workspace' },
|
|
173
|
+
content: { type: 'string', description: 'file content to write' }
|
|
174
|
+
},
|
|
175
|
+
required: ['path', 'content']
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
handler: writeWorkspace
|
|
179
|
+
},
|
|
180
|
+
complete_bootstrap: {
|
|
181
|
+
definition: {
|
|
182
|
+
name: 'complete_bootstrap',
|
|
183
|
+
description: 'signal that your identity ritual is complete. call this after writing your workspace files during bootstrap.',
|
|
184
|
+
input_schema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
handler: completeBootstrap
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
async function httpRequest(input) {
|
|
194
|
+
const { method, url, headers = {}, body } = input;
|
|
195
|
+
|
|
196
|
+
const opts = { method, headers: { ...headers } };
|
|
197
|
+
|
|
198
|
+
if (body !== undefined && body !== null) {
|
|
199
|
+
if (typeof body === 'object') {
|
|
200
|
+
opts.headers['content-type'] = opts.headers['content-type'] || 'application/json';
|
|
201
|
+
opts.body = JSON.stringify(body);
|
|
202
|
+
} else {
|
|
203
|
+
opts.body = String(body);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const res = await fetch(url, opts);
|
|
208
|
+
|
|
209
|
+
const contentType = res.headers.get('content-type') || '';
|
|
210
|
+
let responseBody;
|
|
211
|
+
|
|
212
|
+
if (contentType.includes('application/json')) {
|
|
213
|
+
responseBody = await res.json();
|
|
214
|
+
} else {
|
|
215
|
+
responseBody = await res.text();
|
|
216
|
+
if (responseBody.length > 50000) {
|
|
217
|
+
responseBody = responseBody.slice(0, 50000) + '\n\n[truncated — response exceeded 50000 chars]';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
status: res.status,
|
|
223
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
224
|
+
body: responseBody
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function readUrl(input) {
|
|
229
|
+
const { url } = input;
|
|
230
|
+
|
|
231
|
+
const res = await fetch(url, {
|
|
232
|
+
headers: { 'accept': 'text/html,text/plain,text/markdown,application/json,*/*' }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
return { error: `fetch failed: ${res.status} ${res.statusText}`, url };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const contentType = res.headers.get('content-type') || '';
|
|
240
|
+
let text;
|
|
241
|
+
|
|
242
|
+
if (contentType.includes('application/json')) {
|
|
243
|
+
const json = await res.json();
|
|
244
|
+
text = JSON.stringify(json, null, 2);
|
|
245
|
+
} else {
|
|
246
|
+
text = await res.text();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (contentType.includes('text/html')) {
|
|
250
|
+
text = htmlToText(text);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (text.length > 80000) {
|
|
254
|
+
text = text.slice(0, 80000) + '\n\n[truncated — content exceeded 80000 chars]';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { url, content: text };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function htmlToText(html) {
|
|
261
|
+
let text = html;
|
|
262
|
+
text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
263
|
+
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
264
|
+
text = text.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '');
|
|
265
|
+
text = text.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '');
|
|
266
|
+
text = text.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '');
|
|
267
|
+
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
268
|
+
text = text.replace(/<\/p>/gi, '\n\n');
|
|
269
|
+
text = text.replace(/<\/div>/gi, '\n');
|
|
270
|
+
text = text.replace(/<\/li>/gi, '\n');
|
|
271
|
+
text = text.replace(/<\/h[1-6]>/gi, '\n\n');
|
|
272
|
+
text = text.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '$2 ($1)');
|
|
273
|
+
text = text.replace(/<[^>]+>/g, '');
|
|
274
|
+
text = text.replace(/&/g, '&');
|
|
275
|
+
text = text.replace(/</g, '<');
|
|
276
|
+
text = text.replace(/>/g, '>');
|
|
277
|
+
text = text.replace(/"/g, '"');
|
|
278
|
+
text = text.replace(/'/g, "'");
|
|
279
|
+
text = text.replace(/ /g, ' ');
|
|
280
|
+
text = text.replace(/\n{3,}/g, '\n\n');
|
|
281
|
+
text = text.replace(/[ \t]+/g, ' ');
|
|
282
|
+
return text.trim();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function storeCredential(input, context) {
|
|
286
|
+
const { key, value } = input;
|
|
287
|
+
const agent = stmts.getAgent.get(context.agentId);
|
|
288
|
+
if (!agent) throw new Error('agent not found');
|
|
289
|
+
const config = JSON.parse(agent.tools_config || '{}');
|
|
290
|
+
config[key] = value;
|
|
291
|
+
stmts.updateAgentToolsConfig.run(JSON.stringify(config), context.agentId);
|
|
292
|
+
return { stored: true, key };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function getCredential(input, context) {
|
|
296
|
+
const { key } = input;
|
|
297
|
+
const agent = stmts.getAgent.get(context.agentId);
|
|
298
|
+
if (!agent) return { key, value: null };
|
|
299
|
+
const config = JSON.parse(agent.tools_config || '{}');
|
|
300
|
+
return { key, value: config[key] || null };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function remember(input, context) {
|
|
304
|
+
const { summary } = input;
|
|
305
|
+
const id = uuid();
|
|
306
|
+
stmts.createMemory.run(id, context.agentId, summary);
|
|
307
|
+
return { remembered: true, summary };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function scheduleTask(input, context) {
|
|
311
|
+
const { description, interval_minutes } = input;
|
|
312
|
+
const mins = Math.max(1, interval_minutes);
|
|
313
|
+
const intervalMs = mins * 60 * 1000;
|
|
314
|
+
const id = uuid();
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
stmts.createSchedule.run(id, context.agentId, description, intervalMs, now + intervalMs);
|
|
317
|
+
return {
|
|
318
|
+
scheduled: true,
|
|
319
|
+
id,
|
|
320
|
+
description,
|
|
321
|
+
interval_minutes: mins,
|
|
322
|
+
next_run_at: new Date(now + intervalMs).toISOString()
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function cancelSchedule(input, context) {
|
|
327
|
+
const { schedule_id } = input;
|
|
328
|
+
stmts.deactivateSchedule.run(schedule_id, context.agentId);
|
|
329
|
+
return { cancelled: true, id: schedule_id };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function listSchedules(input, context) {
|
|
333
|
+
const schedules = stmts.agentSchedules.all(context.agentId);
|
|
334
|
+
return schedules.map(s => ({
|
|
335
|
+
id: s.id,
|
|
336
|
+
description: s.description,
|
|
337
|
+
interval_minutes: s.interval_ms / 60000,
|
|
338
|
+
next_run_at: new Date(s.next_run_at).toISOString(),
|
|
339
|
+
active: !!s.active
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function spawnSubagent(input, context) {
|
|
344
|
+
const { task, context: taskContext } = input;
|
|
345
|
+
let prompt = task;
|
|
346
|
+
if (taskContext) prompt = `context: ${taskContext}\n\ntask: ${task}`;
|
|
347
|
+
|
|
348
|
+
let model = 'opus';
|
|
349
|
+
if (context && context.agentId) {
|
|
350
|
+
const agent = stmts.getAgent.get(context.agentId);
|
|
351
|
+
if (agent && agent.model) model = agent.model;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
let done = false;
|
|
356
|
+
const args = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions'];
|
|
357
|
+
const proc = spawn('claude', args, {
|
|
358
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
359
|
+
timeout: 300000
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const fallback = setTimeout(() => {
|
|
363
|
+
if (!done) {
|
|
364
|
+
done = true;
|
|
365
|
+
proc.kill();
|
|
366
|
+
resolve({ error: 'subagent timed out after 5 minutes' });
|
|
367
|
+
}
|
|
368
|
+
}, 310000);
|
|
369
|
+
|
|
370
|
+
let stdout = '';
|
|
371
|
+
let stderr = '';
|
|
372
|
+
|
|
373
|
+
proc.stdout.on('data', d => stdout += d);
|
|
374
|
+
proc.stderr.on('data', d => stderr += d);
|
|
375
|
+
|
|
376
|
+
proc.on('close', code => {
|
|
377
|
+
if (done) return;
|
|
378
|
+
done = true;
|
|
379
|
+
clearTimeout(fallback);
|
|
380
|
+
if (!stdout.trim() && code !== 0) {
|
|
381
|
+
resolve({ error: `subagent exited ${code}: ${stderr.trim()}` });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(stdout.trim());
|
|
386
|
+
const text = typeof parsed.result === 'string' ? parsed.result : '';
|
|
387
|
+
resolve({ result: text });
|
|
388
|
+
} catch {
|
|
389
|
+
resolve({ result: stdout.trim() });
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
proc.on('error', err => {
|
|
394
|
+
if (done) return;
|
|
395
|
+
done = true;
|
|
396
|
+
clearTimeout(fallback);
|
|
397
|
+
resolve({ error: err.message });
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
proc.stdin.write(prompt);
|
|
401
|
+
proc.stdin.end();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function delegateToAgent(input, context) {
|
|
406
|
+
const { agent_name, message } = input;
|
|
407
|
+
|
|
408
|
+
const target = stmts.getAgentByName.get(agent_name);
|
|
409
|
+
if (!target) return { error: `agent "${agent_name}" not found` };
|
|
410
|
+
|
|
411
|
+
if (target.id === context.agentId) {
|
|
412
|
+
return { error: 'cannot delegate to yourself' };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const chat = require('./chat');
|
|
416
|
+
const response = await chat.enqueueMessage(target.id, message);
|
|
417
|
+
return { agent: target.name, response: response.content };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function readWorkspace(input, context) {
|
|
421
|
+
const agent = stmts.getAgent.get(context.agentId);
|
|
422
|
+
if (!agent) throw new Error('agent not found');
|
|
423
|
+
if (input.path.includes('..')) throw new Error('path cannot contain ..');
|
|
424
|
+
const content = workspace.readFile(agent.name, input.path);
|
|
425
|
+
return { path: input.path, content };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function writeWorkspace(input, context) {
|
|
429
|
+
const agent = stmts.getAgent.get(context.agentId);
|
|
430
|
+
if (!agent) throw new Error('agent not found');
|
|
431
|
+
if (input.path.includes('..')) throw new Error('path cannot contain ..');
|
|
432
|
+
workspace.writeFile(agent.name, input.path, input.content);
|
|
433
|
+
return { written: true, path: input.path };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function completeBootstrap(input, context) {
|
|
437
|
+
const agent = stmts.getAgent.get(context.agentId);
|
|
438
|
+
if (!agent) throw new Error('agent not found');
|
|
439
|
+
stmts.setBootstrapped.run(1, context.agentId);
|
|
440
|
+
workspace.deleteFile(agent.name, 'BOOTSTRAP.md');
|
|
441
|
+
const chat = require('./chat');
|
|
442
|
+
chat.emit(context.agentId, 'bootstrap_complete', {});
|
|
443
|
+
return { bootstrapped: true };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function getToolDefinitions(toolNames) {
|
|
447
|
+
if (!toolNames || !toolNames.length) return [];
|
|
448
|
+
return toolNames
|
|
449
|
+
.filter(name => registry[name])
|
|
450
|
+
.map(name => registry[name].definition);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getAllToolNames() {
|
|
454
|
+
return Object.keys(registry);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function getAllToolDefinitions() {
|
|
458
|
+
return Object.values(registry).map(t => t.definition);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function executeTool(name, input, context) {
|
|
462
|
+
const tool = registry[name];
|
|
463
|
+
if (!tool) throw new Error(`unknown tool: ${name}`);
|
|
464
|
+
return await tool.handler(input, context);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
module.exports = { getToolDefinitions, getAllToolNames, getAllToolDefinitions, executeTool };
|