@corsenai/corsen-context-cli 1.2.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 +19 -0
- package/dist/index.js +706 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corsen AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @corsenai/corsen-context-cli
|
|
2
|
+
|
|
3
|
+
Command-line tools for **[Corsen Context](https://github.com/CorsenAI/corsen-context)** — scaffold your integration, generate `llms.txt` from a live site, and check if your site is AI-ready.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# Detect your framework and scaffold config + route files
|
|
7
|
+
npx @corsenai/corsen-context-cli init
|
|
8
|
+
|
|
9
|
+
# Generate llms.txt (and llms-full.txt) from a live site
|
|
10
|
+
npx @corsenai/corsen-context-cli generate --url https://example.com --full
|
|
11
|
+
|
|
12
|
+
# Check whether a site is AI-ready
|
|
13
|
+
npx @corsenai/corsen-context-cli doctor --url https://example.com
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The `doctor` command checks HTTPS, `llms.txt`, `sitemap.xml`, the robots.txt MCP reference, and MCP endpoint availability.
|
|
17
|
+
|
|
18
|
+
- **Full docs:** https://github.com/CorsenAI/corsen-context#readme
|
|
19
|
+
- **License:** MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/init.ts
|
|
5
|
+
var import_node_fs = require("fs");
|
|
6
|
+
var import_node_path = require("path");
|
|
7
|
+
function detectFramework(cwd) {
|
|
8
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "wp-config.php")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "wp-content"))) {
|
|
9
|
+
return { framework: "wordpress", label: "WordPress" };
|
|
10
|
+
}
|
|
11
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "hugo.toml")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "hugo.yaml")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "config.toml"))) {
|
|
12
|
+
return { framework: "hugo", label: "Hugo" };
|
|
13
|
+
}
|
|
14
|
+
const pkgPath = (0, import_node_path.join)(cwd, "package.json");
|
|
15
|
+
if ((0, import_node_fs.existsSync)(pkgPath)) {
|
|
16
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8"));
|
|
17
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
18
|
+
if (allDeps["next"]) {
|
|
19
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "app")) || (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "src", "app"))) {
|
|
20
|
+
return { framework: "nextjs-app", label: "Next.js (App Router)" };
|
|
21
|
+
}
|
|
22
|
+
return { framework: "nextjs-pages", label: "Next.js (Pages Router)" };
|
|
23
|
+
}
|
|
24
|
+
if (allDeps["astro"]) return { framework: "astro", label: "Astro" };
|
|
25
|
+
if (allDeps["express"]) return { framework: "express", label: "Express" };
|
|
26
|
+
}
|
|
27
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "index.html"))) {
|
|
28
|
+
return { framework: "static", label: "Static Site" };
|
|
29
|
+
}
|
|
30
|
+
return { framework: "unknown", label: "Unknown" };
|
|
31
|
+
}
|
|
32
|
+
function writeIfNotExists(filePath, content) {
|
|
33
|
+
if ((0, import_node_fs.existsSync)(filePath)) {
|
|
34
|
+
console.log(` Skipped: ${filePath} (already exists)`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
|
|
38
|
+
(0, import_node_fs.writeFileSync)(filePath, content, "utf-8");
|
|
39
|
+
console.log(` Created: ${filePath}`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
var CONFIG_TEMPLATE = `/** @type {import('@corsenai/corsen-context').CorsenContextConfig} */
|
|
43
|
+
export default {
|
|
44
|
+
siteUrl: 'https://your-site.com',
|
|
45
|
+
siteName: 'Your Site Name',
|
|
46
|
+
description: 'Short description for AI agents.',
|
|
47
|
+
|
|
48
|
+
static: {
|
|
49
|
+
generateLlmsTxt: true,
|
|
50
|
+
includeFullContent: true,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
mcp: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
endpoint: '/v1/mcp',
|
|
56
|
+
tools: ['search_site', 'get_page_content', 'list_content', 'get_sitemap'],
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
content: {
|
|
60
|
+
postTypes: ['post', 'page'],
|
|
61
|
+
excludePaths: ['/admin', '/cart', '/checkout'],
|
|
62
|
+
maxPages: 500,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
security: {
|
|
66
|
+
rateLimit: 100,
|
|
67
|
+
burstLimit: 10,
|
|
68
|
+
allowedOrigins: [],
|
|
69
|
+
// Only trust X-Forwarded-For / X-Real-IP when behind a proxy you control.
|
|
70
|
+
trustProxy: false,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
cache: {
|
|
74
|
+
enabled: true,
|
|
75
|
+
ttl: 3600,
|
|
76
|
+
driver: 'memory',
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
credit: true,
|
|
80
|
+
};
|
|
81
|
+
`;
|
|
82
|
+
var NEXTJS_APP_PROVIDER = `import type { ContentProvider } from '@corsenai/corsen-context';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Implement your content provider here.
|
|
86
|
+
* This tells Corsen Context how to access your site's pages.
|
|
87
|
+
*
|
|
88
|
+
* Replace the stub methods below with real data from your CMS,
|
|
89
|
+
* database, filesystem, or API.
|
|
90
|
+
*/
|
|
91
|
+
export const siteProvider: ContentProvider = {
|
|
92
|
+
async getPages() {
|
|
93
|
+
// Return all public pages with metadata.
|
|
94
|
+
// Example:
|
|
95
|
+
// return [
|
|
96
|
+
// { url: 'https://your-site.com/', title: 'Home', description: 'Welcome', type: 'page' },
|
|
97
|
+
// { url: 'https://your-site.com/blog/hello', title: 'Hello', description: '...', type: 'post' },
|
|
98
|
+
// ];
|
|
99
|
+
return [];
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async getPageContent(url) {
|
|
103
|
+
// Return full markdown content for a given URL.
|
|
104
|
+
// Example:
|
|
105
|
+
// return { url, title: 'Page', description: '...', markdown: '# Page\\n\\nContent', metadata: {} };
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async searchContent(query, limit) {
|
|
110
|
+
// Return search results matching the query.
|
|
111
|
+
// Example:
|
|
112
|
+
// return [{ url: '...', title: '...', description: '...', snippet: '...', score: 1 }];
|
|
113
|
+
return [];
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
`;
|
|
117
|
+
var NEXTJS_APP_MCP_ROUTE = `import { createMCPHandler } from '@corsenai/corsen-context-nextjs';
|
|
118
|
+
import { siteProvider } from '@/lib/corsen-provider';
|
|
119
|
+
|
|
120
|
+
const config = {
|
|
121
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://your-site.com',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const { POST, OPTIONS } = createMCPHandler(config, siteProvider);
|
|
125
|
+
export { POST, OPTIONS };
|
|
126
|
+
`;
|
|
127
|
+
var NEXTJS_APP_SSE_ROUTE = `import { createSSEHandler } from '@corsenai/corsen-context-nextjs';
|
|
128
|
+
import { siteProvider } from '@/lib/corsen-provider';
|
|
129
|
+
|
|
130
|
+
const config = {
|
|
131
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://your-site.com',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Passing the provider shares auth + rate-limit state with the MCP route.
|
|
135
|
+
export const GET = createSSEHandler(config, siteProvider);
|
|
136
|
+
`;
|
|
137
|
+
var NEXTJS_APP_LLMS_ROUTE = `import { createLlmsTxtHandler } from '@corsenai/corsen-context-nextjs';
|
|
138
|
+
import { siteProvider } from '@/lib/corsen-provider';
|
|
139
|
+
|
|
140
|
+
const config = {
|
|
141
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://your-site.com',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const GET = createLlmsTxtHandler(config, siteProvider);
|
|
145
|
+
`;
|
|
146
|
+
var NEXTJS_APP_LLMS_FULL_ROUTE = `import { createLlmsFullTxtHandler } from '@corsenai/corsen-context-nextjs';
|
|
147
|
+
import { siteProvider } from '@/lib/corsen-provider';
|
|
148
|
+
|
|
149
|
+
const config = {
|
|
150
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://your-site.com',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const GET = createLlmsFullTxtHandler(config, siteProvider);
|
|
154
|
+
`;
|
|
155
|
+
var NEXTJS_PAGES_MCP_ROUTE = `import type { NextApiRequest, NextApiResponse } from 'next';
|
|
156
|
+
import { CorsenContext } from '@corsenai/corsen-context';
|
|
157
|
+
// import { siteProvider } from '@/lib/corsen-provider';
|
|
158
|
+
|
|
159
|
+
const config = {
|
|
160
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://your-site.com',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Replace with your provider
|
|
164
|
+
const provider = {
|
|
165
|
+
async getPages() { return []; },
|
|
166
|
+
async getPageContent(url) { return null; },
|
|
167
|
+
async searchContent(query, limit) { return []; },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const cc = new CorsenContext(config, provider);
|
|
171
|
+
|
|
172
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
173
|
+
if (req.method === 'OPTIONS') {
|
|
174
|
+
res.status(204).end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (req.method !== 'POST') {
|
|
179
|
+
res.status(405).json({ error: 'Method not allowed' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const server = cc.createMCPServer();
|
|
184
|
+
|
|
185
|
+
// Security headers
|
|
186
|
+
for (const [key, value] of Object.entries(server.getSecurityHeaders())) {
|
|
187
|
+
res.setHeader(key, value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Use the socket address by default. Only trust X-Forwarded-For behind a
|
|
191
|
+
// proxy you control, otherwise it is spoofable and defeats rate limiting.
|
|
192
|
+
const clientIp = req.socket.remoteAddress || 'unknown';
|
|
193
|
+
|
|
194
|
+
// Forward the API key so CORSEN_CONTEXT_API_KEY auth works.
|
|
195
|
+
const apiKey =
|
|
196
|
+
(req.headers['x-mcp-key'] as string) ||
|
|
197
|
+
(req.headers['authorization'] as string)?.replace('Bearer ', '') ||
|
|
198
|
+
undefined;
|
|
199
|
+
|
|
200
|
+
const rateLimit = await server.checkRateLimit(clientIp, apiKey);
|
|
201
|
+
for (const [key, value] of Object.entries(rateLimit.headers)) {
|
|
202
|
+
res.setHeader(key, value);
|
|
203
|
+
}
|
|
204
|
+
if (!rateLimit.allowed) {
|
|
205
|
+
res.status(429).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Rate limit exceeded' }, id: null });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// skipRateLimit: we already ran the limiter above (don't double-count).
|
|
210
|
+
const result = await server.handleRequest(req.body, clientIp, apiKey, { skipRateLimit: true });
|
|
211
|
+
|
|
212
|
+
// Notification (no id) \u2014 204 No Content
|
|
213
|
+
if (result === null) {
|
|
214
|
+
res.status(204).end();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
res.status(200).json(result);
|
|
219
|
+
}
|
|
220
|
+
`;
|
|
221
|
+
var NEXTJS_PAGES_LLMS_ROUTE = `import type { NextApiRequest, NextApiResponse } from 'next';
|
|
222
|
+
import { CorsenContext } from '@corsenai/corsen-context';
|
|
223
|
+
|
|
224
|
+
const config = {
|
|
225
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || 'https://your-site.com',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const provider = {
|
|
229
|
+
async getPages() { return []; },
|
|
230
|
+
async getPageContent(url) { return null; },
|
|
231
|
+
async searchContent(query, limit) { return []; },
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const cc = new CorsenContext(config, provider);
|
|
235
|
+
|
|
236
|
+
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
|
|
237
|
+
const text = await cc.generateLlmsTxt();
|
|
238
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
239
|
+
res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=3600');
|
|
240
|
+
res.status(200).send(text);
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
var EXPRESS_MIDDLEWARE = `import express from 'express';
|
|
244
|
+
import { CorsenContext } from '@corsenai/corsen-context';
|
|
245
|
+
// import config from './corsen-context.config.mjs';
|
|
246
|
+
|
|
247
|
+
const config = {
|
|
248
|
+
siteUrl: 'https://your-site.com',
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Replace with your content provider
|
|
252
|
+
const provider = {
|
|
253
|
+
async getPages() { return []; },
|
|
254
|
+
async getPageContent(url) { return null; },
|
|
255
|
+
async searchContent(query, limit) { return []; },
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const cc = new CorsenContext(config, provider);
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Mount these routes in your Express app:
|
|
262
|
+
* import corsenContextRoutes from './corsen-context.routes.js';
|
|
263
|
+
* corsenContextRoutes(app);
|
|
264
|
+
*/
|
|
265
|
+
export default function corsenContextRoutes(app) {
|
|
266
|
+
// Ensure JSON body parsing is available for the MCP endpoint.
|
|
267
|
+
app.use('/v1/mcp', express.json());
|
|
268
|
+
|
|
269
|
+
// Serve /llms.txt
|
|
270
|
+
app.get('/llms.txt', async (_req, res) => {
|
|
271
|
+
const text = await cc.generateLlmsTxt();
|
|
272
|
+
res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(text);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Serve /llms-full.txt
|
|
276
|
+
app.get('/llms-full.txt', async (_req, res) => {
|
|
277
|
+
const text = await cc.generateLlmsFullTxt();
|
|
278
|
+
res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(text);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// MCP endpoint
|
|
282
|
+
app.post('/v1/mcp', async (req, res) => {
|
|
283
|
+
const server = cc.createMCPServer();
|
|
284
|
+
|
|
285
|
+
// Security headers
|
|
286
|
+
for (const [key, value] of Object.entries(server.getSecurityHeaders())) {
|
|
287
|
+
res.set(key, value);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Use the socket address by default. Only trust X-Forwarded-For behind a
|
|
291
|
+
// proxy you control, otherwise it is spoofable and defeats rate limiting.
|
|
292
|
+
const clientIp = req.socket.remoteAddress || 'unknown';
|
|
293
|
+
|
|
294
|
+
// Forward the API key so CORSEN_CONTEXT_API_KEY auth works.
|
|
295
|
+
const apiKey = req.headers['x-mcp-key']?.toString()
|
|
296
|
+
|| req.headers['authorization']?.toString().replace('Bearer ', '')
|
|
297
|
+
|| undefined;
|
|
298
|
+
|
|
299
|
+
const rateLimit = await server.checkRateLimit(clientIp, apiKey);
|
|
300
|
+
for (const [key, value] of Object.entries(rateLimit.headers)) {
|
|
301
|
+
res.set(key, value);
|
|
302
|
+
}
|
|
303
|
+
if (!rateLimit.allowed) {
|
|
304
|
+
return res.status(429).json({
|
|
305
|
+
jsonrpc: '2.0', error: { code: -32000, message: 'Rate limit exceeded' }, id: null,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// skipRateLimit: we already ran the limiter above (don't double-count).
|
|
310
|
+
const result = await server.handleRequest(req.body, clientIp, apiKey, { skipRateLimit: true });
|
|
311
|
+
|
|
312
|
+
// Notification \u2014 no response
|
|
313
|
+
if (result === null) {
|
|
314
|
+
return res.status(204).end();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
res.json(result);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
`;
|
|
321
|
+
var ASTRO_PROVIDER = `import type { ContentProvider } from '@corsenai/corsen-context';
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Implement your content provider here \u2014 tell Corsen Context how to read your
|
|
325
|
+
* pages. Replace the stubs with real data from your content collections or CMS.
|
|
326
|
+
*/
|
|
327
|
+
export const siteProvider: ContentProvider = {
|
|
328
|
+
async getPages() {
|
|
329
|
+
return [];
|
|
330
|
+
},
|
|
331
|
+
async getPageContent(url) {
|
|
332
|
+
return null;
|
|
333
|
+
},
|
|
334
|
+
async searchContent(query, limit) {
|
|
335
|
+
return [];
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
`;
|
|
339
|
+
var ASTRO_MCP_ENDPOINT = `import { createMCPHandler } from '@corsenai/corsen-context-astro';
|
|
340
|
+
import { siteProvider } from '../../lib/corsen-provider';
|
|
341
|
+
|
|
342
|
+
// The Astro adapter handles auth, rate limiting (by clientAddress), CORS, and
|
|
343
|
+
// security headers. It reads the real client IP from Astro's clientAddress.
|
|
344
|
+
export const { POST, OPTIONS } = createMCPHandler(
|
|
345
|
+
{ siteUrl: import.meta.env.SITE ?? 'https://your-site.com' },
|
|
346
|
+
siteProvider,
|
|
347
|
+
);
|
|
348
|
+
`;
|
|
349
|
+
var ASTRO_LLMS_ENDPOINT = `import { createLlmsTxtHandler } from '@corsenai/corsen-context-astro';
|
|
350
|
+
import { siteProvider } from '../lib/corsen-provider';
|
|
351
|
+
|
|
352
|
+
export const GET = createLlmsTxtHandler(
|
|
353
|
+
{ siteUrl: import.meta.env.SITE ?? 'https://your-site.com' },
|
|
354
|
+
siteProvider,
|
|
355
|
+
);
|
|
356
|
+
`;
|
|
357
|
+
function scaffoldNextjsApp(cwd) {
|
|
358
|
+
const appDir = (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "src", "app")) ? (0, import_node_path.join)(cwd, "src") : cwd;
|
|
359
|
+
const libDir = (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "src")) ? (0, import_node_path.join)(cwd, "src", "lib") : (0, import_node_path.join)(cwd, "lib");
|
|
360
|
+
writeIfNotExists((0, import_node_path.join)(libDir, "corsen-provider.ts"), NEXTJS_APP_PROVIDER);
|
|
361
|
+
writeIfNotExists((0, import_node_path.join)(appDir, "app", "api", "mcp", "route.ts"), NEXTJS_APP_MCP_ROUTE);
|
|
362
|
+
writeIfNotExists((0, import_node_path.join)(appDir, "app", "api", "mcp", "sse", "route.ts"), NEXTJS_APP_SSE_ROUTE);
|
|
363
|
+
writeIfNotExists((0, import_node_path.join)(appDir, "app", "api", "corsen-context", "llms-txt", "route.ts"), NEXTJS_APP_LLMS_ROUTE);
|
|
364
|
+
writeIfNotExists((0, import_node_path.join)(appDir, "app", "api", "corsen-context", "llms-full-txt", "route.ts"), NEXTJS_APP_LLMS_FULL_ROUTE);
|
|
365
|
+
console.log("\n Next steps:");
|
|
366
|
+
console.log(" 1. npm install @corsenai/corsen-context @corsenai/corsen-context-nextjs");
|
|
367
|
+
console.log(" 2. Edit lib/corsen-provider.ts with your content source");
|
|
368
|
+
console.log(" 3. Update siteUrl in the route files or set NEXT_PUBLIC_SITE_URL env var");
|
|
369
|
+
console.log(" 4. Optionally wrap next.config.mjs with withCorsenContext() for /llms.txt rewrites");
|
|
370
|
+
}
|
|
371
|
+
function scaffoldNextjsPages(cwd) {
|
|
372
|
+
const pagesDir = (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "src", "pages")) ? (0, import_node_path.join)(cwd, "src", "pages") : (0, import_node_path.join)(cwd, "pages");
|
|
373
|
+
writeIfNotExists((0, import_node_path.join)(pagesDir, "api", "mcp.ts"), NEXTJS_PAGES_MCP_ROUTE);
|
|
374
|
+
writeIfNotExists((0, import_node_path.join)(pagesDir, "api", "llms-txt.ts"), NEXTJS_PAGES_LLMS_ROUTE);
|
|
375
|
+
console.log("\n Next steps:");
|
|
376
|
+
console.log(" 1. npm install @corsenai/corsen-context");
|
|
377
|
+
console.log(" 2. Edit pages/api/mcp.ts \u2014 replace the stub provider");
|
|
378
|
+
console.log(" 3. Set NEXT_PUBLIC_SITE_URL env var");
|
|
379
|
+
console.log(" 4. Add rewrites in next.config.mjs:");
|
|
380
|
+
console.log(' { source: "/llms.txt", destination: "/api/llms-txt" }');
|
|
381
|
+
}
|
|
382
|
+
function scaffoldExpress(cwd) {
|
|
383
|
+
const srcDir = (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "src")) ? (0, import_node_path.join)(cwd, "src") : cwd;
|
|
384
|
+
writeIfNotExists((0, import_node_path.join)(srcDir, "corsen-context.routes.js"), EXPRESS_MIDDLEWARE);
|
|
385
|
+
console.log("\n Next steps:");
|
|
386
|
+
console.log(" 1. npm install @corsenai/corsen-context");
|
|
387
|
+
console.log(" 2. Import and mount the routes in your Express app:");
|
|
388
|
+
console.log(' import corsenContextRoutes from "./corsen-context.routes.js";');
|
|
389
|
+
console.log(" corsenContextRoutes(app);");
|
|
390
|
+
console.log(" 3. Edit the provider in corsen-context.routes.js");
|
|
391
|
+
}
|
|
392
|
+
function scaffoldAstro(cwd) {
|
|
393
|
+
const srcDir = (0, import_node_fs.existsSync)((0, import_node_path.join)(cwd, "src")) ? (0, import_node_path.join)(cwd, "src") : cwd;
|
|
394
|
+
const pagesDir = (0, import_node_path.join)(srcDir, "pages");
|
|
395
|
+
writeIfNotExists((0, import_node_path.join)(srcDir, "lib", "corsen-provider.ts"), ASTRO_PROVIDER);
|
|
396
|
+
writeIfNotExists((0, import_node_path.join)(pagesDir, "v1", "mcp.ts"), ASTRO_MCP_ENDPOINT);
|
|
397
|
+
writeIfNotExists((0, import_node_path.join)(pagesDir, "llms.txt.ts"), ASTRO_LLMS_ENDPOINT);
|
|
398
|
+
console.log("\n Next steps:");
|
|
399
|
+
console.log(" 1. npm install @corsenai/corsen-context @corsenai/corsen-context-astro");
|
|
400
|
+
console.log(' 2. Make sure Astro SSR is enabled (output: "server" or "hybrid" in astro.config)');
|
|
401
|
+
console.log(" 3. Edit the provider in src/lib/corsen-provider.ts with your content source");
|
|
402
|
+
}
|
|
403
|
+
function scaffoldStatic(cwd) {
|
|
404
|
+
console.log("\n Static site detected. Options:");
|
|
405
|
+
console.log(" 1. Generate llms.txt from your live site:");
|
|
406
|
+
console.log(" npx @corsenai/corsen-context-cli generate --url https://your-site.com");
|
|
407
|
+
console.log(" 2. Place the generated llms.txt in your site root");
|
|
408
|
+
console.log(" 3. For a dynamic MCP server, deploy a serverless function:");
|
|
409
|
+
console.log(" - Vercel: create api/mcp.ts");
|
|
410
|
+
console.log(" - Netlify: create netlify/functions/mcp.ts");
|
|
411
|
+
console.log(" - Cloudflare Workers: use the core library directly");
|
|
412
|
+
}
|
|
413
|
+
function scaffoldHugo(cwd) {
|
|
414
|
+
console.log("\n Hugo detected. Options:");
|
|
415
|
+
console.log(" 1. Generate llms.txt from your live site:");
|
|
416
|
+
console.log(" npx @corsenai/corsen-context-cli generate --url https://your-site.com");
|
|
417
|
+
console.log(" 2. Place llms.txt in your static/ directory");
|
|
418
|
+
console.log(" 3. For a dynamic MCP server, deploy a serverless function alongside your Hugo site");
|
|
419
|
+
}
|
|
420
|
+
async function init() {
|
|
421
|
+
const cwd = process.cwd();
|
|
422
|
+
const { framework, label } = detectFramework(cwd);
|
|
423
|
+
console.log(`
|
|
424
|
+
Detected framework: ${label}`);
|
|
425
|
+
console.log(" Initializing Corsen Context...\n");
|
|
426
|
+
writeIfNotExists((0, import_node_path.join)(cwd, "corsen-context.config.mjs"), CONFIG_TEMPLATE);
|
|
427
|
+
switch (framework) {
|
|
428
|
+
case "nextjs-app":
|
|
429
|
+
scaffoldNextjsApp(cwd);
|
|
430
|
+
break;
|
|
431
|
+
case "nextjs-pages":
|
|
432
|
+
scaffoldNextjsPages(cwd);
|
|
433
|
+
break;
|
|
434
|
+
case "express":
|
|
435
|
+
scaffoldExpress(cwd);
|
|
436
|
+
break;
|
|
437
|
+
case "astro":
|
|
438
|
+
scaffoldAstro(cwd);
|
|
439
|
+
break;
|
|
440
|
+
case "hugo":
|
|
441
|
+
scaffoldHugo(cwd);
|
|
442
|
+
break;
|
|
443
|
+
case "static":
|
|
444
|
+
scaffoldStatic(cwd);
|
|
445
|
+
break;
|
|
446
|
+
case "wordpress":
|
|
447
|
+
console.log("\n WordPress detected.");
|
|
448
|
+
console.log(" Install the Corsen Context plugin from WordPress.org or GitHub:");
|
|
449
|
+
console.log(" https://github.com/CorsenAI/corsen-context/tree/main/packages/wordpress-plugin");
|
|
450
|
+
break;
|
|
451
|
+
default:
|
|
452
|
+
console.log("\n Framework not detected. Generic setup:");
|
|
453
|
+
console.log(" 1. npm install @corsenai/corsen-context");
|
|
454
|
+
console.log(" 2. See: https://github.com/CorsenAI/corsen-context#quick-start");
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
console.log("\n Edit corsen-context.config.mjs to set your siteUrl and options.");
|
|
458
|
+
console.log(' Run "npx @corsenai/corsen-context-cli doctor --url https://your-site.com" to validate.\n');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/generate.ts
|
|
462
|
+
var import_node_fs2 = require("fs");
|
|
463
|
+
var import_node_path2 = require("path");
|
|
464
|
+
var import_corsen_context = require("@corsenai/corsen-context");
|
|
465
|
+
function parseArgs(args2) {
|
|
466
|
+
const result = {};
|
|
467
|
+
for (let i = 0; i < args2.length; i++) {
|
|
468
|
+
if ((args2[i] === "--url" || args2[i] === "-u") && args2[i + 1]) {
|
|
469
|
+
result.url = args2[++i];
|
|
470
|
+
}
|
|
471
|
+
if ((args2[i] === "--output" || args2[i] === "-o") && args2[i + 1]) {
|
|
472
|
+
result.output = args2[++i];
|
|
473
|
+
}
|
|
474
|
+
if (args2[i] === "--full") {
|
|
475
|
+
result.full = true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
async function generate(args2) {
|
|
481
|
+
const { url, output, full } = parseArgs(args2);
|
|
482
|
+
if (!url) {
|
|
483
|
+
console.error(" Error: --url is required");
|
|
484
|
+
console.error(" Usage: npx corsen-context generate --url https://mysite.com [--full]");
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
console.log(`
|
|
488
|
+
Generating llms.txt for ${url}...`);
|
|
489
|
+
console.log(" Discovering + fetching pages via sitemap...");
|
|
490
|
+
const provider = (0, import_corsen_context.createSitemapProvider)(url, { maxPages: 100 });
|
|
491
|
+
const cc = new import_corsen_context.CorsenContext({ siteUrl: url }, provider);
|
|
492
|
+
const outputDir = output || process.cwd();
|
|
493
|
+
console.log(" Generating llms.txt...");
|
|
494
|
+
const llmsTxt = await cc.generateLlmsTxt();
|
|
495
|
+
const llmsPath = (0, import_node_path2.join)(outputDir, "llms.txt");
|
|
496
|
+
(0, import_node_fs2.writeFileSync)(llmsPath, llmsTxt, "utf-8");
|
|
497
|
+
console.log(` Written: ${llmsPath}`);
|
|
498
|
+
if (full) {
|
|
499
|
+
console.log(" Generating llms-full.txt...");
|
|
500
|
+
const llmsFull = await cc.generateLlmsFullTxt();
|
|
501
|
+
const fullPath = (0, import_node_path2.join)(outputDir, "llms-full.txt");
|
|
502
|
+
(0, import_node_fs2.writeFileSync)(fullPath, llmsFull, "utf-8");
|
|
503
|
+
console.log(` Written: ${fullPath}`);
|
|
504
|
+
}
|
|
505
|
+
console.log("\n Done!\n");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/doctor.ts
|
|
509
|
+
var import_corsen_context2 = require("@corsenai/corsen-context");
|
|
510
|
+
function parseArgs2(args2) {
|
|
511
|
+
const result = {};
|
|
512
|
+
for (let i = 0; i < args2.length; i++) {
|
|
513
|
+
if ((args2[i] === "--url" || args2[i] === "-u") && args2[i + 1]) {
|
|
514
|
+
result.url = args2[++i];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
async function doctor(args2) {
|
|
520
|
+
const { url } = parseArgs2(args2);
|
|
521
|
+
if (!url) {
|
|
522
|
+
console.error(" Error: --url is required");
|
|
523
|
+
console.error(" Usage: npx corsen-context doctor --url https://mysite.com");
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
console.log(`
|
|
527
|
+
Checking AI readiness for ${url}...
|
|
528
|
+
`);
|
|
529
|
+
const results = [];
|
|
530
|
+
results.push({
|
|
531
|
+
name: "HTTPS",
|
|
532
|
+
status: url.startsWith("https://") ? "pass" : "fail",
|
|
533
|
+
message: url.startsWith("https://") ? "Site uses HTTPS" : "Site must use HTTPS"
|
|
534
|
+
});
|
|
535
|
+
const isPrivate = await (0, import_corsen_context2.isPrivateUrl)(url);
|
|
536
|
+
results.push({
|
|
537
|
+
name: "Public URL",
|
|
538
|
+
status: !isPrivate ? "pass" : "fail",
|
|
539
|
+
message: !isPrivate ? "URL is publicly accessible" : "URL points to private network"
|
|
540
|
+
});
|
|
541
|
+
const base = url.replace(/\/$/, "");
|
|
542
|
+
try {
|
|
543
|
+
const res = await fetch(`${base}/llms.txt`, {
|
|
544
|
+
signal: AbortSignal.timeout(1e4),
|
|
545
|
+
redirect: "follow"
|
|
546
|
+
});
|
|
547
|
+
results.push({
|
|
548
|
+
name: "llms.txt",
|
|
549
|
+
status: res.ok ? "pass" : "warn",
|
|
550
|
+
message: res.ok ? `Found /llms.txt (${res.status})` : `No /llms.txt found (${res.status})`
|
|
551
|
+
});
|
|
552
|
+
} catch {
|
|
553
|
+
results.push({
|
|
554
|
+
name: "llms.txt",
|
|
555
|
+
status: "warn",
|
|
556
|
+
message: "Could not check /llms.txt"
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const res = await fetch(`${base}/sitemap.xml`, {
|
|
561
|
+
signal: AbortSignal.timeout(1e4),
|
|
562
|
+
redirect: "follow"
|
|
563
|
+
});
|
|
564
|
+
results.push({
|
|
565
|
+
name: "Sitemap",
|
|
566
|
+
status: res.ok ? "pass" : "warn",
|
|
567
|
+
message: res.ok ? `Found /sitemap.xml (${res.status})` : `No /sitemap.xml found (${res.status})`
|
|
568
|
+
});
|
|
569
|
+
} catch {
|
|
570
|
+
results.push({
|
|
571
|
+
name: "Sitemap",
|
|
572
|
+
status: "warn",
|
|
573
|
+
message: "Could not check /sitemap.xml"
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const res = await fetch(`${base}/robots.txt`, {
|
|
578
|
+
signal: AbortSignal.timeout(1e4),
|
|
579
|
+
redirect: "follow"
|
|
580
|
+
});
|
|
581
|
+
if (res.ok) {
|
|
582
|
+
const text = await res.text();
|
|
583
|
+
const hasMcp = text.toLowerCase().includes("mcp:");
|
|
584
|
+
results.push({
|
|
585
|
+
name: "robots.txt MCP",
|
|
586
|
+
status: hasMcp ? "pass" : "warn",
|
|
587
|
+
message: hasMcp ? "robots.txt contains MCP endpoint reference" : "robots.txt exists but no MCP reference found"
|
|
588
|
+
});
|
|
589
|
+
} else {
|
|
590
|
+
results.push({
|
|
591
|
+
name: "robots.txt MCP",
|
|
592
|
+
status: "warn",
|
|
593
|
+
message: "No robots.txt found"
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
results.push({
|
|
598
|
+
name: "robots.txt MCP",
|
|
599
|
+
status: "warn",
|
|
600
|
+
message: "Could not check robots.txt"
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
const res = await fetch(`${base}/v1/mcp`, {
|
|
605
|
+
method: "POST",
|
|
606
|
+
headers: { "Content-Type": "application/json" },
|
|
607
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1 }),
|
|
608
|
+
signal: AbortSignal.timeout(1e4)
|
|
609
|
+
});
|
|
610
|
+
if (res.ok) {
|
|
611
|
+
const data = await res.json();
|
|
612
|
+
const protocolVersion = data?.result?.protocolVersion;
|
|
613
|
+
results.push({
|
|
614
|
+
name: "MCP Endpoint",
|
|
615
|
+
status: protocolVersion ? "pass" : "warn",
|
|
616
|
+
message: protocolVersion ? `MCP endpoint active (protocol ${protocolVersion})` : "MCP endpoint responded but invalid protocol"
|
|
617
|
+
});
|
|
618
|
+
} else {
|
|
619
|
+
results.push({
|
|
620
|
+
name: "MCP Endpoint",
|
|
621
|
+
status: "warn",
|
|
622
|
+
message: `MCP endpoint at /v1/mcp returned ${res.status}`
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
results.push({
|
|
627
|
+
name: "MCP Endpoint",
|
|
628
|
+
status: "warn",
|
|
629
|
+
message: "No MCP endpoint found at /v1/mcp"
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const icons = { pass: "\u2705", fail: "\u274C", warn: "\u26A0\uFE0F" };
|
|
633
|
+
for (const r of results) {
|
|
634
|
+
console.log(` ${icons[r.status]} ${r.name}: ${r.message}`);
|
|
635
|
+
}
|
|
636
|
+
const passes = results.filter((r) => r.status === "pass").length;
|
|
637
|
+
const fails = results.filter((r) => r.status === "fail").length;
|
|
638
|
+
const warns = results.filter((r) => r.status === "warn").length;
|
|
639
|
+
console.log(`
|
|
640
|
+
Score: ${passes}/${results.length} checks passed`);
|
|
641
|
+
if (fails > 0) console.log(` ${fails} critical issue(s) found`);
|
|
642
|
+
if (warns > 0) console.log(` ${warns} warning(s)`);
|
|
643
|
+
if (passes === results.length) {
|
|
644
|
+
console.log("\n Your site is AI-ready!\n");
|
|
645
|
+
} else {
|
|
646
|
+
console.log('\n Run "npx corsen-context init" to set up missing components.\n');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/index.ts
|
|
651
|
+
var import_corsen_context3 = require("@corsenai/corsen-context");
|
|
652
|
+
var args = process.argv.slice(2);
|
|
653
|
+
var command = args[0];
|
|
654
|
+
var VERSION = import_corsen_context3.CORSEN_CONTEXT_VERSION;
|
|
655
|
+
function printHelp() {
|
|
656
|
+
console.log(`
|
|
657
|
+
corsen-context v${VERSION}
|
|
658
|
+
${import_corsen_context3.CREDIT_LINE}
|
|
659
|
+
|
|
660
|
+
Usage:
|
|
661
|
+
corsen-context <command> [options]
|
|
662
|
+
|
|
663
|
+
Commands:
|
|
664
|
+
init Detect framework, create config & integration files
|
|
665
|
+
generate Force regeneration of llms.txt and llms-full.txt
|
|
666
|
+
doctor Check if your site is AI-ready (validate setup)
|
|
667
|
+
|
|
668
|
+
Options:
|
|
669
|
+
--help Show this help message
|
|
670
|
+
--version Show version number
|
|
671
|
+
|
|
672
|
+
Examples:
|
|
673
|
+
npx @corsenai/corsen-context-cli init
|
|
674
|
+
npx @corsenai/corsen-context-cli generate --url https://mysite.com
|
|
675
|
+
npx @corsenai/corsen-context-cli doctor --url https://mysite.com
|
|
676
|
+
`);
|
|
677
|
+
}
|
|
678
|
+
async function main() {
|
|
679
|
+
if (!command || command === "--help" || command === "-h") {
|
|
680
|
+
printHelp();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (command === "--version" || command === "-v") {
|
|
684
|
+
console.log(VERSION);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
switch (command) {
|
|
688
|
+
case "init":
|
|
689
|
+
await init();
|
|
690
|
+
break;
|
|
691
|
+
case "generate":
|
|
692
|
+
await generate(args.slice(1));
|
|
693
|
+
break;
|
|
694
|
+
case "doctor":
|
|
695
|
+
await doctor(args.slice(1));
|
|
696
|
+
break;
|
|
697
|
+
default:
|
|
698
|
+
console.error(`Unknown command: ${command}`);
|
|
699
|
+
printHelp();
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
main().catch((err) => {
|
|
704
|
+
console.error("Error:", err.message);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@corsenai/corsen-context-cli",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "CLI for Corsen Context — init, generate, and validate your AI context layer",
|
|
5
|
+
"author": "Corsen AI <contact@corsen.ai>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/CorsenAI/corsen-context",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/CorsenAI/corsen-context.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"llms-txt",
|
|
16
|
+
"ai",
|
|
17
|
+
"cli",
|
|
18
|
+
"corsen"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"corsen-context": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@corsenai/corsen-context": "1.2.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"tsup": "^8.3.0",
|
|
34
|
+
"typescript": "^5.7.0",
|
|
35
|
+
"vitest": "^2.1.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/CorsenAI/corsen-context/issues"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"clean": "rm -rf dist"
|
|
51
|
+
}
|
|
52
|
+
}
|