@rtrentjones/greenlight 0.2.4

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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/assets/skills/deploy-verify-promote/SKILL.md +53 -0
  3. package/assets/skills/provider-cloudflare/SKILL.md +42 -0
  4. package/assets/skills/provider-github/SKILL.md +35 -0
  5. package/assets/skills/provider-hcp/SKILL.md +46 -0
  6. package/assets/skills/provider-oci/SKILL.md +58 -0
  7. package/assets/skills/provider-supabase/SKILL.md +40 -0
  8. package/assets/skills/provider-vercel/SKILL.md +39 -0
  9. package/dist/agent-web-I4LXW4SR.js +7 -0
  10. package/dist/bin.js +1951 -0
  11. package/dist/chunk-6N7MD6FR.js +75 -0
  12. package/dist/chunk-KFKYLGFX.js +271 -0
  13. package/dist/chunk-KP3Y6WRU.js +45 -0
  14. package/dist/chunk-QFKE5JKC.js +12 -0
  15. package/dist/chunk-UXHHLEYO.js +231 -0
  16. package/dist/chunk-WFZTRXBF.js +61 -0
  17. package/dist/chunk-XBDQJVAX.js +94 -0
  18. package/dist/eval-LLQPOEQX.js +9 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +16 -0
  21. package/dist/mcp-KU7WKB5K.js +7 -0
  22. package/dist/playwright-CGTTHGIL.js +7 -0
  23. package/dist/test-7GMOU7I5.js +7 -0
  24. package/package.json +51 -0
  25. package/templates/_template-astro/README.md +18 -0
  26. package/templates/_template-astro/astro.config.mjs +9 -0
  27. package/templates/_template-astro/package.json +18 -0
  28. package/templates/_template-astro/src/pages/index.astro +18 -0
  29. package/templates/_template-astro/tsconfig.json +5 -0
  30. package/templates/_template-astro/wrangler.jsonc +12 -0
  31. package/templates/_template-mcp/README.md +28 -0
  32. package/templates/_template-mcp/oci/Dockerfile +11 -0
  33. package/templates/_template-mcp/oci/package.json +12 -0
  34. package/templates/_template-mcp/oci/server.ts +80 -0
  35. package/templates/_template-mcp/workers/README.md +32 -0
  36. package/templates/_template-next/README.md +5 -0
@@ -0,0 +1,94 @@
1
+ import {
2
+ msg,
3
+ report
4
+ } from "./chunk-QFKE5JKC.js";
5
+
6
+ // ../packages/verify/src/mcp.ts
7
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9
+ async function verifyMcp(baseUrl, spec) {
10
+ const checks = [];
11
+ const client = new Client({ name: "greenlight-verify", version: "0.0.0" });
12
+ const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
13
+ try {
14
+ await client.connect(transport);
15
+ checks.push({ name: "initialize handshake", pass: true });
16
+ } catch (e) {
17
+ checks.push({ name: "initialize handshake", pass: false, detail: msg(e) });
18
+ return report("mcp", baseUrl, checks);
19
+ }
20
+ try {
21
+ const { tools } = await client.listTools();
22
+ const names = tools.map((t) => t.name);
23
+ checks.push({ name: `tools/list responded (${names.length} tools)`, pass: true });
24
+ for (const t of spec.expectTools) {
25
+ const has = names.includes(t);
26
+ checks.push({
27
+ name: `tools/list includes "${t}"`,
28
+ pass: has,
29
+ detail: has ? void 0 : `got [${names.join(", ")}]`
30
+ });
31
+ }
32
+ } catch (e) {
33
+ checks.push({ name: "tools/list", pass: false, detail: msg(e) });
34
+ }
35
+ if (spec.call) {
36
+ const label = `tools/call ${spec.call.name}`;
37
+ try {
38
+ const res = await client.callTool({
39
+ name: spec.call.name,
40
+ arguments: spec.call.args ?? {}
41
+ });
42
+ const reasons = [];
43
+ if (res.isError) reasons.push("result.isError = true");
44
+ for (const k of spec.call.expectKeys ?? []) {
45
+ if (!res.structuredContent || !(k in res.structuredContent)) {
46
+ reasons.push(`structuredContent missing "${k}"`);
47
+ }
48
+ }
49
+ checks.push({
50
+ name: label,
51
+ pass: reasons.length === 0,
52
+ detail: reasons.join("; ") || void 0
53
+ });
54
+ } catch (e) {
55
+ checks.push({ name: label, pass: false, detail: msg(e) });
56
+ }
57
+ }
58
+ await client.close();
59
+ if (spec.requireAuthRejection) checks.push(await checkAuthRejection(baseUrl));
60
+ return report("mcp", baseUrl, checks);
61
+ }
62
+ async function checkAuthRejection(baseUrl) {
63
+ try {
64
+ const res = await fetch(baseUrl, {
65
+ method: "POST",
66
+ headers: {
67
+ "content-type": "application/json",
68
+ accept: "application/json, text/event-stream"
69
+ },
70
+ body: JSON.stringify({
71
+ jsonrpc: "2.0",
72
+ id: 1,
73
+ method: "initialize",
74
+ params: {
75
+ protocolVersion: "2025-06-18",
76
+ capabilities: {},
77
+ clientInfo: { name: "greenlight-verify-probe", version: "0.0.0" }
78
+ }
79
+ })
80
+ });
81
+ const rejected = res.status === 401 || res.status === 403;
82
+ return {
83
+ name: "unauthenticated request rejected",
84
+ pass: rejected,
85
+ detail: rejected ? void 0 : `expected 401/403, got ${res.status}`
86
+ };
87
+ } catch (e) {
88
+ return { name: "unauthenticated request rejected", pass: false, detail: msg(e) };
89
+ }
90
+ }
91
+
92
+ export {
93
+ verifyMcp
94
+ };
@@ -0,0 +1,9 @@
1
+ import {
2
+ llmJudge,
3
+ verifyEval
4
+ } from "./chunk-6N7MD6FR.js";
5
+ import "./chunk-QFKE5JKC.js";
6
+ export {
7
+ llmJudge,
8
+ verifyEval
9
+ };
@@ -0,0 +1,2 @@
1
+ export { GreenlightConfig, defineConfig, loadConfig } from '@rtrentjones/greenlight-shared';
2
+ export { VerifySpec, defineVerify } from '@rtrentjones/greenlight-verify';
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import {
2
+ defineConfig,
3
+ defineVerify,
4
+ loadConfig
5
+ } from "./chunk-KFKYLGFX.js";
6
+ import "./chunk-XBDQJVAX.js";
7
+ import "./chunk-WFZTRXBF.js";
8
+ import "./chunk-KP3Y6WRU.js";
9
+ import "./chunk-UXHHLEYO.js";
10
+ import "./chunk-6N7MD6FR.js";
11
+ import "./chunk-QFKE5JKC.js";
12
+ export {
13
+ defineConfig,
14
+ defineVerify,
15
+ loadConfig
16
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ verifyMcp
3
+ } from "./chunk-XBDQJVAX.js";
4
+ import "./chunk-QFKE5JKC.js";
5
+ export {
6
+ verifyMcp
7
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ verifyPlaywright
3
+ } from "./chunk-WFZTRXBF.js";
4
+ import "./chunk-QFKE5JKC.js";
5
+ export {
6
+ verifyPlaywright
7
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ verifyTest
3
+ } from "./chunk-KP3Y6WRU.js";
4
+ import "./chunk-QFKE5JKC.js";
5
+ export {
6
+ verifyTest
7
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@rtrentjones/greenlight",
3
+ "version": "0.2.4",
4
+ "description": "Greenlight CLI — setup and lifecycle for the harness.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/RTrentJones/greenlight.git",
9
+ "directory": "cli"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "bin": {
14
+ "greenlight": "./dist/bin.js"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "templates",
19
+ "assets"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.12.0",
26
+ "jiti": "^2.4.2",
27
+ "zod": "^3.24.1"
28
+ },
29
+ "optionalDependencies": {
30
+ "playwright": "^1.49.0",
31
+ "@anthropic-ai/sdk": "^0.69.0"
32
+ },
33
+ "devDependencies": {
34
+ "@rtrentjones/greenlight-adapters": "0.2.4",
35
+ "@rtrentjones/greenlight-loop": "0.2.4",
36
+ "@rtrentjones/greenlight-shared": "0.2.4",
37
+ "@rtrentjones/greenlight-verify": "0.2.4"
38
+ },
39
+ "scripts": {
40
+ "build": "node scripts/copy-assets.mjs && tsup",
41
+ "typecheck": "tsc -p tsconfig.json",
42
+ "dev": "tsx src/bin.ts"
43
+ },
44
+ "types": "./dist/index.d.ts",
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist/index.d.ts",
48
+ "import": "./dist/index.js"
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,18 @@
1
+ # `_template-astro`
2
+
3
+ Lane template for **Astro on Cloudflare Workers** (Static Assets) — verify mode `api`
4
+ (+ light `playwright`). Materialized into a tool by `greenlight add <name> --lane astro --target workers`.
5
+
6
+ A complete, copy-ready minimal site: a homepage, `@astrojs/sitemap`, a `wrangler.jsonc`
7
+ for Workers Static Assets, and `astro/tsconfigs/strict`. `add` rewrites `package.json`'s
8
+ `name` to your tool name. `site` comes from `SITE_URL` (default `example.dev`).
9
+
10
+ ```
11
+ greenlight add marketing --lane astro --target workers
12
+ pnpm --filter marketing build && pnpm --filter marketing preview
13
+ greenlight verify marketing --url http://localhost:4321
14
+ ```
15
+
16
+ The default astro verify spec is a generic web smoke (homepage 200 + no broken internal
17
+ links). For a content site that also has a feed/sitemap (like the blog), add a
18
+ `verify.config.ts` asserting `rssValid` / `sitemapValid` — see `apps/blog/verify.config.ts`.
@@ -0,0 +1,9 @@
1
+ import sitemap from '@astrojs/sitemap';
2
+ import { defineConfig } from 'astro/config';
3
+
4
+ // `site` is injected from the manifest domain at build time (SITE_URL); the default
5
+ // keeps the template generic — no real domain (seam rule 15.2.1).
6
+ export default defineConfig({
7
+ site: process.env.SITE_URL ?? 'https://example.dev',
8
+ integrations: [sitemap()],
9
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "greenlight-template-astro",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "astro dev",
8
+ "build": "astro build",
9
+ "preview": "astro preview"
10
+ },
11
+ "dependencies": {
12
+ "@astrojs/sitemap": "^3.2.1",
13
+ "astro": "^5.7.0"
14
+ },
15
+ "devDependencies": {
16
+ "wrangler": "^4.20.0"
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ const title = 'New Astro tool';
3
+ ---
4
+
5
+ <!doctype html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10
+ <title>{title}</title>
11
+ </head>
12
+ <body>
13
+ <main>
14
+ <h1>{title}</h1>
15
+ <p>Scaffolded by <code>greenlight add &lt;name&gt; --lane astro --target workers</code>. Edit <code>src/pages/index.astro</code> and ship it through the loop.</p>
16
+ </main>
17
+ </body>
18
+ </html>
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "include": [".astro/types.d.ts", "**/*"],
4
+ "exclude": ["dist"]
5
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ // Astro static site on Cloudflare Workers Static Assets. `greenlight add` rewrites
3
+ // package.json name; set the worker name to match your tool. Routes/custom domains
4
+ // are managed by Terraform (Phase 5) — names stay generic (seam rule 15.2.1).
5
+ "name": "astro-tool",
6
+ "compatibility_date": "2025-06-01",
7
+ "assets": { "directory": "./dist" },
8
+ "env": {
9
+ "beta": { "name": "astro-tool-beta" },
10
+ "prod": { "name": "astro-tool" }
11
+ }
12
+ }
@@ -0,0 +1,28 @@
1
+ # `_template-mcp`
2
+
3
+ Lane template for **MCP servers** — verify mode `mcp` (initialize → `tools/list` → call → auth assertion). Materialized into a tool by `greenlight add`. Two target shapes:
4
+
5
+ ## `oci` — Node streamable-HTTP server (recommended; the BAMCP shape)
6
+
7
+ A plain Node HTTP server using `@modelcontextprotocol/sdk` (`StreamableHTTPServerTransport`), containerized and run behind a Cloudflare Tunnel in prod. Best for stateful servers or ones needing local binaries/filesystem (e.g. samtools). See [oci/server.ts](oci/server.ts) + [oci/Dockerfile](oci/Dockerfile). This is the reference implementation — `tools/ping-mcp` is an instance of it.
8
+
9
+ Local dev / loop proof (no cloud):
10
+ ```
11
+ PORT=8787 node oci/server.ts # or `pnpm --filter <pkg> start`
12
+ greenlight verify <name> --url http://127.0.0.1:8787/mcp
13
+ ```
14
+
15
+ ## `workers` — remote MCP on the edge (optional)
16
+
17
+ Cloudflare's `agents` package (`McpAgent` / `createMcpHandler`) can host a remote MCP on Workers. **Caveat:** `agents` pulls heavy transitive deps (`ai`, `react`) and currently needs an `ai` alias in `wrangler` config to bundle. For a simple server, prefer the `oci`/Node shape above; reach for `workers` only when you specifically want edge hosting + Durable-Object session state.
18
+
19
+ ## Verify spec
20
+
21
+ Ship a `verify.config.ts` (default export) so `greenlight verify` asserts the real contract:
22
+ ```ts
23
+ export default { mode: 'mcp', expectTools: ['<tool>'], call: { name: '<tool>' } };
24
+ ```
25
+
26
+ ## Auth
27
+
28
+ `auth: none` only for public read-only servers. Mutating/private servers default to `bearer`/`oauth` (greenlight-v1.md §6/§14).
@@ -0,0 +1,11 @@
1
+ # MCP server (oci lane). Node 24 strips TypeScript types natively, so server.ts runs directly.
2
+ FROM node:24-slim
3
+ WORKDIR /app
4
+
5
+ COPY package.json ./
6
+ RUN npm install --omit=dev
7
+
8
+ COPY . .
9
+ ENV PORT=8787
10
+ EXPOSE 8787
11
+ CMD ["node", "src/server.ts"]
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "greenlight-template-mcp-oci",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node src/server.ts"
8
+ },
9
+ "dependencies": {
10
+ "@modelcontextprotocol/sdk": "^1.12.0"
11
+ }
12
+ }
@@ -0,0 +1,80 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import http from 'node:http';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
6
+
7
+ // MCP server template (oci lane): Node streamable-HTTP, containerized, run behind a
8
+ // Cloudflare Tunnel in prod. Rename `my-mcp` and add your tools.
9
+ const PORT = Number(process.env.PORT ?? 8787);
10
+ const transports: Record<string, StreamableHTTPServerTransport> = {};
11
+
12
+ function buildMcpServer(): McpServer {
13
+ const server = new McpServer({ name: 'my-mcp', version: '0.0.0' });
14
+ server.registerTool(
15
+ 'echo',
16
+ { description: 'Echo the input text.', inputSchema: {} },
17
+ async () => ({ content: [{ type: 'text', text: 'hello from my-mcp' }] }),
18
+ );
19
+ return server;
20
+ }
21
+
22
+ function readJson(req: http.IncomingMessage): Promise<unknown> {
23
+ return new Promise((resolve, reject) => {
24
+ let data = '';
25
+ req.on('data', (c) => {
26
+ data += c;
27
+ });
28
+ req.on('end', () => {
29
+ try {
30
+ resolve(data ? JSON.parse(data) : undefined);
31
+ } catch (e) {
32
+ reject(e);
33
+ }
34
+ });
35
+ req.on('error', reject);
36
+ });
37
+ }
38
+
39
+ async function handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
40
+ const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
41
+ if (url.pathname !== '/mcp') {
42
+ res.writeHead(404).end('connect at /mcp');
43
+ return;
44
+ }
45
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
46
+ if (req.method === 'POST') {
47
+ const body = await readJson(req);
48
+ let transport = sessionId ? transports[sessionId] : undefined;
49
+ if (!transport && isInitializeRequest(body)) {
50
+ transport = new StreamableHTTPServerTransport({
51
+ sessionIdGenerator: () => randomUUID(),
52
+ onsessioninitialized: (sid) => {
53
+ if (transport) transports[sid] = transport;
54
+ },
55
+ });
56
+ await buildMcpServer().connect(transport);
57
+ }
58
+ if (!transport) {
59
+ res.writeHead(400).end('no session');
60
+ return;
61
+ }
62
+ await transport.handleRequest(req, res, body);
63
+ } else if (
64
+ (req.method === 'GET' || req.method === 'DELETE') &&
65
+ sessionId &&
66
+ transports[sessionId]
67
+ ) {
68
+ await transports[sessionId].handleRequest(req, res);
69
+ } else {
70
+ res.writeHead(405).end();
71
+ }
72
+ }
73
+
74
+ http
75
+ .createServer((req, res) => {
76
+ void handle(req, res);
77
+ })
78
+ .listen(PORT, () => {
79
+ console.log(`my-mcp listening on :${PORT}/mcp`);
80
+ });
@@ -0,0 +1,32 @@
1
+ # `_template-mcp/workers` (optional shape)
2
+
3
+ Remote MCP on Cloudflare Workers via Cloudflare's [`agents`](https://www.npmjs.com/package/agents) package (`McpAgent` for Durable-Object session state, or `createMcpHandler` for a stateless fetch handler).
4
+
5
+ **Status / caveat (Phase 4):** `agents` pulls heavy transitive deps (`ai`, `react`) and its bundle does a dynamic `import("ai")` that `wrangler` fails to resolve without an `alias` entry — e.g. in `wrangler.jsonc`:
6
+
7
+ ```jsonc
8
+ { "alias": { "ai": "./src/ai-stub.ts" } }
9
+ ```
10
+
11
+ For a simple server this overhead isn't worth it — use the [`../oci`](../oci) Node shape, which proves the same protocol loop locally (`greenlight verify <name> --url http://127.0.0.1:8787/mcp`) with no exotic deps. Reach for `workers` only when you specifically need edge hosting + DO-backed sessions.
12
+
13
+ Sketch (stateless handler):
14
+
15
+ ```ts
16
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
+ import { createMcpHandler } from 'agents/mcp';
18
+
19
+ const server = new McpServer({ name: 'my-mcp', version: '0.0.0' });
20
+ server.registerTool('ping', { description: 'ping', inputSchema: {} }, async () => ({
21
+ content: [{ type: 'text', text: 'pong' }],
22
+ }));
23
+ const mcp = createMcpHandler(server);
24
+
25
+ export default {
26
+ fetch(request: Request, env: unknown, ctx: ExecutionContext) {
27
+ return new URL(request.url).pathname === '/mcp'
28
+ ? mcp(request, env, ctx)
29
+ : new Response('connect at /mcp', { status: 404 });
30
+ },
31
+ };
32
+ ```
@@ -0,0 +1,5 @@
1
+ # `_template-next` (placeholder)
2
+
3
+ Lane template for **Next.js on Vercel** with Supabase — verify mode `api + playwright`.
4
+
5
+ > **Phase 0:** placeholder only. Real template content arrives when the `next` lane is exercised (HeistMind migration, **Phase 9** — greenlight-v1.md §16). Materialized into a tool by `greenlight add` / `greenlight adopt`.