@cordbot/agent 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/LICENSE +21 -0
- package/README.md +112 -0
- package/bin/cordbot.js +10 -0
- package/dist/agent/manager.js +256 -0
- package/dist/agent/stream.js +359 -0
- package/dist/auth.js +161 -0
- package/dist/cli.js +128 -0
- package/dist/discord/client.js +24 -0
- package/dist/discord/events.js +283 -0
- package/dist/discord/sync.js +148 -0
- package/dist/index.js +72 -0
- package/dist/init.js +82 -0
- package/dist/permissions/discord.js +66 -0
- package/dist/scheduler/parser.js +62 -0
- package/dist/scheduler/runner.js +201 -0
- package/dist/service/config.js +2 -0
- package/dist/service/manifest.js +27 -0
- package/dist/service/token-manager.js +143 -0
- package/dist/service/types.js +1 -0
- package/dist/storage/database.js +122 -0
- package/dist/tools/builtin-loader.js +21 -0
- package/dist/tools/cron/add_job.js +96 -0
- package/dist/tools/cron/list_jobs.js +55 -0
- package/dist/tools/cron/remove_job.js +64 -0
- package/dist/tools/cron/update_job.js +97 -0
- package/dist/tools/gmail/list_messages.js +87 -0
- package/dist/tools/gmail/send_email.js +118 -0
- package/dist/tools/loader.js +84 -0
- package/dist/tools/share_file.js +61 -0
- package/package.json +65 -0
- package/templates/.claude-cron.template +3 -0
- package/templates/channel-CLAUDE.md.template +26 -0
- package/templates/cron-skill.md +63 -0
- package/templates/root-CLAUDE.md.template +78 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { parseCronFile } from '../../scheduler/parser.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const schema = z.object({});
|
|
6
|
+
export function createTool(getCwd) {
|
|
7
|
+
return tool('cron_list_jobs', 'List all scheduled cron jobs for this Discord channel. Shows job names, schedules, tasks, and whether they are one-time. Use this to see what jobs are currently configured before adding, updating, or removing jobs.', schema.shape, async () => {
|
|
8
|
+
try {
|
|
9
|
+
const cwd = getCwd();
|
|
10
|
+
const cronPath = path.join(cwd, '.claude-cron');
|
|
11
|
+
const config = parseCronFile(cronPath);
|
|
12
|
+
if (config.jobs.length === 0) {
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
jobs: [],
|
|
19
|
+
message: 'No cron jobs configured for this channel.'
|
|
20
|
+
}, null, 2)
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: JSON.stringify({
|
|
30
|
+
jobs: config.jobs.map(job => ({
|
|
31
|
+
name: job.name,
|
|
32
|
+
schedule: job.schedule,
|
|
33
|
+
task: job.task,
|
|
34
|
+
oneTime: job.oneTime || false
|
|
35
|
+
})),
|
|
36
|
+
count: config.jobs.length
|
|
37
|
+
}, null, 2)
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
error: `Failed to list cron jobs: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
49
|
+
}, null, 2)
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { parseCronFile } from '../../scheduler/parser.js';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
const schema = z.object({
|
|
8
|
+
name: z.string().describe('Name of the cron job to remove')
|
|
9
|
+
});
|
|
10
|
+
export function createTool(getCwd) {
|
|
11
|
+
return tool('cron_remove_job', 'Remove a scheduled cron job from this Discord channel by name. The job will stop running immediately. Use cron_list_jobs first to see available job names.', schema.shape, async (params) => {
|
|
12
|
+
try {
|
|
13
|
+
const cwd = getCwd();
|
|
14
|
+
const cronPath = path.join(cwd, '.claude-cron');
|
|
15
|
+
// Read existing jobs
|
|
16
|
+
const config = parseCronFile(cronPath);
|
|
17
|
+
// Find the job
|
|
18
|
+
const jobToRemove = config.jobs.find(job => job.name === params.name);
|
|
19
|
+
if (!jobToRemove) {
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{
|
|
23
|
+
type: 'text',
|
|
24
|
+
text: JSON.stringify({
|
|
25
|
+
error: `Job "${params.name}" not found.`,
|
|
26
|
+
availableJobs: config.jobs.map(j => j.name)
|
|
27
|
+
}, null, 2)
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Remove the job
|
|
33
|
+
const updatedJobs = config.jobs.filter(job => job.name !== params.name);
|
|
34
|
+
// Write back to file
|
|
35
|
+
const yamlContent = yaml.dump({ jobs: updatedJobs });
|
|
36
|
+
fs.writeFileSync(cronPath, yamlContent, 'utf-8');
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
success: true,
|
|
43
|
+
message: `Cron job "${params.name}" removed successfully!`,
|
|
44
|
+
removedJob: jobToRemove,
|
|
45
|
+
remainingJobs: updatedJobs.length
|
|
46
|
+
}, null, 2)
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: JSON.stringify({
|
|
57
|
+
error: `Failed to remove cron job: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
58
|
+
}, null, 2)
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { parseCronFile, validateCronSchedule } from '../../scheduler/parser.js';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
const schema = z.object({
|
|
8
|
+
name: z.string().describe('Name of the cron job to update'),
|
|
9
|
+
schedule: z.string().optional().describe('New cron schedule (leave empty to keep current)'),
|
|
10
|
+
task: z.string().optional().describe('New task description (leave empty to keep current)'),
|
|
11
|
+
oneTime: z.boolean().optional().describe('Update one-time flag (leave empty to keep current)')
|
|
12
|
+
});
|
|
13
|
+
export function createTool(getCwd) {
|
|
14
|
+
return tool('cron_update_job', 'Update an existing cron job\'s schedule, task, or one-time setting. You can update one or more fields. Use cron_list_jobs first to see current job details.', schema.shape, async (params) => {
|
|
15
|
+
try {
|
|
16
|
+
// Validate new schedule if provided
|
|
17
|
+
if (params.schedule && !validateCronSchedule(params.schedule)) {
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: 'text',
|
|
22
|
+
text: JSON.stringify({
|
|
23
|
+
error: `Invalid cron schedule format: "${params.schedule}". Must be 5 fields: minute hour day month weekday.`,
|
|
24
|
+
validFormat: 'minute hour day month weekday',
|
|
25
|
+
examples: [
|
|
26
|
+
'0 9 * * * - Every day at 9:00 AM',
|
|
27
|
+
'0 9 * * 1 - Every Monday at 9:00 AM',
|
|
28
|
+
'*/30 * * * * - Every 30 minutes'
|
|
29
|
+
]
|
|
30
|
+
}, null, 2)
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const cwd = getCwd();
|
|
36
|
+
const cronPath = path.join(cwd, '.claude-cron');
|
|
37
|
+
// Read existing jobs
|
|
38
|
+
const config = parseCronFile(cronPath);
|
|
39
|
+
// Find the job
|
|
40
|
+
const jobIndex = config.jobs.findIndex(job => job.name === params.name);
|
|
41
|
+
if (jobIndex === -1) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: JSON.stringify({
|
|
47
|
+
error: `Job "${params.name}" not found.`,
|
|
48
|
+
availableJobs: config.jobs.map(j => j.name)
|
|
49
|
+
}, null, 2)
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const oldJob = { ...config.jobs[jobIndex] };
|
|
55
|
+
// Update fields
|
|
56
|
+
if (params.schedule) {
|
|
57
|
+
config.jobs[jobIndex].schedule = params.schedule;
|
|
58
|
+
}
|
|
59
|
+
if (params.task) {
|
|
60
|
+
config.jobs[jobIndex].task = params.task;
|
|
61
|
+
}
|
|
62
|
+
if (params.oneTime !== undefined) {
|
|
63
|
+
config.jobs[jobIndex].oneTime = params.oneTime;
|
|
64
|
+
}
|
|
65
|
+
// Write back to file
|
|
66
|
+
const yamlContent = yaml.dump({ jobs: config.jobs });
|
|
67
|
+
fs.writeFileSync(cronPath, yamlContent, 'utf-8');
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: JSON.stringify({
|
|
73
|
+
success: true,
|
|
74
|
+
message: `Cron job "${params.name}" updated successfully!`,
|
|
75
|
+
changes: {
|
|
76
|
+
before: oldJob,
|
|
77
|
+
after: config.jobs[jobIndex]
|
|
78
|
+
}
|
|
79
|
+
}, null, 2)
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: JSON.stringify({
|
|
90
|
+
error: `Failed to update cron job: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
91
|
+
}, null, 2)
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const gmailListMessagesSchema = z.object({
|
|
5
|
+
maxResults: z.number().min(1).max(100).default(10).optional().describe('Max messages to return (1-100)'),
|
|
6
|
+
query: z.string().optional().describe('Gmail search query (e.g., "from:example@gmail.com", "subject:meeting", "is:unread")')
|
|
7
|
+
});
|
|
8
|
+
export function createTool(context) {
|
|
9
|
+
return tool('gmail_list_messages', 'List email messages from Gmail inbox with optional search query and filters', gmailListMessagesSchema.shape, async (params) => {
|
|
10
|
+
// Get fresh token from token manager
|
|
11
|
+
const tokenData = await context.getToken('gmail');
|
|
12
|
+
if (!tokenData) {
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({
|
|
18
|
+
error: 'Gmail token unavailable. Please reconnect Gmail in the dashboard.',
|
|
19
|
+
requiresReauth: true
|
|
20
|
+
}, null, 2)
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
// Initialize Gmail API with fresh token
|
|
27
|
+
const oauth2Client = new google.auth.OAuth2();
|
|
28
|
+
oauth2Client.setCredentials({ access_token: tokenData.accessToken });
|
|
29
|
+
const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
|
|
30
|
+
// List messages
|
|
31
|
+
const listResponse = await gmail.users.messages.list({
|
|
32
|
+
userId: 'me',
|
|
33
|
+
maxResults: Math.min(params.maxResults || 10, 100),
|
|
34
|
+
q: params.query
|
|
35
|
+
});
|
|
36
|
+
if (!listResponse.data.messages?.length) {
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: JSON.stringify({ messages: [], message: 'No messages found.' }, null, 2)
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Fetch message details
|
|
47
|
+
const messages = await Promise.all(listResponse.data.messages.map(async (msg) => {
|
|
48
|
+
const detail = await gmail.users.messages.get({
|
|
49
|
+
userId: 'me',
|
|
50
|
+
id: msg.id,
|
|
51
|
+
format: 'metadata',
|
|
52
|
+
metadataHeaders: ['Subject', 'From', 'Date']
|
|
53
|
+
});
|
|
54
|
+
const headers = detail.data.payload?.headers || [];
|
|
55
|
+
const getHeader = (name) => headers.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value || '';
|
|
56
|
+
return {
|
|
57
|
+
id: msg.id,
|
|
58
|
+
subject: getHeader('Subject'),
|
|
59
|
+
from: getHeader('From'),
|
|
60
|
+
date: getHeader('Date'),
|
|
61
|
+
snippet: detail.data.snippet || ''
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: JSON.stringify({ messages, totalResults: messages.length }, null, 2)
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('Gmail API error:', error);
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: JSON.stringify({
|
|
80
|
+
error: `Failed to list Gmail messages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
81
|
+
}, null, 2)
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const gmailSendEmailSchema = z.object({
|
|
5
|
+
to: z.string().email().describe('Recipient email address'),
|
|
6
|
+
subject: z.string().describe('Email subject line'),
|
|
7
|
+
body: z.string().describe('Email body content (plain text or HTML)'),
|
|
8
|
+
cc: z.string().email().optional().describe('CC email address (optional)'),
|
|
9
|
+
bcc: z.string().email().optional().describe('BCC email address (optional)')
|
|
10
|
+
});
|
|
11
|
+
export function createTool(context) {
|
|
12
|
+
return tool('gmail_send_email', 'Send an email via Gmail. Requires user approval before sending. The email will be sent from the authenticated Gmail account.', gmailSendEmailSchema.shape, async (params) => {
|
|
13
|
+
// Request permission from user
|
|
14
|
+
const permissionMessage = [
|
|
15
|
+
`Send email to **${params.to}**`,
|
|
16
|
+
params.cc ? `CC: ${params.cc}` : null,
|
|
17
|
+
params.bcc ? `BCC: ${params.bcc}` : null,
|
|
18
|
+
`Subject: "${params.subject}"`,
|
|
19
|
+
`\nBody preview: ${params.body.slice(0, 100)}${params.body.length > 100 ? '...' : ''}`
|
|
20
|
+
].filter(Boolean).join('\n');
|
|
21
|
+
try {
|
|
22
|
+
await context.requestPermission(permissionMessage);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: JSON.stringify({
|
|
30
|
+
error: error instanceof Error ? error.message : 'Permission denied',
|
|
31
|
+
cancelled: true
|
|
32
|
+
}, null, 2)
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Get fresh token from token manager
|
|
38
|
+
const tokenData = await context.getToken('gmail');
|
|
39
|
+
if (!tokenData) {
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: JSON.stringify({
|
|
45
|
+
error: 'Gmail token unavailable. Please reconnect Gmail in the dashboard.',
|
|
46
|
+
requiresReauth: true
|
|
47
|
+
}, null, 2)
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// Initialize Gmail API with fresh token
|
|
54
|
+
const oauth2Client = new google.auth.OAuth2();
|
|
55
|
+
oauth2Client.setCredentials({ access_token: tokenData.accessToken });
|
|
56
|
+
const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
|
|
57
|
+
// Compose email in RFC 2822 format
|
|
58
|
+
const emailLines = [
|
|
59
|
+
`To: ${params.to}`,
|
|
60
|
+
`Subject: ${params.subject}`
|
|
61
|
+
];
|
|
62
|
+
if (params.cc) {
|
|
63
|
+
emailLines.push(`Cc: ${params.cc}`);
|
|
64
|
+
}
|
|
65
|
+
if (params.bcc) {
|
|
66
|
+
emailLines.push(`Bcc: ${params.bcc}`);
|
|
67
|
+
}
|
|
68
|
+
// Add MIME headers
|
|
69
|
+
emailLines.push('MIME-Version: 1.0');
|
|
70
|
+
emailLines.push('Content-Type: text/plain; charset=utf-8');
|
|
71
|
+
emailLines.push(''); // Empty line separates headers from body
|
|
72
|
+
emailLines.push(params.body);
|
|
73
|
+
const email = emailLines.join('\r\n');
|
|
74
|
+
// Encode email in base64url format
|
|
75
|
+
const encodedEmail = Buffer.from(email)
|
|
76
|
+
.toString('base64')
|
|
77
|
+
.replace(/\+/g, '-')
|
|
78
|
+
.replace(/\//g, '_')
|
|
79
|
+
.replace(/=+$/, '');
|
|
80
|
+
// Send email
|
|
81
|
+
const response = await gmail.users.messages.send({
|
|
82
|
+
userId: 'me',
|
|
83
|
+
requestBody: {
|
|
84
|
+
raw: encodedEmail
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: JSON.stringify({
|
|
92
|
+
success: true,
|
|
93
|
+
message: 'Email sent successfully!',
|
|
94
|
+
messageId: response.data.id,
|
|
95
|
+
threadId: response.data.threadId,
|
|
96
|
+
to: params.to,
|
|
97
|
+
subject: params.subject
|
|
98
|
+
}, null, 2)
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error('Gmail send error:', error);
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: JSON.stringify({
|
|
110
|
+
error: `Failed to send email: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
111
|
+
details: error instanceof Error ? error.stack : undefined
|
|
112
|
+
}, null, 2)
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { permissionManager } from '../discord/events.js';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
export async function loadDynamicTools(manifest, tokenManager, getCurrentChannel) {
|
|
9
|
+
const tools = [];
|
|
10
|
+
// Create tool context with ALL tokens - tools will request what they need
|
|
11
|
+
const toolContext = {
|
|
12
|
+
getToken: (category) => tokenManager.getToken(category),
|
|
13
|
+
requestPermission: async (message) => {
|
|
14
|
+
const channel = getCurrentChannel();
|
|
15
|
+
if (!channel) {
|
|
16
|
+
throw new Error('No Discord channel context available for permission request');
|
|
17
|
+
}
|
|
18
|
+
const requestId = nanoid();
|
|
19
|
+
await permissionManager.requestPermission(channel, message, requestId);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
// Check if there are any tools configured
|
|
23
|
+
if (!manifest.toolsConfig || Object.keys(manifest.toolsConfig).length === 0) {
|
|
24
|
+
console.log(' ℹ️ No tools enabled in manifest');
|
|
25
|
+
return tools;
|
|
26
|
+
}
|
|
27
|
+
const toolsDir = __dirname;
|
|
28
|
+
// Iterate over each domain (e.g., gmail, calendar, etc.)
|
|
29
|
+
for (const [domain, toolNames] of Object.entries(manifest.toolsConfig)) {
|
|
30
|
+
await loadDomainTools(domain, toolNames, toolsDir, toolContext, tools);
|
|
31
|
+
}
|
|
32
|
+
return tools;
|
|
33
|
+
}
|
|
34
|
+
async function loadDomainTools(domain, toolNames, toolsDir, toolContext, tools) {
|
|
35
|
+
const domainDir = path.join(toolsDir, domain);
|
|
36
|
+
// Check if domain folder exists
|
|
37
|
+
if (!fs.existsSync(domainDir) || !fs.statSync(domainDir).isDirectory()) {
|
|
38
|
+
console.warn(` ⚠️ Domain folder not found: ${domain}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Load each tool in this domain
|
|
42
|
+
for (const toolName of toolNames) {
|
|
43
|
+
const toolId = `${domain}_${toolName}`;
|
|
44
|
+
// Look for .ts or .js file
|
|
45
|
+
const possibleFiles = [
|
|
46
|
+
path.join(domainDir, `${toolName}.ts`),
|
|
47
|
+
path.join(domainDir, `${toolName}.js`)
|
|
48
|
+
];
|
|
49
|
+
const toolFile = possibleFiles.find(f => fs.existsSync(f));
|
|
50
|
+
if (!toolFile) {
|
|
51
|
+
console.warn(` ⚠️ Tool file not found: ${domain}/${toolName}`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
// Dynamically import the tool module
|
|
56
|
+
const toolModule = await import(toolFile);
|
|
57
|
+
// Look for the createTool export (or other common patterns)
|
|
58
|
+
const createFn = toolModule.createTool ||
|
|
59
|
+
toolModule[`create${toCamelCase(toolId)}Tool`] ||
|
|
60
|
+
toolModule.default;
|
|
61
|
+
if (typeof createFn !== 'function') {
|
|
62
|
+
console.warn(` ⚠️ Tool ${toolId}: No createTool export found`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// Create the tool with the context (all tokens available)
|
|
66
|
+
const tool = createFn(toolContext);
|
|
67
|
+
tools.push(tool);
|
|
68
|
+
console.log(` ✓ Loaded tool: ${toolId}`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(` ✗ Failed to load tool ${toolId}:`, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Convert snake_case to CamelCase
|
|
77
|
+
* gmail_send_email -> GmailSendEmail
|
|
78
|
+
*/
|
|
79
|
+
function toCamelCase(str) {
|
|
80
|
+
return str
|
|
81
|
+
.split('_')
|
|
82
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
83
|
+
.join('');
|
|
84
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
const schema = z.object({
|
|
6
|
+
filePath: z.string().describe('Path to file to share with the user via Discord attachment. Can be an absolute path or relative to current working directory.')
|
|
7
|
+
});
|
|
8
|
+
export function createTool(getCwd, queueFileForSharing) {
|
|
9
|
+
return tool('shareFile', 'Share a file with the user by attaching it to Discord. The file will be attached after your response completes. Use this to send generated diagrams, reports, code files, or any other files you want to share. Accepts absolute paths or paths relative to the current working directory.', schema.shape, async ({ filePath }) => {
|
|
10
|
+
try {
|
|
11
|
+
const cwd = getCwd();
|
|
12
|
+
// If path is absolute, use it directly; otherwise join with cwd
|
|
13
|
+
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
14
|
+
// Validate file exists
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(fullPath);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
error: `File not found: ${filePath}`,
|
|
25
|
+
attemptedPath: fullPath,
|
|
26
|
+
workingDirectory: cwd
|
|
27
|
+
}, null, 2)
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
isError: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Queue the file for attachment
|
|
34
|
+
queueFileForSharing(fullPath);
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: JSON.stringify({
|
|
40
|
+
success: true,
|
|
41
|
+
message: `File "${filePath}" will be attached to Discord after response completes`
|
|
42
|
+
}, null, 2)
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: JSON.stringify({
|
|
53
|
+
error: `Failed to queue file: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
54
|
+
}, null, 2)
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
isError: true
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cordbot/agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Discord bot powered by Claude Agent SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cordbot": "./bin/cordbot.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bin",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "tsx watch src/cli.ts",
|
|
17
|
+
"build": "tsc && chmod +x bin/cordbot.js",
|
|
18
|
+
"start": "node dist/cli.js",
|
|
19
|
+
"copy-readme": "node scripts/copy-readme.js",
|
|
20
|
+
"prepublishOnly": "npm run copy-readme && npm run build",
|
|
21
|
+
"test": "vitest",
|
|
22
|
+
"test:ui": "vitest --ui",
|
|
23
|
+
"test:run": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"discord",
|
|
27
|
+
"claude",
|
|
28
|
+
"bot",
|
|
29
|
+
"ai",
|
|
30
|
+
"coding",
|
|
31
|
+
"assistant"
|
|
32
|
+
],
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.0",
|
|
37
|
+
"@types/better-sqlite3": "^7.6.8",
|
|
38
|
+
"@types/express": "^5.0.6",
|
|
39
|
+
"better-sqlite3": "^9.2.2",
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"chokidar": "^3.5.3",
|
|
42
|
+
"discord.js": "^14.14.1",
|
|
43
|
+
"dotenv": "^16.3.1",
|
|
44
|
+
"express": "^5.2.1",
|
|
45
|
+
"googleapis": "^128.0.0",
|
|
46
|
+
"inquirer": "^9.2.12",
|
|
47
|
+
"js-yaml": "^4.1.0",
|
|
48
|
+
"node-cron": "^3.0.3",
|
|
49
|
+
"open": "^11.0.0",
|
|
50
|
+
"ora": "^8.0.1",
|
|
51
|
+
"zod": "^3.24.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/inquirer": "^9.0.7",
|
|
55
|
+
"@types/js-yaml": "^4.0.9",
|
|
56
|
+
"@types/node": "^20.10.6",
|
|
57
|
+
"@types/node-cron": "^3.0.11",
|
|
58
|
+
"tsx": "^4.7.0",
|
|
59
|
+
"typescript": "^5.3.3",
|
|
60
|
+
"vitest": "^1.2.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=18.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# {{CHANNEL_NAME}}
|
|
2
|
+
|
|
3
|
+
<!-- Add channel-specific project context here -->
|
|
4
|
+
|
|
5
|
+
**Working Directory:** `{{FOLDER_PATH}}`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Automation
|
|
10
|
+
|
|
11
|
+
This channel supports scheduled autonomous tasks via `.claude-cron`.
|
|
12
|
+
|
|
13
|
+
**Example `.claude-cron`:**
|
|
14
|
+
```yaml
|
|
15
|
+
jobs:
|
|
16
|
+
- name: "Daily summary"
|
|
17
|
+
schedule: "0 9 * * *"
|
|
18
|
+
task: "Summarize recent changes"
|
|
19
|
+
postTo: "thread"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Results will be posted as threads in this channel.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
See the root CLAUDE.md file for general Discord bot behavior and capabilities.
|