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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/bin/kennel.mjs +3 -0
  4. package/dist/automations/notifications.d.ts +40 -0
  5. package/dist/automations/notifications.js +220 -0
  6. package/dist/cli/dispatch.d.ts +1 -0
  7. package/dist/cli/dispatch.js +51 -0
  8. package/dist/cli/usage.d.ts +1 -0
  9. package/dist/cli/usage.js +14 -0
  10. package/dist/deploy/cloudflare.d.ts +6 -0
  11. package/dist/deploy/cloudflare.js +91 -0
  12. package/dist/deploy/environment.d.ts +2 -0
  13. package/dist/deploy/environment.js +47 -0
  14. package/dist/deploy/smoke-tests.d.ts +3 -0
  15. package/dist/deploy/smoke-tests.js +71 -0
  16. package/dist/deploy/trigger-dev.d.ts +2 -0
  17. package/dist/deploy/trigger-dev.js +255 -0
  18. package/dist/main.d.ts +1 -0
  19. package/dist/main.js +8 -0
  20. package/dist/mcp/agent-tools.d.ts +8 -0
  21. package/dist/mcp/agent-tools.js +247 -0
  22. package/dist/mcp/config.d.ts +25 -0
  23. package/dist/mcp/config.js +171 -0
  24. package/dist/mcp/data.d.ts +14 -0
  25. package/dist/mcp/data.js +120 -0
  26. package/dist/mcp/function-source.d.ts +7 -0
  27. package/dist/mcp/function-source.js +40 -0
  28. package/dist/mcp/generate.d.ts +1 -0
  29. package/dist/mcp/generate.js +47 -0
  30. package/dist/mcp/install-page.d.ts +1 -0
  31. package/dist/mcp/install-page.js +360 -0
  32. package/dist/notifications/email-worker.d.ts +8 -0
  33. package/dist/notifications/email-worker.js +137 -0
  34. package/dist/package/mcp-data.d.ts +1 -0
  35. package/dist/package/mcp-data.js +1 -0
  36. package/dist/package/mcp-email-worker.d.ts +1 -0
  37. package/dist/package/mcp-email-worker.js +1 -0
  38. package/dist/package/mcp-http.d.ts +1 -0
  39. package/dist/package/mcp-http.js +1 -0
  40. package/dist/package/mcp-stdio.d.ts +1 -0
  41. package/dist/package/mcp-stdio.js +1 -0
  42. package/dist/package/mcp.d.ts +1 -0
  43. package/dist/package/mcp.js +1 -0
  44. package/dist/publish/cloudflare-init.d.ts +12 -0
  45. package/dist/publish/cloudflare-init.js +98 -0
  46. package/dist/publish/config.d.ts +28 -0
  47. package/dist/publish/config.js +190 -0
  48. package/dist/publish/options.d.ts +13 -0
  49. package/dist/publish/options.js +78 -0
  50. package/dist/schema/doctor.d.ts +1 -0
  51. package/dist/schema/doctor.js +69 -0
  52. package/dist/schema/documents.d.ts +18 -0
  53. package/dist/schema/documents.js +127 -0
  54. package/dist/schema/pointers.d.ts +5 -0
  55. package/dist/schema/pointers.js +48 -0
  56. package/dist/schema/types.d.ts +36 -0
  57. package/dist/schema/types.js +1 -0
  58. package/dist/schema/validation.d.ts +8 -0
  59. package/dist/schema/validation.js +212 -0
  60. package/dist/shared/json.d.ts +4 -0
  61. package/dist/shared/json.js +56 -0
  62. package/dist/shared/object.d.ts +2 -0
  63. package/dist/shared/object.js +3 -0
  64. package/dist/shared/package-manager.d.ts +11 -0
  65. package/dist/shared/package-manager.js +39 -0
  66. package/dist/shared/path.d.ts +4 -0
  67. package/dist/shared/path.js +40 -0
  68. package/dist/shared/process.d.ts +7 -0
  69. package/dist/shared/process.js +12 -0
  70. package/dist/shared/project-config.d.ts +2 -0
  71. package/dist/shared/project-config.js +26 -0
  72. package/dist/site/dev.d.ts +10 -0
  73. package/dist/site/dev.js +128 -0
  74. package/dist/site/vitepress.d.ts +21 -0
  75. package/dist/site/vitepress.js +66 -0
  76. package/package.json +74 -0
  77. package/templates/cloudflare/email-code-worker.js +344 -0
@@ -0,0 +1,171 @@
1
+ import { resolve } from 'node:path';
2
+ import { isObject } from '../shared/object.js';
3
+ import { DEFAULT_PROJECT_CONFIG } from '../shared/project-config.js';
4
+ export function splitMcpGenerateArgs(input) {
5
+ const options = {
6
+ configFile: DEFAULT_PROJECT_CONFIG,
7
+ dataOnly: false,
8
+ functionOnly: false,
9
+ targetPath: '.',
10
+ };
11
+ for (let index = 0; index < input.length; index += 1) {
12
+ const arg = input[index];
13
+ if (arg === '--config') {
14
+ options.configFile = input[index + 1] ?? '';
15
+ index += 1;
16
+ continue;
17
+ }
18
+ if (arg.startsWith('--config=')) {
19
+ options.configFile = arg.slice('--config='.length);
20
+ continue;
21
+ }
22
+ if (arg === '--data-only') {
23
+ options.dataOnly = true;
24
+ continue;
25
+ }
26
+ if (arg === '--function-only') {
27
+ options.functionOnly = true;
28
+ continue;
29
+ }
30
+ if (arg.startsWith('-'))
31
+ throw new Error(`Unknown mcp generate option: ${arg}`);
32
+ options.targetPath = arg;
33
+ }
34
+ if (!options.configFile)
35
+ throw new Error('mcp generate requires a config file name');
36
+ if (options.dataOnly && options.functionOnly)
37
+ throw new Error('Use only one of --data-only or --function-only');
38
+ return options;
39
+ }
40
+ export function installMcpConfig(config) {
41
+ if (isObject(config.install?.mcp))
42
+ return config.install.mcp;
43
+ return {};
44
+ }
45
+ export function coreMcpConfig(config) {
46
+ return isObject(config.mcp) ? config.mcp : {};
47
+ }
48
+ function runtimeMcpConfig(config) {
49
+ return {
50
+ ...installMcpConfig(config),
51
+ ...coreMcpConfig(config),
52
+ };
53
+ }
54
+ export function mcpDataConfig(config) {
55
+ const coreMcp = coreMcpConfig(config);
56
+ const installMcp = installMcpConfig(config);
57
+ if (isObject(coreMcp.data))
58
+ return coreMcp.data;
59
+ if (isObject(installMcp.data))
60
+ return installMcp.data;
61
+ if (isObject(config.mcpData))
62
+ return config.mcpData;
63
+ return {};
64
+ }
65
+ export function customTools(config) {
66
+ const mcp = runtimeMcpConfig(config);
67
+ if (!Array.isArray(mcp.customTools))
68
+ return [];
69
+ return mcp.customTools.map((entry, index) => {
70
+ if (!isObject(entry))
71
+ throw new Error(`mcp customTools[${index}] must be an object`);
72
+ const exportName = String(entry.exportName ?? '').trim();
73
+ const importPath = String(entry.importPath ?? '').trim();
74
+ if (!/^[A-Za-z_$][\w$]*$/.test(exportName)) {
75
+ throw new Error(`mcp customTools[${index}].exportName must be a valid import identifier`);
76
+ }
77
+ if (!importPath)
78
+ throw new Error(`mcp customTools[${index}].importPath is required`);
79
+ return { exportName, importPath };
80
+ });
81
+ }
82
+ export function toolModules(config) {
83
+ const mcp = runtimeMcpConfig(config);
84
+ if (!Array.isArray(mcp.toolModules))
85
+ return [];
86
+ return mcp.toolModules.map((entry, index) => {
87
+ if (!isObject(entry))
88
+ throw new Error(`mcp toolModules[${index}] must be an object`);
89
+ const exportName = String(entry.exportName ?? '').trim();
90
+ const importPath = String(entry.importPath ?? '').trim();
91
+ if (!/^[A-Za-z_$][\w$]*$/.test(exportName)) {
92
+ throw new Error(`mcp toolModules[${index}].exportName must be a valid import identifier`);
93
+ }
94
+ if (!importPath)
95
+ throw new Error(`mcp toolModules[${index}].importPath is required`);
96
+ return { exportName, importPath };
97
+ });
98
+ }
99
+ export function agentToolsDirectory(config) {
100
+ const mcp = runtimeMcpConfig(config);
101
+ const configured = mcp.agentTools ?? config.agentTools;
102
+ if (configured === false)
103
+ return null;
104
+ if (isObject(configured) && 'directory' in configured) {
105
+ return String(configured.directory ?? 'agents');
106
+ }
107
+ return 'agents';
108
+ }
109
+ export function resolveInside(root, filePath, label) {
110
+ const resolvedRoot = resolve(root);
111
+ const target = resolve(resolvedRoot, filePath);
112
+ if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}/`)) {
113
+ throw new Error(`${label} must resolve inside project root: ${filePath}`);
114
+ }
115
+ return target;
116
+ }
117
+ export function outputPath(root, config) {
118
+ const data = mcpDataConfig(config);
119
+ const configured = typeof data.outputPath === 'string' && data.outputPath.trim()
120
+ ? data.outputPath.trim()
121
+ : 'mcp/_generated/mcp-data.json';
122
+ return resolveInside(root, configured, 'mcp data output path');
123
+ }
124
+ export function functionPath(root, config) {
125
+ const mcp = runtimeMcpConfig(config);
126
+ const configured = typeof mcp.functionPath === 'string' && mcp.functionPath.trim()
127
+ ? mcp.functionPath.trim()
128
+ : 'mcp/index.ts';
129
+ return resolveInside(root, configured, 'mcp function path');
130
+ }
131
+ function toolAliases(config) {
132
+ const mcp = runtimeMcpConfig(config);
133
+ if (!isObject(mcp.toolAliases))
134
+ return {};
135
+ return Object.fromEntries(Object.entries(mcp.toolAliases).map(([alias, target]) => [alias, String(target)]));
136
+ }
137
+ function repoIntegrations(config) {
138
+ const mcp = runtimeMcpConfig(config);
139
+ if (!isObject(mcp.repoIntegrations))
140
+ return undefined;
141
+ return mcp.repoIntegrations;
142
+ }
143
+ function access(config) {
144
+ const mcp = runtimeMcpConfig(config);
145
+ if (!isObject(mcp.access))
146
+ return undefined;
147
+ return mcp.access;
148
+ }
149
+ function toolPacks(config) {
150
+ const mcp = runtimeMcpConfig(config);
151
+ if (!isObject(mcp.toolPacks))
152
+ return undefined;
153
+ return mcp.toolPacks;
154
+ }
155
+ export function serverConfig(config) {
156
+ const mcp = runtimeMcpConfig(config);
157
+ const serverName = String(mcp.serverName ?? config.name ?? 'index');
158
+ return {
159
+ ...(access(config) ? { access: access(config) } : {}),
160
+ ...(isObject(mcp.analytics) ? { analytics: mcp.analytics } : {}),
161
+ ...(isObject(mcp.conversationSnapshots) ? { conversationSnapshots: mcp.conversationSnapshots } : {}),
162
+ instructions: typeof mcp.instructions === 'string' && mcp.instructions.trim()
163
+ ? mcp.instructions.trim()
164
+ : "Find relevant Index documents and repo entries for the user's topic. Use search_index_docs and search_index_repos for topic lookup, then read index://doc/{slug} or index://repo/{name} when more detail is needed.",
165
+ name: mcp.runtimeName ?? `${serverName}-mcp`,
166
+ ...(repoIntegrations(config) ? { repoIntegrations: repoIntegrations(config) } : {}),
167
+ toolAliases: toolAliases(config),
168
+ ...(toolPacks(config) ? { toolPacks: toolPacks(config) } : {}),
169
+ version: mcp.version ?? '1.0.0',
170
+ };
171
+ }
@@ -0,0 +1,14 @@
1
+ import type { AgentToolEntry } from './agent-tools.js';
2
+ export type DocEntry = {
3
+ content: string;
4
+ modified: number;
5
+ path: string;
6
+ size: number;
7
+ slug: string;
8
+ summary: string;
9
+ title: string;
10
+ };
11
+ export type RepoEntry = Record<string, unknown>;
12
+ export declare function collectDocs(root: string, config: any): DocEntry[];
13
+ export declare function parseRepoRegistry(root: string, config: any): RepoEntry[];
14
+ export declare function writeMcpData(target: string, docs: DocEntry[], repos: RepoEntry[], agentTools: AgentToolEntry[]): void;
@@ -0,0 +1,120 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { readJson } from '../shared/json.js';
5
+ import { isObject } from '../shared/object.js';
6
+ import { mcpDataConfig, resolveInside } from './config.js';
7
+ const defaultIgnoredDirs = [
8
+ '.agents',
9
+ '.git',
10
+ '.github',
11
+ '.gtl',
12
+ '.kennel',
13
+ '.vitepress',
14
+ 'email-worker',
15
+ 'functions',
16
+ 'mcp',
17
+ 'node_modules',
18
+ 'public',
19
+ 'scripts',
20
+ 'src',
21
+ ];
22
+ export function collectDocs(root, config) {
23
+ return collectMarkdown(root, root, ignoredDirs(config))
24
+ .map((path) => parseDoc(root, path))
25
+ .sort((left, right) => left.title.localeCompare(right.title));
26
+ }
27
+ function ignoredDirs(config) {
28
+ const data = mcpDataConfig(config);
29
+ return new Set([
30
+ ...defaultIgnoredDirs,
31
+ ...(Array.isArray(data.ignoredDirs) ? data.ignoredDirs.map(String) : []),
32
+ ]);
33
+ }
34
+ function collectMarkdown(root, dir, ignored) {
35
+ const entries = readdirSync(dir, { withFileTypes: true });
36
+ const files = [];
37
+ for (const entry of entries) {
38
+ const path = join(dir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ if (!ignored.has(entry.name)) {
41
+ files.push(...collectMarkdown(root, path, ignored));
42
+ }
43
+ continue;
44
+ }
45
+ if (entry.isFile() && entry.name.endsWith('.md'))
46
+ files.push(path);
47
+ }
48
+ return files;
49
+ }
50
+ function parseDoc(root, path) {
51
+ const content = readFileSync(path, 'utf8');
52
+ const relativePath = relative(root, path);
53
+ const slug = relativePath.replace(/(^|\/)index\.md$/, '$1index').replace(/\.md$/, '').replace(/\//g, '--');
54
+ const stat = statSync(path);
55
+ return {
56
+ content,
57
+ modified: Math.floor(stat.mtimeMs / 1000),
58
+ path: relativePath,
59
+ size: stat.size,
60
+ slug,
61
+ summary: summariseMarkdown(content),
62
+ title: extractTitle(content, relativePath),
63
+ };
64
+ }
65
+ function extractTitle(content, path) {
66
+ const titleMatch = content.match(/^title:\s*["']?(.+?)["']?\s*$/m);
67
+ if (titleMatch?.[1])
68
+ return titleMatch[1].trim();
69
+ const headingMatch = content.match(/^#\s+(.+?)\s*$/m);
70
+ if (headingMatch?.[1])
71
+ return headingMatch[1].trim();
72
+ return path.replace(/\.md$/, '').split('/').pop()?.replace(/-/g, ' ') ?? path;
73
+ }
74
+ function summariseMarkdown(content) {
75
+ const body = content.replace(/^---[\s\S]*?---\s*/, '');
76
+ for (const block of body.split(/\n\s*\n/)) {
77
+ const cleaned = block.trim();
78
+ if (!cleaned || cleaned.startsWith('#') || cleaned.startsWith('|'))
79
+ continue;
80
+ const singleLine = cleaned.replace(/\s+/g, ' ');
81
+ return singleLine.length > 280 ? `${singleLine.slice(0, 280)}...` : singleLine;
82
+ }
83
+ return '';
84
+ }
85
+ function repoRegistryPath(root, config) {
86
+ const data = mcpDataConfig(config);
87
+ const configured = typeof data.repoRegistryPath === 'string' ? data.repoRegistryPath : 'repos/index.yaml';
88
+ if (!configured)
89
+ return null;
90
+ const path = resolveInside(root, configured, 'mcp repo registry path');
91
+ return existsSync(path) ? path : null;
92
+ }
93
+ export function parseRepoRegistry(root, config) {
94
+ const path = repoRegistryPath(root, config);
95
+ if (!path)
96
+ return [];
97
+ const parsed = parseYaml(readFileSync(path, 'utf8'));
98
+ const repos = Array.isArray(parsed?.repos) ? parsed.repos : [];
99
+ return repos
100
+ .filter(isObject)
101
+ .map((repo) => ({ ...repo }))
102
+ .sort((left, right) => String(left.name ?? '').localeCompare(String(right.name ?? '')));
103
+ }
104
+ function sameMcpData(existing, docs, repos, agentTools) {
105
+ if (!isObject(existing))
106
+ return false;
107
+ return JSON.stringify(existing.docs) === JSON.stringify(docs)
108
+ && JSON.stringify(existing.repos) === JSON.stringify(repos)
109
+ && JSON.stringify(existing.agentTools ?? []) === JSON.stringify(agentTools);
110
+ }
111
+ export function writeMcpData(target, docs, repos, agentTools) {
112
+ const existing = existsSync(target) ? readJson(target) : null;
113
+ const generatedAt = sameMcpData(existing, docs, repos, agentTools) && typeof existing.generatedAt === 'string'
114
+ ? existing.generatedAt
115
+ : new Date().toISOString();
116
+ const next = `${JSON.stringify({ generatedAt, docs, repos, agentTools }, null, 2)}\n`;
117
+ if (existsSync(target) && readFileSync(target, 'utf8') === next)
118
+ return;
119
+ writeFileSync(target, next);
120
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpCustomToolConfig, McpToolModuleConfig } from './config.js';
2
+ export type McpFunctionSourceOptions = {
3
+ customTools?: McpCustomToolConfig[];
4
+ serverConfig: Record<string, unknown>;
5
+ toolModules?: McpToolModuleConfig[];
6
+ };
7
+ export declare function mcpFunctionSource(options: McpFunctionSourceOptions): string;
@@ -0,0 +1,40 @@
1
+ export function mcpFunctionSource(options) {
2
+ const customTools = options.customTools ?? [];
3
+ const toolModules = options.toolModules ?? [];
4
+ const customToolImports = customTools
5
+ .map((tool) => `import { ${tool.exportName} } from ${JSON.stringify(tool.importPath)};`)
6
+ .join('\n');
7
+ const toolModuleImports = toolModules
8
+ .map((toolModule) => `import { ${toolModule.exportName} } from ${JSON.stringify(toolModule.importPath)};`)
9
+ .join('\n');
10
+ const customToolNames = customTools.map((tool) => tool.exportName);
11
+ const toolModuleNames = toolModules.map((toolModule) => toolModule.exportName);
12
+ const customToolsDeclaration = customToolNames.length || toolModuleNames.length
13
+ ? `\nconst customTools = [${[
14
+ ...customToolNames,
15
+ ...toolModuleNames.map((name) => `...${name}`),
16
+ ].join(', ')}];\n`
17
+ : '';
18
+ const customToolOption = customToolNames.length || toolModuleNames.length ? ', customTools' : '';
19
+ return `import { createMcpHttpHandler } from "@raggle-ai/kennel-mcp";
20
+ import data from "./_generated/mcp-data.json";
21
+ ${customToolImports ? `${customToolImports}\n` : ''}${toolModuleImports ? `${toolModuleImports}\n` : ''}
22
+
23
+ const serverConfig = ${JSON.stringify(options.serverConfig, null, 2)} as {
24
+ access?: Record<string, unknown>;
25
+ analytics?: Record<string, unknown>;
26
+ conversationSnapshots?: Record<string, unknown>;
27
+ instructions: string;
28
+ name: string;
29
+ toolAliases: Record<string, string>;
30
+ toolPacks?: Record<string, unknown>;
31
+ version: string;
32
+ };
33
+ ${customToolsDeclaration}
34
+
35
+ const handleMcpRequest = createMcpHttpHandler({ data, serverConfig${customToolOption} });
36
+
37
+ export const onRequest = async ({ env, request }: { env?: Record<string, unknown>; request: Request }) =>
38
+ handleMcpRequest(request, env);
39
+ `;
40
+ }
@@ -0,0 +1 @@
1
+ export declare function generateMcp(input: string[]): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, relative } from 'node:path';
3
+ import { collectAgentTools, collectMcpData, writeMcpData, } from '@raggle-ai/kennel-mcp/data';
4
+ import { readJson } from '../shared/json.js';
5
+ import { writeNotificationEmailWorker } from '../notifications/email-worker.js';
6
+ import { projectConfigPath } from '../shared/project-config.js';
7
+ import { agentToolsDirectory, customTools, functionPath, mcpDataConfig, outputPath, serverConfig, splitMcpGenerateArgs, toolModules, } from './config.js';
8
+ import { mcpFunctionSource } from './function-source.js';
9
+ import { writeInstallPage } from './install-page.js';
10
+ export async function generateMcp(input) {
11
+ const options = splitMcpGenerateArgs(input);
12
+ const configPath = projectConfigPath(options.targetPath, options.configFile);
13
+ const root = dirname(configPath);
14
+ const config = readJson(configPath);
15
+ if (!options.functionOnly) {
16
+ const installPage = writeInstallPage(root, config);
17
+ if (installPage)
18
+ console.log(`ok ${relative(root, installPage)}`);
19
+ const dataConfig = {
20
+ ...(Array.isArray(config.srcExclude) ? { srcExclude: config.srcExclude } : {}),
21
+ ...mcpDataConfig(config),
22
+ };
23
+ const agentDirectory = agentToolsDirectory(config);
24
+ const agentTools = agentDirectory ? collectAgentTools(root, agentDirectory) : [];
25
+ const { docs, health, repos } = collectMcpData(root, dataConfig);
26
+ const target = outputPath(root, config);
27
+ mkdirSync(dirname(target), { recursive: true });
28
+ writeMcpData(target, docs, repos, agentTools, health);
29
+ console.log(`ok ${relative(root, target)} (${docs.length} docs, ${repos.length} repos, ${agentTools.length} agent tools)`);
30
+ }
31
+ if (!options.dataOnly) {
32
+ const target = functionPath(root, config);
33
+ mkdirSync(dirname(target), { recursive: true });
34
+ writeFileSync(target, mcpFunctionSource({
35
+ customTools: customTools(config),
36
+ serverConfig: serverConfig(config),
37
+ toolModules: toolModules(config),
38
+ }));
39
+ console.log(`ok ${relative(root, target)}`);
40
+ const emailWorker = writeNotificationEmailWorker(root, config, configPath);
41
+ if (emailWorker) {
42
+ console.log(`ok ${relative(root, emailWorker.workerPath)}`);
43
+ console.log(`ok ${relative(root, emailWorker.testPath)}`);
44
+ console.log(`ok ${relative(root, emailWorker.wranglerPath)}`);
45
+ }
46
+ }
47
+ }
@@ -0,0 +1 @@
1
+ export declare function writeInstallPage(root: string, config: any): string | undefined;