kennel 0.1.8
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 +203 -0
- package/bin/kennel.mjs +3 -0
- package/dist/automations/notifications.d.ts +40 -0
- package/dist/automations/notifications.js +220 -0
- package/dist/cli/dispatch.d.ts +1 -0
- package/dist/cli/dispatch.js +51 -0
- package/dist/cli/usage.d.ts +1 -0
- package/dist/cli/usage.js +14 -0
- package/dist/deploy/cloudflare.d.ts +6 -0
- package/dist/deploy/cloudflare.js +91 -0
- package/dist/deploy/environment.d.ts +2 -0
- package/dist/deploy/environment.js +47 -0
- package/dist/deploy/smoke-tests.d.ts +3 -0
- package/dist/deploy/smoke-tests.js +71 -0
- package/dist/deploy/trigger-dev.d.ts +2 -0
- package/dist/deploy/trigger-dev.js +255 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +8 -0
- package/dist/mcp/agent-tools.d.ts +8 -0
- package/dist/mcp/agent-tools.js +247 -0
- package/dist/mcp/config.d.ts +25 -0
- package/dist/mcp/config.js +171 -0
- package/dist/mcp/data.d.ts +14 -0
- package/dist/mcp/data.js +120 -0
- package/dist/mcp/function-source.d.ts +7 -0
- package/dist/mcp/function-source.js +40 -0
- package/dist/mcp/generate.d.ts +1 -0
- package/dist/mcp/generate.js +47 -0
- package/dist/mcp/install-page.d.ts +1 -0
- package/dist/mcp/install-page.js +360 -0
- package/dist/notifications/email-worker.d.ts +8 -0
- package/dist/notifications/email-worker.js +137 -0
- package/dist/package/mcp-data.d.ts +1 -0
- package/dist/package/mcp-data.js +1 -0
- package/dist/package/mcp-email-worker.d.ts +1 -0
- package/dist/package/mcp-email-worker.js +1 -0
- package/dist/package/mcp-http.d.ts +1 -0
- package/dist/package/mcp-http.js +1 -0
- package/dist/package/mcp-stdio.d.ts +1 -0
- package/dist/package/mcp-stdio.js +1 -0
- package/dist/package/mcp.d.ts +1 -0
- package/dist/package/mcp.js +1 -0
- package/dist/publish/cloudflare-init.d.ts +12 -0
- package/dist/publish/cloudflare-init.js +98 -0
- package/dist/publish/config.d.ts +28 -0
- package/dist/publish/config.js +190 -0
- package/dist/publish/options.d.ts +13 -0
- package/dist/publish/options.js +78 -0
- package/dist/schema/doctor.d.ts +1 -0
- package/dist/schema/doctor.js +69 -0
- package/dist/schema/documents.d.ts +18 -0
- package/dist/schema/documents.js +127 -0
- package/dist/schema/pointers.d.ts +5 -0
- package/dist/schema/pointers.js +48 -0
- package/dist/schema/types.d.ts +36 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/validation.d.ts +8 -0
- package/dist/schema/validation.js +212 -0
- package/dist/shared/json.d.ts +4 -0
- package/dist/shared/json.js +56 -0
- package/dist/shared/object.d.ts +2 -0
- package/dist/shared/object.js +3 -0
- package/dist/shared/package-manager.d.ts +11 -0
- package/dist/shared/package-manager.js +39 -0
- package/dist/shared/path.d.ts +4 -0
- package/dist/shared/path.js +40 -0
- package/dist/shared/process.d.ts +7 -0
- package/dist/shared/process.js +12 -0
- package/dist/shared/project-config.d.ts +2 -0
- package/dist/shared/project-config.js +26 -0
- package/dist/site/dev.d.ts +10 -0
- package/dist/site/dev.js +128 -0
- package/dist/site/vitepress.d.ts +21 -0
- package/dist/site/vitepress.js +66 -0
- package/package.json +74 -0
- package/templates/cloudflare/email-code-worker.js +344 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { commandLine } from '../shared/process.js';
|
|
4
|
+
import { dopplerTokenCommand, hasDopplerTokenConfig, publishAccountId, } from '../publish/config.js';
|
|
5
|
+
function captureEnvironment(root, config, options) {
|
|
6
|
+
const previous = {
|
|
7
|
+
token: process.env.CLOUDFLARE_API_TOKEN,
|
|
8
|
+
account: process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
9
|
+
};
|
|
10
|
+
const accountId = publishAccountId(config);
|
|
11
|
+
if (accountId)
|
|
12
|
+
process.env.CLOUDFLARE_ACCOUNT_ID = accountId;
|
|
13
|
+
const tokenCommand = options.useDopplerToken || hasDopplerTokenConfig(config)
|
|
14
|
+
? dopplerTokenCommand(config)
|
|
15
|
+
: null;
|
|
16
|
+
if (tokenCommand) {
|
|
17
|
+
console.log(`${commandLine(tokenCommand.command, tokenCommand.args)} -> CLOUDFLARE_API_TOKEN`);
|
|
18
|
+
if (!options.dryRun) {
|
|
19
|
+
const result = spawnSync(tokenCommand.command, tokenCommand.args, {
|
|
20
|
+
cwd: root,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
23
|
+
});
|
|
24
|
+
if (result.status !== 0)
|
|
25
|
+
process.exit(result.status ?? 1);
|
|
26
|
+
process.env.CLOUDFLARE_API_TOKEN = result.stdout.trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return previous;
|
|
30
|
+
}
|
|
31
|
+
function restoreEnvironment(previous) {
|
|
32
|
+
if (previous.token === undefined) {
|
|
33
|
+
delete process.env.CLOUDFLARE_API_TOKEN;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
process.env.CLOUDFLARE_API_TOKEN = previous.token;
|
|
37
|
+
}
|
|
38
|
+
if (previous.account === undefined) {
|
|
39
|
+
delete process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
process.env.CLOUDFLARE_ACCOUNT_ID = previous.account;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function withCloudflareEnvironment(root, config, options, callback) {
|
|
46
|
+
return Effect.runSync(Effect.acquireUseRelease(Effect.sync(() => captureEnvironment(root, config, options)), () => Effect.sync(callback), (previous) => Effect.sync(() => restoreEnvironment(previous))));
|
|
47
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PublishOptions } from '../publish/options.js';
|
|
2
|
+
export declare function runHttpTest(root: string, hostname: string, testEmail: string, options: PublishOptions): void;
|
|
3
|
+
export declare function runAccessHttpTest(root: string, hostname: string, options: PublishOptions): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { commandExists, commandLine } from '../shared/process.js';
|
|
3
|
+
import { publishTestUrl } from '../publish/config.js';
|
|
4
|
+
export function runHttpTest(root, hostname, testEmail, options) {
|
|
5
|
+
const baseUrl = publishTestUrl(hostname);
|
|
6
|
+
if (!baseUrl) {
|
|
7
|
+
console.log('skip test: no hostname configured');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!commandExists('curl')) {
|
|
11
|
+
console.log('skip test: curl is not available');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const rootTest = {
|
|
15
|
+
command: 'curl',
|
|
16
|
+
args: ['-fsS', '-H', 'Cache-Control: no-cache', baseUrl],
|
|
17
|
+
};
|
|
18
|
+
console.log(`${commandLine(rootTest.command, rootTest.args)} | contains "Sign in with email"`);
|
|
19
|
+
if (!options.dryRun) {
|
|
20
|
+
const result = spawnSync(rootTest.command, rootTest.args, { cwd: root, encoding: 'utf8' });
|
|
21
|
+
if (result.status !== 0 || !result.stdout.includes('Sign in with email')) {
|
|
22
|
+
throw new Error(`${baseUrl} did not return the Kennel sign-in page`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const invalidTest = {
|
|
26
|
+
command: 'curl',
|
|
27
|
+
args: ['-fsS', '-X', 'POST', `${baseUrl}/__kennel_auth/request`, '-F', 'email=outside@example.com', '-F', 'redirect=/'],
|
|
28
|
+
};
|
|
29
|
+
console.log(`${commandLine(invalidTest.command, invalidTest.args)} | contains "approved email"`);
|
|
30
|
+
if (!options.dryRun) {
|
|
31
|
+
const result = spawnSync(invalidTest.command, invalidTest.args, { cwd: root, encoding: 'utf8' });
|
|
32
|
+
if (result.status !== 0 || !result.stdout.includes('approved email')) {
|
|
33
|
+
throw new Error(`${baseUrl} did not reject an outside email address`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!testEmail)
|
|
37
|
+
return;
|
|
38
|
+
const sendTest = {
|
|
39
|
+
command: 'curl',
|
|
40
|
+
args: ['-fsS', '-X', 'POST', `${baseUrl}/__kennel_auth/request`, '-F', `email=${testEmail}`, '-F', 'redirect=/'],
|
|
41
|
+
};
|
|
42
|
+
console.log(`${commandLine(sendTest.command, sendTest.args)} | contains "Check your email"`);
|
|
43
|
+
if (!options.dryRun) {
|
|
44
|
+
const result = spawnSync(sendTest.command, sendTest.args, { cwd: root, encoding: 'utf8' });
|
|
45
|
+
if (result.status !== 0 || !result.stdout.includes('Check your email')) {
|
|
46
|
+
throw new Error(`${baseUrl} did not advance an approved email to code entry`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function runAccessHttpTest(root, hostname, options) {
|
|
51
|
+
const baseUrl = publishTestUrl(hostname);
|
|
52
|
+
if (!baseUrl) {
|
|
53
|
+
console.log('skip test: no hostname configured');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!commandExists('curl')) {
|
|
57
|
+
console.log('skip test: curl is not available');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const accessTest = {
|
|
61
|
+
command: 'curl',
|
|
62
|
+
args: ['-fsSI', '-H', 'Cache-Control: no-cache', baseUrl],
|
|
63
|
+
};
|
|
64
|
+
console.log(`${commandLine(accessTest.command, accessTest.args)} | redirects to Cloudflare Access`);
|
|
65
|
+
if (!options.dryRun) {
|
|
66
|
+
const result = spawnSync(accessTest.command, accessTest.args, { cwd: root, encoding: 'utf8' });
|
|
67
|
+
if (result.status !== 0 || !/cloudflareaccess\.com|cdn-cgi\/access/i.test(result.stdout)) {
|
|
68
|
+
throw new Error(`${baseUrl} did not redirect to Cloudflare Access`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { isObject } from '../shared/object.js';
|
|
5
|
+
import { commandLine } from '../shared/process.js';
|
|
6
|
+
import { publicationConfig, publishProjectName } from '../publish/config.js';
|
|
7
|
+
export function syncTriggerDev(root, config, options) {
|
|
8
|
+
const triggerDev = triggerDevConfig(config);
|
|
9
|
+
if (!triggerDev)
|
|
10
|
+
return;
|
|
11
|
+
if (!options.deploy)
|
|
12
|
+
return;
|
|
13
|
+
const secretKeyEnv = typeof triggerDev.secretKeyEnv === 'string' && triggerDev.secretKeyEnv.trim()
|
|
14
|
+
? triggerDev.secretKeyEnv.trim()
|
|
15
|
+
: 'TRIGGER_SECRET_KEY';
|
|
16
|
+
const previousSecret = process.env[secretKeyEnv];
|
|
17
|
+
try {
|
|
18
|
+
hydrateTriggerSecret(root, triggerDev, secretKeyEnv, options);
|
|
19
|
+
syncTriggerDevWithSecret(root, config, triggerDev, secretKeyEnv, options);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (previousSecret === undefined) {
|
|
23
|
+
delete process.env[secretKeyEnv];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
process.env[secretKeyEnv] = previousSecret;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function syncTriggerDevWithSecret(root, config, triggerDev, secretKeyEnv, options) {
|
|
31
|
+
const cwd = typeof triggerDev.cwd === 'string' && triggerDev.cwd.trim()
|
|
32
|
+
? resolve(root, triggerDev.cwd.trim())
|
|
33
|
+
: root;
|
|
34
|
+
const deployCommand = typeof triggerDev.deployCommand === 'string' ? triggerDev.deployCommand.trim() : '';
|
|
35
|
+
if (deployCommand) {
|
|
36
|
+
runTriggerDevCommand(cwd, deployCommand, options);
|
|
37
|
+
}
|
|
38
|
+
const schedules = triggerDevSchedules(triggerDev, config, root);
|
|
39
|
+
if (schedules.length === 0)
|
|
40
|
+
return;
|
|
41
|
+
writeAutomationConfig(root, schedules, options);
|
|
42
|
+
logScheduleNotifications(schedules);
|
|
43
|
+
const apiUrl = triggerDevApiUrl(triggerDev);
|
|
44
|
+
const secretKey = process.env[secretKeyEnv];
|
|
45
|
+
if (!options.dryRun && !secretKey) {
|
|
46
|
+
throw new Error(`Trigger.dev schedules require ${secretKeyEnv}`);
|
|
47
|
+
}
|
|
48
|
+
for (const schedule of schedules) {
|
|
49
|
+
console.log(`trigger.dev schedule ${schedule.task} ${schedule.cron} ${schedule.timezone} dedup ${schedule.deduplicationKey}`);
|
|
50
|
+
if (options.dryRun)
|
|
51
|
+
continue;
|
|
52
|
+
createOrUpdateSchedule(apiUrl, secretKey ?? '', schedule);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function hydrateTriggerSecret(root, triggerDev, secretKeyEnv, options) {
|
|
56
|
+
const tokenCommand = triggerDevDopplerTokenCommand(triggerDev);
|
|
57
|
+
if (!tokenCommand)
|
|
58
|
+
return;
|
|
59
|
+
console.log(`${commandLine(tokenCommand.command, tokenCommand.args)} -> ${secretKeyEnv}`);
|
|
60
|
+
if (options.dryRun)
|
|
61
|
+
return;
|
|
62
|
+
const result = spawnSync(tokenCommand.command, tokenCommand.args, {
|
|
63
|
+
cwd: root,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
66
|
+
});
|
|
67
|
+
if (result.status !== 0)
|
|
68
|
+
process.exit(result.status ?? 1);
|
|
69
|
+
process.env[secretKeyEnv] = result.stdout.trim();
|
|
70
|
+
}
|
|
71
|
+
function triggerDevDopplerTokenCommand(triggerDev) {
|
|
72
|
+
const token = isObject(triggerDev.token)
|
|
73
|
+
? triggerDev.token
|
|
74
|
+
: isObject(triggerDev.dopplerToken)
|
|
75
|
+
? triggerDev.dopplerToken
|
|
76
|
+
: null;
|
|
77
|
+
if (!token)
|
|
78
|
+
return null;
|
|
79
|
+
const secret = typeof token.secret === 'string' && token.secret.trim() ? token.secret.trim() : '';
|
|
80
|
+
if (!secret)
|
|
81
|
+
return null;
|
|
82
|
+
const args = ['secrets', 'get', secret];
|
|
83
|
+
if (typeof token.scope === 'string' && token.scope.trim())
|
|
84
|
+
args.push('--scope', token.scope.trim());
|
|
85
|
+
if (typeof token.project === 'string' && token.project.trim())
|
|
86
|
+
args.push('--project', token.project.trim());
|
|
87
|
+
if (typeof token.config === 'string' && token.config.trim())
|
|
88
|
+
args.push('--config', token.config.trim());
|
|
89
|
+
args.push('--plain', '--raw');
|
|
90
|
+
return { command: 'doppler', args };
|
|
91
|
+
}
|
|
92
|
+
function triggerDevConfig(config) {
|
|
93
|
+
const publication = publicationConfig(config);
|
|
94
|
+
if (isObject(publication.triggerDev))
|
|
95
|
+
return publication.triggerDev;
|
|
96
|
+
if (isObject(publication.trigger))
|
|
97
|
+
return publication.trigger;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function triggerDevSchedules(triggerDev, config, root) {
|
|
101
|
+
if (!Array.isArray(triggerDev.schedules))
|
|
102
|
+
return [];
|
|
103
|
+
const projectName = publishProjectName(config, root);
|
|
104
|
+
return triggerDev.schedules.map((entry, index) => normalizeSchedule(entry, publicationConfig(config), projectName, index));
|
|
105
|
+
}
|
|
106
|
+
function normalizeSchedule(entry, publication, projectName, index) {
|
|
107
|
+
if (!isObject(entry))
|
|
108
|
+
throw new Error(`Trigger.dev schedule ${index + 1} must be an object`);
|
|
109
|
+
const raw = entry;
|
|
110
|
+
const task = requiredString(raw.task, `Trigger.dev schedule ${index + 1} task`);
|
|
111
|
+
const cron = requiredString(raw.cron, `Trigger.dev schedule ${index + 1} cron`);
|
|
112
|
+
const timezone = optionalString(raw.timezone) ?? 'UTC';
|
|
113
|
+
const id = optionalString(raw.id) ?? task;
|
|
114
|
+
const deduplicationKey = optionalString(raw.deduplicationKey) ?? `kennel:${projectName}:${id}`;
|
|
115
|
+
const externalId = optionalString(raw.externalId);
|
|
116
|
+
const notifications = normalizeNotifications(raw.notifications, publication);
|
|
117
|
+
return {
|
|
118
|
+
cron,
|
|
119
|
+
deduplicationKey,
|
|
120
|
+
...(externalId ? { externalId } : {}),
|
|
121
|
+
id,
|
|
122
|
+
notifications,
|
|
123
|
+
task,
|
|
124
|
+
timezone,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function normalizeNotifications(value, publication) {
|
|
128
|
+
if (!isObject(value))
|
|
129
|
+
return [];
|
|
130
|
+
const notificationConfig = isObject(publication.notifications) ? publication.notifications : {};
|
|
131
|
+
const slackConfig = isObject(notificationConfig.slack) ? notificationConfig.slack : {};
|
|
132
|
+
const emailConfig = isObject(notificationConfig.email) ? notificationConfig.email : {};
|
|
133
|
+
const notifications = [];
|
|
134
|
+
if (isObject(value.slack)) {
|
|
135
|
+
const channelId = requiredString(value.slack.channelId, 'automation Slack notification channelId');
|
|
136
|
+
const tokenSecret = optionalString(value.slack.tokenSecret) ?? optionalString(slackConfig.tokenSecret);
|
|
137
|
+
notifications.push({
|
|
138
|
+
channelId,
|
|
139
|
+
...(tokenSecret ? { tokenSecret } : {}),
|
|
140
|
+
type: 'slack',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (isObject(value.email)) {
|
|
144
|
+
const group = optionalString(value.email.group) ?? optionalString(emailConfig.defaultGroup);
|
|
145
|
+
if (!group)
|
|
146
|
+
throw new Error('automation email notification group is required');
|
|
147
|
+
const endpointSecret = optionalString(value.email.endpointSecret) ?? optionalString(emailConfig.endpointSecret);
|
|
148
|
+
const provider = optionalString(value.email.provider) ?? optionalString(emailConfig.provider) ?? 'email';
|
|
149
|
+
const tokenSecret = optionalString(value.email.tokenSecret) ?? optionalString(emailConfig.tokenSecret);
|
|
150
|
+
notifications.push({
|
|
151
|
+
...(endpointSecret ? { endpointSecret } : {}),
|
|
152
|
+
group,
|
|
153
|
+
provider,
|
|
154
|
+
...(tokenSecret ? { tokenSecret } : {}),
|
|
155
|
+
type: 'email',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return notifications;
|
|
159
|
+
}
|
|
160
|
+
function writeAutomationConfig(root, schedules, options) {
|
|
161
|
+
if (!schedules.some((schedule) => schedule.notifications.length > 0))
|
|
162
|
+
return;
|
|
163
|
+
const outputPath = join(root, '.kennel', 'generated', 'automations', 'config.json');
|
|
164
|
+
console.log(`write ${relative(root, outputPath)}`);
|
|
165
|
+
if (options.dryRun)
|
|
166
|
+
return;
|
|
167
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
168
|
+
writeFileSync(outputPath, `${JSON.stringify({
|
|
169
|
+
schedules: schedules.map((schedule) => ({
|
|
170
|
+
cron: schedule.cron,
|
|
171
|
+
id: schedule.id,
|
|
172
|
+
notifications: schedule.notifications,
|
|
173
|
+
task: schedule.task,
|
|
174
|
+
timezone: schedule.timezone,
|
|
175
|
+
})),
|
|
176
|
+
}, null, 2)}\n`);
|
|
177
|
+
}
|
|
178
|
+
function logScheduleNotifications(schedules) {
|
|
179
|
+
for (const schedule of schedules) {
|
|
180
|
+
for (const notification of schedule.notifications) {
|
|
181
|
+
if (notification.type === 'slack') {
|
|
182
|
+
const token = notification.tokenSecret ? ` token ${notification.tokenSecret}` : '';
|
|
183
|
+
console.log(`automation notification ${schedule.task} slack ${notification.channelId}${token}`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const endpoint = notification.endpointSecret ? ` endpoint ${notification.endpointSecret}` : '';
|
|
187
|
+
const token = notification.tokenSecret ? ` token ${notification.tokenSecret}` : '';
|
|
188
|
+
console.log(`automation notification ${schedule.task} email ${notification.group} via ${notification.provider}${endpoint}${token}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function requiredString(value, label) {
|
|
194
|
+
const stringValue = optionalString(value);
|
|
195
|
+
if (!stringValue)
|
|
196
|
+
throw new Error(`${label} is required`);
|
|
197
|
+
return stringValue;
|
|
198
|
+
}
|
|
199
|
+
function optionalString(value) {
|
|
200
|
+
if (typeof value !== 'string')
|
|
201
|
+
return undefined;
|
|
202
|
+
const trimmed = value.trim();
|
|
203
|
+
return trimmed || undefined;
|
|
204
|
+
}
|
|
205
|
+
function triggerDevApiUrl(triggerDev) {
|
|
206
|
+
const configured = optionalString(triggerDev.apiUrl) ?? process.env.TRIGGER_API_URL ?? 'https://api.trigger.dev';
|
|
207
|
+
return configured.replace(/\/+$/u, '');
|
|
208
|
+
}
|
|
209
|
+
function runTriggerDevCommand(cwd, command, options) {
|
|
210
|
+
console.log(command);
|
|
211
|
+
if (options.dryRun)
|
|
212
|
+
return;
|
|
213
|
+
const result = spawnSync('sh', ['-c', command], {
|
|
214
|
+
cwd,
|
|
215
|
+
env: process.env,
|
|
216
|
+
stdio: 'inherit',
|
|
217
|
+
});
|
|
218
|
+
if (result.status !== 0)
|
|
219
|
+
process.exit(result.status ?? 1);
|
|
220
|
+
}
|
|
221
|
+
function createOrUpdateSchedule(apiUrl, secretKey, schedule) {
|
|
222
|
+
const body = JSON.stringify({
|
|
223
|
+
task: schedule.task,
|
|
224
|
+
cron: schedule.cron,
|
|
225
|
+
deduplicationKey: schedule.deduplicationKey,
|
|
226
|
+
...(schedule.externalId ? { externalId: schedule.externalId } : {}),
|
|
227
|
+
timezone: schedule.timezone,
|
|
228
|
+
});
|
|
229
|
+
const result = spawnSync('node', ['-e', `
|
|
230
|
+
const [apiUrl, secretKey, body] = process.argv.slice(1);
|
|
231
|
+
(async () => {
|
|
232
|
+
const response = await fetch(new URL('/api/v1/schedules', apiUrl), {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: \`Bearer \${secretKey}\`,
|
|
236
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
237
|
+
},
|
|
238
|
+
body,
|
|
239
|
+
});
|
|
240
|
+
const text = await response.text();
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
console.error(text || \`Trigger.dev schedule sync failed with status \${response.status}\`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
})().catch((error) => {
|
|
246
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|
|
249
|
+
`, apiUrl, secretKey, body], {
|
|
250
|
+
env: process.env,
|
|
251
|
+
stdio: 'inherit',
|
|
252
|
+
});
|
|
253
|
+
if (result.status !== 0)
|
|
254
|
+
process.exit(result.status ?? 1);
|
|
255
|
+
}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { installMcpConfig, resolveInside } from './config.js';
|
|
4
|
+
export function collectAgentTools(root, config) {
|
|
5
|
+
const directory = agentToolsDirectory(root, config);
|
|
6
|
+
if (!directory || !existsSync(directory))
|
|
7
|
+
return [];
|
|
8
|
+
const constantsByPath = new Map();
|
|
9
|
+
const tools = collectTypeScriptFiles(directory)
|
|
10
|
+
.flatMap((path) => {
|
|
11
|
+
const source = readFileSync(path, 'utf8');
|
|
12
|
+
const constants = stringConstants(source);
|
|
13
|
+
constantsByPath.set(path, constants);
|
|
14
|
+
return parseAgentTools(source, relative(root, path), constants);
|
|
15
|
+
})
|
|
16
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
return tools.filter((tool) => {
|
|
19
|
+
if (seen.has(tool.name))
|
|
20
|
+
return false;
|
|
21
|
+
seen.add(tool.name);
|
|
22
|
+
return true;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function agentToolsDirectory(root, config) {
|
|
26
|
+
const mcp = installMcpConfig(config);
|
|
27
|
+
const configured = mcp.agentTools ?? config.agentTools;
|
|
28
|
+
if (configured === false)
|
|
29
|
+
return null;
|
|
30
|
+
const directory = typeof configured === 'object' && configured && 'directory' in configured
|
|
31
|
+
? String(configured.directory ?? 'agents')
|
|
32
|
+
: 'agents';
|
|
33
|
+
return resolveInside(root, directory, 'mcp agent tools directory');
|
|
34
|
+
}
|
|
35
|
+
function collectTypeScriptFiles(directory) {
|
|
36
|
+
const files = [];
|
|
37
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
38
|
+
const path = join(directory, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
files.push(...collectTypeScriptFiles(path));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (entry.isFile() && /\.m?ts$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
44
|
+
files.push(path);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return files;
|
|
48
|
+
}
|
|
49
|
+
function parseAgentTools(source, agentPath, constants) {
|
|
50
|
+
const tools = [];
|
|
51
|
+
const defineToolPattern = /export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*defineTool\s*\(\s*\{/g;
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = defineToolPattern.exec(source)) !== null) {
|
|
54
|
+
const exportName = match[1] ?? '';
|
|
55
|
+
const objectStart = match.index + match[0].length - 1;
|
|
56
|
+
const objectEnd = findMatching(source, objectStart, '{', '}');
|
|
57
|
+
if (objectEnd === -1)
|
|
58
|
+
continue;
|
|
59
|
+
const objectSource = source.slice(objectStart, objectEnd + 1);
|
|
60
|
+
const name = extractStringProperty(objectSource, 'name');
|
|
61
|
+
if (!name)
|
|
62
|
+
continue;
|
|
63
|
+
tools.push({
|
|
64
|
+
agentPath,
|
|
65
|
+
description: extractStringProperty(objectSource, 'description')
|
|
66
|
+
?? `Agent tool exported from ${agentPath}.`,
|
|
67
|
+
exportName,
|
|
68
|
+
inputSchema: parseParametersSchema(objectSource, constants),
|
|
69
|
+
name,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return tools;
|
|
73
|
+
}
|
|
74
|
+
function parseParametersSchema(objectSource, constants) {
|
|
75
|
+
const parameters = extractObjectProperty(objectSource, 'parameters');
|
|
76
|
+
if (!parameters) {
|
|
77
|
+
return {
|
|
78
|
+
additionalProperties: true,
|
|
79
|
+
properties: {},
|
|
80
|
+
type: 'object',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const schema = {
|
|
84
|
+
additionalProperties: extractBooleanProperty(parameters, 'additionalProperties') ?? true,
|
|
85
|
+
properties: parseSchemaProperties(parameters, constants),
|
|
86
|
+
type: 'object',
|
|
87
|
+
};
|
|
88
|
+
const required = extractStringArrayProperty(parameters, 'required');
|
|
89
|
+
if (required.length)
|
|
90
|
+
schema.required = required;
|
|
91
|
+
return schema;
|
|
92
|
+
}
|
|
93
|
+
function parseSchemaProperties(parameters, constants) {
|
|
94
|
+
const propertiesSource = extractObjectProperty(parameters, 'properties');
|
|
95
|
+
if (!propertiesSource)
|
|
96
|
+
return {};
|
|
97
|
+
const properties = {};
|
|
98
|
+
let index = 1;
|
|
99
|
+
while (index < propertiesSource.length - 1) {
|
|
100
|
+
const match = /(?:^|,)\s*([A-Za-z_$][\w$]*|"[^"]+"|'[^']+')\s*:/.exec(propertiesSource.slice(index));
|
|
101
|
+
if (!match || typeof match.index !== 'number')
|
|
102
|
+
break;
|
|
103
|
+
const name = normalisePropertyName(match[1] ?? '');
|
|
104
|
+
const colon = index + match.index + match[0].length - 1;
|
|
105
|
+
const valueStart = nextNonWhitespace(propertiesSource, colon + 1);
|
|
106
|
+
if (valueStart === -1)
|
|
107
|
+
break;
|
|
108
|
+
if (propertiesSource[valueStart] !== '{') {
|
|
109
|
+
index = valueStart + 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const valueEnd = findMatching(propertiesSource, valueStart, '{', '}');
|
|
113
|
+
if (valueEnd === -1)
|
|
114
|
+
break;
|
|
115
|
+
properties[name] = parseSchemaProperty(propertiesSource.slice(valueStart, valueEnd + 1), constants);
|
|
116
|
+
index = valueEnd + 1;
|
|
117
|
+
}
|
|
118
|
+
return properties;
|
|
119
|
+
}
|
|
120
|
+
function parseSchemaProperty(source, constants) {
|
|
121
|
+
const property = {};
|
|
122
|
+
const type = extractStringProperty(source, 'type');
|
|
123
|
+
const description = extractStringProperty(source, 'description');
|
|
124
|
+
const pattern = extractStringOrConstantProperty(source, 'pattern', constants);
|
|
125
|
+
const defaultValue = extractPrimitiveProperty(source, 'default', constants);
|
|
126
|
+
if (type)
|
|
127
|
+
property.type = type;
|
|
128
|
+
if (description)
|
|
129
|
+
property.description = description;
|
|
130
|
+
if (pattern)
|
|
131
|
+
property.pattern = pattern;
|
|
132
|
+
if (defaultValue !== undefined)
|
|
133
|
+
property.default = defaultValue;
|
|
134
|
+
return property;
|
|
135
|
+
}
|
|
136
|
+
function extractObjectProperty(source, property) {
|
|
137
|
+
const propertyIndex = source.search(new RegExp(`\\b${escapeRegExp(property)}\\s*:`));
|
|
138
|
+
if (propertyIndex === -1)
|
|
139
|
+
return null;
|
|
140
|
+
const braceStart = source.indexOf('{', propertyIndex);
|
|
141
|
+
if (braceStart === -1)
|
|
142
|
+
return null;
|
|
143
|
+
const braceEnd = findMatching(source, braceStart, '{', '}');
|
|
144
|
+
return braceEnd === -1 ? null : source.slice(braceStart, braceEnd + 1);
|
|
145
|
+
}
|
|
146
|
+
function extractStringProperty(source, property) {
|
|
147
|
+
const match = new RegExp(`\\b${escapeRegExp(property)}\\s*:\\s*(["'\`])([\\s\\S]*?)\\1`).exec(source);
|
|
148
|
+
return match ? decodeStringLiteral(match[2] ?? '') : null;
|
|
149
|
+
}
|
|
150
|
+
function extractStringOrConstantProperty(source, property, constants) {
|
|
151
|
+
const stringValue = extractStringProperty(source, property);
|
|
152
|
+
if (stringValue)
|
|
153
|
+
return stringValue;
|
|
154
|
+
const match = new RegExp(`\\b${escapeRegExp(property)}\\s*:\\s*([A-Za-z_$][\\w$]*)`).exec(source);
|
|
155
|
+
return match?.[1] ? constants[match[1]] ?? null : null;
|
|
156
|
+
}
|
|
157
|
+
function extractBooleanProperty(source, property) {
|
|
158
|
+
const match = new RegExp(`\\b${escapeRegExp(property)}\\s*:\\s*(true|false)\\b`).exec(source);
|
|
159
|
+
return match ? match[1] === 'true' : null;
|
|
160
|
+
}
|
|
161
|
+
function extractStringArrayProperty(source, property) {
|
|
162
|
+
const match = new RegExp(`\\b${escapeRegExp(property)}\\s*:\\s*\\[([\\s\\S]*?)\\]`).exec(source);
|
|
163
|
+
if (!match?.[1])
|
|
164
|
+
return [];
|
|
165
|
+
return Array.from(match[1].matchAll(/(["'])([\s\S]*?)\1/g)).map((entry) => decodeStringLiteral(entry[2] ?? ''));
|
|
166
|
+
}
|
|
167
|
+
function extractPrimitiveProperty(source, property, constants) {
|
|
168
|
+
const stringValue = extractStringOrConstantProperty(source, property, constants);
|
|
169
|
+
if (stringValue !== null)
|
|
170
|
+
return stringValue;
|
|
171
|
+
const match = new RegExp(`\\b${escapeRegExp(property)}\\s*:\\s*(true|false|-?\\d+(?:\\.\\d+)?)\\b`).exec(source);
|
|
172
|
+
if (!match?.[1])
|
|
173
|
+
return undefined;
|
|
174
|
+
if (match[1] === 'true')
|
|
175
|
+
return true;
|
|
176
|
+
if (match[1] === 'false')
|
|
177
|
+
return false;
|
|
178
|
+
return Number(match[1]);
|
|
179
|
+
}
|
|
180
|
+
function stringConstants(source) {
|
|
181
|
+
const constants = {};
|
|
182
|
+
for (const match of source.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*(["'\`])([\s\S]*?)\2/g)) {
|
|
183
|
+
const name = match[1];
|
|
184
|
+
const value = match[3];
|
|
185
|
+
if (name && value !== undefined)
|
|
186
|
+
constants[name] = decodeStringLiteral(value);
|
|
187
|
+
}
|
|
188
|
+
return constants;
|
|
189
|
+
}
|
|
190
|
+
function findMatching(source, start, open, close) {
|
|
191
|
+
let depth = 0;
|
|
192
|
+
let quote = null;
|
|
193
|
+
let escaped = false;
|
|
194
|
+
for (let index = start; index < source.length; index += 1) {
|
|
195
|
+
const char = source[index];
|
|
196
|
+
if (quote) {
|
|
197
|
+
if (escaped) {
|
|
198
|
+
escaped = false;
|
|
199
|
+
}
|
|
200
|
+
else if (char === '\\') {
|
|
201
|
+
escaped = true;
|
|
202
|
+
}
|
|
203
|
+
else if (char === quote) {
|
|
204
|
+
quote = null;
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
209
|
+
quote = char;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (char === open)
|
|
213
|
+
depth += 1;
|
|
214
|
+
if (char === close) {
|
|
215
|
+
depth -= 1;
|
|
216
|
+
if (depth === 0)
|
|
217
|
+
return index;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return -1;
|
|
221
|
+
}
|
|
222
|
+
function nextNonWhitespace(source, start) {
|
|
223
|
+
for (let index = start; index < source.length; index += 1) {
|
|
224
|
+
if (!/\s/.test(source[index] ?? ''))
|
|
225
|
+
return index;
|
|
226
|
+
}
|
|
227
|
+
return -1;
|
|
228
|
+
}
|
|
229
|
+
function normalisePropertyName(value) {
|
|
230
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
231
|
+
return decodeStringLiteral(value.slice(1, -1));
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
function decodeStringLiteral(value) {
|
|
236
|
+
return value
|
|
237
|
+
.replace(/\\n/g, '\n')
|
|
238
|
+
.replace(/\\r/g, '\r')
|
|
239
|
+
.replace(/\\t/g, '\t')
|
|
240
|
+
.replace(/\\`/g, '`')
|
|
241
|
+
.replace(/\\"/g, '"')
|
|
242
|
+
.replace(/\\'/g, "'")
|
|
243
|
+
.replace(/\\\\/g, '\\');
|
|
244
|
+
}
|
|
245
|
+
function escapeRegExp(value) {
|
|
246
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
247
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type McpGenerateOptions = {
|
|
2
|
+
configFile: string;
|
|
3
|
+
dataOnly: boolean;
|
|
4
|
+
functionOnly: boolean;
|
|
5
|
+
targetPath: string;
|
|
6
|
+
};
|
|
7
|
+
export type McpCustomToolConfig = {
|
|
8
|
+
exportName: string;
|
|
9
|
+
importPath: string;
|
|
10
|
+
};
|
|
11
|
+
export type McpToolModuleConfig = {
|
|
12
|
+
exportName: string;
|
|
13
|
+
importPath: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function splitMcpGenerateArgs(input: string[]): McpGenerateOptions;
|
|
16
|
+
export declare function installMcpConfig(config: any): Record<string, any>;
|
|
17
|
+
export declare function coreMcpConfig(config: any): Record<string, any>;
|
|
18
|
+
export declare function mcpDataConfig(config: any): Record<string, any>;
|
|
19
|
+
export declare function customTools(config: any): McpCustomToolConfig[];
|
|
20
|
+
export declare function toolModules(config: any): McpToolModuleConfig[];
|
|
21
|
+
export declare function agentToolsDirectory(config: any): string | null;
|
|
22
|
+
export declare function resolveInside(root: string, filePath: string, label: string): string;
|
|
23
|
+
export declare function outputPath(root: string, config: any): string;
|
|
24
|
+
export declare function functionPath(root: string, config: any): string;
|
|
25
|
+
export declare function serverConfig(config: any): Record<string, unknown>;
|