great-cto 2.3.4 → 2.5.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/dist/main.js CHANGED
@@ -83,6 +83,18 @@ function parseArgs(argv) {
83
83
  args.command = "scan";
84
84
  else if (a === "list-rules")
85
85
  args.command = "list-rules";
86
+ else if (a === "ci")
87
+ args.command = "ci";
88
+ else if (a === "mcp")
89
+ args.command = "mcp";
90
+ else if (a === "adapt")
91
+ args.command = "adapt";
92
+ else if (a === "serve")
93
+ args.command = "serve";
94
+ else if (a === "webhook")
95
+ args.command = "webhook";
96
+ else if (a === "report")
97
+ args.command = "report";
86
98
  else if (a.startsWith("--dir="))
87
99
  args.dir = a.slice("--dir=".length);
88
100
  else if (a === "--dir")
@@ -316,6 +328,10 @@ ${bold("Usage:")}
316
328
  npx great-cto register [--dir PATH]
317
329
  npx great-cto scan [path] [--severity LVL] [--scanner NAME] [--sarif FILE]
318
330
  npx great-cto list-rules
331
+ npx great-cto ci [path] [--fail-on LVL] [--sarif F] [--junit F]
332
+ npx great-cto mcp [--sse --port N]
333
+ npx great-cto adapt --platform [claude|codex|cursor|aider|continue|all]
334
+ npx great-cto serve [--port 3142]
319
335
  npx great-cto help
320
336
  npx great-cto version
321
337
 
@@ -338,6 +354,32 @@ ${bold("Scan (AI-security):")}
338
354
  great-cto list-rules Print rule catalog
339
355
  ${dim("(exits 1 if findings ≥ severity threshold; CI-friendly)")}
340
356
 
357
+ ${bold("CI gate:")}
358
+ great-cto ci Single-command CI gate (scan + archetype check)
359
+ great-cto ci --fail-on critical Exit 1 only on critical findings (default)
360
+ great-cto ci --sarif out.sarif Emit SARIF (uploadable to GitHub Security)
361
+ great-cto ci --junit out.xml Emit JUnit XML for test reporters
362
+ ${dim("(auto-detects \$GITHUB_ACTIONS → emits ::error:: annotations)")}
363
+
364
+ ${bold("MCP server (cross-platform):")}
365
+ great-cto mcp Stdio MCP server — works in Claude Desktop /
366
+ Cursor / Continue / any MCP host
367
+ great-cto mcp --sse --port 8765 SSE mode for remote / multi-client (TODO v2.5)
368
+ ${dim("Tools exposed: scan, list_rules, detect_archetype, estimate_cost, query_decisions")}
369
+
370
+ ${bold("Platform adapter (multi-tool support):")}
371
+ great-cto adapt --platform claude Generate AGENTS.md + CLAUDE.md
372
+ great-cto adapt --platform codex Generate AGENTS.md (OpenAI Codex CLI)
373
+ great-cto adapt --platform cursor Generate .cursorrules + .cursor/rules/*.mdc
374
+ great-cto adapt --platform aider Generate .aider.conf.yml + CONVENTIONS.md
375
+ great-cto adapt --platform continue Generate .continue/rules.md
376
+ great-cto adapt --platform all All of the above
377
+ ${dim("Idempotent — re-run after editing .great_cto/PROJECT.md")}
378
+
379
+ ${bold("Webhook server (preview):")}
380
+ great-cto serve --port 3142 Webhook receiver (logs to ~/.great_cto/webhook-events.log)
381
+ ${dim("Endpoints: POST /webhook/github, POST /webhook/generic, GET /events, GET /healthz")}
382
+
341
383
  ${bold("Options:")}
342
384
  -y, --yes Skip confirmation prompts (non-interactive)
343
385
  --dry-run Show what would be done without doing it
@@ -732,6 +774,99 @@ async function main() {
732
774
  process.exit(1);
733
775
  }
734
776
  }
777
+ if (args.command === "ci") {
778
+ try {
779
+ const { runCi, parseCiArgs } = await import("./ci.js");
780
+ const code = await runCi(parseCiArgs(rawArgv));
781
+ process.exit(code);
782
+ }
783
+ catch (e) {
784
+ error(e.message);
785
+ process.exit(2);
786
+ }
787
+ }
788
+ if (args.command === "mcp") {
789
+ try {
790
+ const { runMcp } = await import("./mcp.js");
791
+ const sse = rawArgv.includes("--sse");
792
+ const portArg = rawArgv.indexOf("--port");
793
+ const port = portArg >= 0 ? parseInt(rawArgv[portArg + 1] ?? "8765", 10) : 8765;
794
+ const code = await runMcp({ mode: sse ? "sse" : "stdio", port, version: getCliVersion() });
795
+ process.exit(code);
796
+ }
797
+ catch (e) {
798
+ error(e.message);
799
+ process.exit(2);
800
+ }
801
+ }
802
+ if (args.command === "adapt") {
803
+ try {
804
+ const { runAdapt } = await import("./adapt.js");
805
+ const platArg = rawArgv.indexOf("--platform");
806
+ const platform = (platArg >= 0 ? rawArgv[platArg + 1] : "all");
807
+ const valid = ["claude", "codex", "cursor", "aider", "continue", "all"];
808
+ if (!valid.includes(platform)) {
809
+ error(`adapt: --platform must be one of ${valid.join(", ")} (got: ${platform})`);
810
+ process.exit(2);
811
+ }
812
+ const code = await runAdapt({
813
+ platform,
814
+ dryRun: rawArgv.includes("--dry-run"),
815
+ cwd: args.dir,
816
+ });
817
+ process.exit(code);
818
+ }
819
+ catch (e) {
820
+ error(e.message);
821
+ process.exit(2);
822
+ }
823
+ }
824
+ if (args.command === "serve") {
825
+ try {
826
+ const { runServe } = await import("./serve.js");
827
+ const code = await runServe({
828
+ port: args.boardPort === 3141 ? 3142 : args.boardPort,
829
+ noLog: rawArgv.includes("--no-log"),
830
+ insecure: rawArgv.includes("--insecure"),
831
+ });
832
+ process.exit(code);
833
+ }
834
+ catch (e) {
835
+ error(e.message);
836
+ process.exit(2);
837
+ }
838
+ }
839
+ if (args.command === "webhook") {
840
+ try {
841
+ const { runWebhookCli, parseWebhookArgs } = await import("./webhook-cli.js");
842
+ const parsed = parseWebhookArgs(rawArgv);
843
+ if (!parsed) {
844
+ error("usage: great-cto webhook list | add-incoming <name> --secret <s> | add-outgoing <name> --url <u> --format <f> --triggers <t1,t2> | remove <name> | test <name>");
845
+ process.exit(2);
846
+ }
847
+ const code = await runWebhookCli(parsed);
848
+ process.exit(code);
849
+ }
850
+ catch (e) {
851
+ error(e.message);
852
+ process.exit(2);
853
+ }
854
+ }
855
+ if (args.command === "report") {
856
+ try {
857
+ const { runReport, parseReportArgs } = await import("./report.js");
858
+ const parsed = parseReportArgs(rawArgv, args.dir);
859
+ if (!parsed) {
860
+ process.exit(2);
861
+ }
862
+ const code = await runReport(parsed);
863
+ process.exit(code);
864
+ }
865
+ catch (e) {
866
+ error(e.message);
867
+ process.exit(2);
868
+ }
869
+ }
735
870
  if (args.command === "version") {
736
871
  // Version resolved in index.mjs or from package.json at runtime
737
872
  try {
package/dist/mcp.js ADDED
@@ -0,0 +1,355 @@
1
+ // great-cto mcp — Model Context Protocol server.
2
+ //
3
+ // Exposes great_cto's core capabilities as MCP tools so any MCP-compatible
4
+ // host (Claude Desktop, Cursor, Continue, Codex CLI via MCP, custom agents)
5
+ // can call them. This is the single biggest cross-platform multiplier — one
6
+ // implementation, all clients.
7
+ //
8
+ // Modes:
9
+ // stdio (default) — Claude Desktop / Cursor / Continue spawn this as subprocess
10
+ // sse — long-running HTTP/SSE for remote / multi-client access
11
+ //
12
+ // Tools exposed:
13
+ // scan OWASP LLM Top 10 + 24 rules → findings
14
+ // list_rules full rule catalogue
15
+ // detect_archetype archetype + compliance for a path
16
+ // estimate_cost LLM/human time for a task
17
+ // query_decisions search ~/.great_cto/decisions.md
18
+ //
19
+ // Protocol: minimal MCP 2024-11-05 implementation. We hand-roll because
20
+ // adding @modelcontextprotocol/sdk would balloon install size for what is
21
+ // fundamentally a few JSON messages over stdio.
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { join, resolve } from "node:path";
25
+ const PROTOCOL_VERSION = "2024-11-05";
26
+ const SERVER_INFO = {
27
+ name: "great-cto",
28
+ version: "", // populated at startup
29
+ };
30
+ // ── Tool implementations ────────────────────────────────────────────────────
31
+ async function toolScan(args) {
32
+ const { scan } = await import("./agentshield/scanner.js");
33
+ const path = args.path ?? ".";
34
+ const report = scan(resolve(path), {
35
+ minSeverity: (args.severity ?? "info"),
36
+ scanners: args.scanner,
37
+ });
38
+ return {
39
+ files_scanned: report.filesScanned,
40
+ duration_ms: report.durationMs,
41
+ findings: report.findings.map((f) => ({
42
+ rule_id: f.rule.id,
43
+ severity: f.rule.severity,
44
+ title: f.rule.title,
45
+ owasp: f.rule.owasp,
46
+ file: f.location.file,
47
+ line: f.location.line,
48
+ snippet: f.location.snippet,
49
+ })),
50
+ };
51
+ }
52
+ async function toolListRules() {
53
+ const { loadRules } = await import("./agentshield/rules-loader.js");
54
+ const rules = loadRules();
55
+ return {
56
+ count: rules.length,
57
+ rules: rules.map((r) => ({
58
+ id: r.id,
59
+ severity: r.severity,
60
+ scanner: r.scanner,
61
+ title: r.title,
62
+ owasp: r.owasp ?? null,
63
+ })),
64
+ };
65
+ }
66
+ async function toolDetectArchetype(args) {
67
+ const { detect } = await import("./detect.js");
68
+ const { pickArchetype, suggestCompliance } = await import("./archetypes.js");
69
+ const cwd = resolve(args.path ?? ".");
70
+ const detected = await detect(cwd);
71
+ const result = pickArchetype(detected);
72
+ const compliance = suggestCompliance(detected, result.primary);
73
+ return {
74
+ archetype: result.primary,
75
+ confidence: result.confidence,
76
+ rationale: result.rationale,
77
+ alternatives: result.alternatives,
78
+ compliance,
79
+ };
80
+ }
81
+ async function toolEstimateCost(args) {
82
+ // Rough heuristic — for production, agents call /cost feature directly.
83
+ // This is the LLM-host-friendly summary.
84
+ const scale = args.scale ?? "standard";
85
+ const minutesByScale = {
86
+ quick: 15,
87
+ standard: 45,
88
+ deep: 90,
89
+ };
90
+ const llmRatePerHr = 0.02;
91
+ const humanRatePerHr = 150;
92
+ const minutes = minutesByScale[scale] ?? 45;
93
+ const llmUsd = +(minutes / 60 * llmRatePerHr).toFixed(4);
94
+ const humanUsd = +(minutes / 60 * humanRatePerHr).toFixed(2);
95
+ const humanDays = +(minutes / 60 / 6).toFixed(1); // 6 productive hrs / day
96
+ return {
97
+ task: args.task_description ?? "",
98
+ archetype: args.archetype ?? "unknown",
99
+ scale,
100
+ llm_agent: { wall_clock_min: minutes, cost_usd: llmUsd },
101
+ human_team: { working_days: humanDays, cost_usd: humanUsd },
102
+ savings_x: Math.round(humanUsd / Math.max(llmUsd, 0.0001)),
103
+ };
104
+ }
105
+ function toolQueryDecisions(args) {
106
+ const decisionsPath = join(homedir(), ".great_cto", "decisions.md");
107
+ if (!existsSync(decisionsPath)) {
108
+ return { count: 0, results: [], note: "No ~/.great_cto/decisions.md found." };
109
+ }
110
+ const text = readFileSync(decisionsPath, "utf8");
111
+ const entries = text.split(/\n---\n/).filter(s => s.trim());
112
+ const q = (args.query ?? "").toLowerCase();
113
+ const limit = args.limit ?? 10;
114
+ const matches = q
115
+ ? entries.filter(e => e.toLowerCase().includes(q))
116
+ : entries.slice(-limit);
117
+ return {
118
+ count: matches.length,
119
+ total_decisions: entries.length,
120
+ results: matches.slice(0, limit).map(e => {
121
+ const titleMatch = e.match(/^##\s+(.+)$/m);
122
+ return {
123
+ title: titleMatch?.[1] ?? "(untitled)",
124
+ excerpt: e.slice(0, 400),
125
+ };
126
+ }),
127
+ };
128
+ }
129
+ // ── Tool dispatch table ────────────────────────────────────────────────────
130
+ const TOOLS = [
131
+ {
132
+ name: "scan",
133
+ description: "Scan code for AI/LLM-specific security issues (OWASP LLM Top 10, 24 rules). Returns findings with severity, file, line, OWASP mapping.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ path: { type: "string", description: "File or directory to scan (default: cwd)" },
138
+ severity: {
139
+ type: "string",
140
+ enum: ["info", "low", "medium", "high", "critical"],
141
+ description: "Minimum severity to report",
142
+ },
143
+ scanner: {
144
+ type: "array",
145
+ items: {
146
+ type: "string",
147
+ enum: ["prompt-injection", "secrets-in-prompts", "ssrf-in-tools", "rag-poisoning", "cost-runaway"],
148
+ },
149
+ description: "Limit to specific scanner categories",
150
+ },
151
+ },
152
+ },
153
+ handler: toolScan,
154
+ },
155
+ {
156
+ name: "list_rules",
157
+ description: "List all 24 AgentShield security rules with severity and OWASP LLM Top 10 mapping.",
158
+ inputSchema: { type: "object", properties: {} },
159
+ handler: toolListRules,
160
+ },
161
+ {
162
+ name: "detect_archetype",
163
+ description: "Detect the project archetype (one of 25: fintech, healthcare, commerce, agent-product, mlops, edtech, gov-public, insurance, ...) and the compliance gates that apply.",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: {
167
+ path: { type: "string", description: "Project root (default: cwd)" },
168
+ },
169
+ },
170
+ handler: toolDetectArchetype,
171
+ },
172
+ {
173
+ name: "estimate_cost",
174
+ description: "Estimate LLM-agent wall-clock time and cost vs human-team equivalent for a task. Returns LLM/human cost and savings ratio (~7500×).",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ task_description: { type: "string" },
179
+ archetype: { type: "string" },
180
+ scale: { type: "string", enum: ["quick", "standard", "deep"] },
181
+ },
182
+ },
183
+ handler: toolEstimateCost,
184
+ },
185
+ {
186
+ name: "query_decisions",
187
+ description: "Search the cross-project ADR log at ~/.great_cto/decisions.md. Returns matching decisions for the given query string.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ query: { type: "string", description: "Search string (case-insensitive). Empty = recent decisions." },
192
+ limit: { type: "number", description: "Max results (default 10)" },
193
+ },
194
+ },
195
+ handler: toolQueryDecisions,
196
+ },
197
+ ];
198
+ // ── JSON-RPC handler ───────────────────────────────────────────────────────
199
+ async function handle(req) {
200
+ const { method, id = null, params } = req;
201
+ // Notifications (no id) get no response
202
+ const isNotification = id === null || id === undefined;
203
+ const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: id, result };
204
+ const fail = (code, message, data) => isNotification ? null : { jsonrpc: "2.0", id: id, error: { code, message, data } };
205
+ try {
206
+ switch (method) {
207
+ case "initialize":
208
+ return reply({
209
+ protocolVersion: PROTOCOL_VERSION,
210
+ capabilities: { tools: {} },
211
+ serverInfo: SERVER_INFO,
212
+ });
213
+ case "initialized":
214
+ case "notifications/initialized":
215
+ return null;
216
+ case "tools/list":
217
+ return reply({
218
+ tools: TOOLS.map(t => ({
219
+ name: t.name,
220
+ description: t.description,
221
+ inputSchema: t.inputSchema,
222
+ })),
223
+ });
224
+ case "tools/call": {
225
+ const name = params?.name;
226
+ const args = params?.arguments ?? {};
227
+ const tool = TOOLS.find(t => t.name === name);
228
+ if (!tool)
229
+ return fail(-32601, `Unknown tool: ${name}`);
230
+ const result = await tool.handler(args);
231
+ return reply({
232
+ content: [
233
+ { type: "text", text: JSON.stringify(result, null, 2) },
234
+ ],
235
+ isError: false,
236
+ });
237
+ }
238
+ case "ping":
239
+ return reply({});
240
+ default:
241
+ return fail(-32601, `Method not found: ${method}`);
242
+ }
243
+ }
244
+ catch (e) {
245
+ return fail(-32603, `Internal error: ${e.message}`);
246
+ }
247
+ }
248
+ // ── SSE transport ──────────────────────────────────────────────────────────
249
+ async function runSse(port, version) {
250
+ const { createServer } = await import("node:http");
251
+ // Each connection gets a unique session id and an open SSE stream.
252
+ // Inbound JSON-RPC arrives via POST /message?sessionId=<id>; responses are
253
+ // pushed back over the SSE stream. This matches the standard MCP SSE
254
+ // transport (https://spec.modelcontextprotocol.io/specification/transports).
255
+ const sessions = new Map();
256
+ const server = createServer(async (req, res) => {
257
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
258
+ if (req.method === "GET" && url.pathname === "/sse") {
259
+ const sessionId = `s-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
260
+ res.writeHead(200, {
261
+ "Content-Type": "text/event-stream",
262
+ "Cache-Control": "no-cache, no-transform",
263
+ Connection: "keep-alive",
264
+ "X-Accel-Buffering": "no",
265
+ });
266
+ // Initial endpoint event — tells client where to POST messages
267
+ res.write(`event: endpoint\ndata: /message?sessionId=${sessionId}\n\n`);
268
+ sessions.set(sessionId, res);
269
+ req.on("close", () => sessions.delete(sessionId));
270
+ return;
271
+ }
272
+ if (req.method === "POST" && url.pathname === "/message") {
273
+ const sessionId = url.searchParams.get("sessionId") ?? "";
274
+ const sse = sessions.get(sessionId);
275
+ if (!sse) {
276
+ res.writeHead(404, { "Content-Type": "application/json" });
277
+ res.end(JSON.stringify({ error: "unknown sessionId" }));
278
+ return;
279
+ }
280
+ const chunks = [];
281
+ req.on("data", c => chunks.push(Buffer.from(c)));
282
+ req.on("end", async () => {
283
+ const body = Buffer.concat(chunks).toString("utf8");
284
+ try {
285
+ const reqJson = JSON.parse(body);
286
+ const reply = await handle(reqJson);
287
+ if (reply) {
288
+ sse.write(`event: message\ndata: ${JSON.stringify(reply)}\n\n`);
289
+ }
290
+ res.writeHead(202).end();
291
+ }
292
+ catch (e) {
293
+ res.writeHead(400, { "Content-Type": "application/json" });
294
+ res.end(JSON.stringify({ error: e.message }));
295
+ }
296
+ });
297
+ return;
298
+ }
299
+ if (req.method === "GET" && url.pathname === "/healthz") {
300
+ res.writeHead(200, { "Content-Type": "application/json" });
301
+ res.end(JSON.stringify({ ok: true, version, sessions: sessions.size, transport: "sse" }));
302
+ return;
303
+ }
304
+ res.writeHead(404).end();
305
+ });
306
+ return new Promise(resolve => {
307
+ server.listen(port, "127.0.0.1", () => {
308
+ process.stderr.write(`great-cto mcp v${version} (sse) → http://localhost:${port}/sse\n`);
309
+ process.stderr.write(` GET /sse open event stream\n`);
310
+ process.stderr.write(` POST /message?sessionId=... send JSON-RPC\n`);
311
+ process.stderr.write(` GET /healthz liveness\n`);
312
+ });
313
+ process.on("SIGINT", () => { server.close(); resolve(0); });
314
+ process.on("SIGTERM", () => { server.close(); resolve(0); });
315
+ });
316
+ }
317
+ // ── stdio transport ────────────────────────────────────────────────────────
318
+ async function runStdio() {
319
+ // Read newline-delimited JSON from stdin, write to stdout. This is the
320
+ // standard MCP stdio transport.
321
+ let buffer = "";
322
+ process.stdin.setEncoding("utf8");
323
+ process.stdin.on("data", async (chunk) => {
324
+ buffer += chunk;
325
+ let nl;
326
+ while ((nl = buffer.indexOf("\n")) >= 0) {
327
+ const line = buffer.slice(0, nl).trim();
328
+ buffer = buffer.slice(nl + 1);
329
+ if (!line)
330
+ continue;
331
+ try {
332
+ const req = JSON.parse(line);
333
+ const res = await handle(req);
334
+ if (res)
335
+ process.stdout.write(JSON.stringify(res) + "\n");
336
+ }
337
+ catch (e) {
338
+ process.stderr.write(`mcp: parse error: ${e.message}\n`);
339
+ }
340
+ }
341
+ });
342
+ return new Promise(resolve => {
343
+ process.stdin.on("end", () => resolve(0));
344
+ process.stdin.on("error", () => resolve(2));
345
+ });
346
+ }
347
+ export async function runMcp(args) {
348
+ SERVER_INFO.version = args.version;
349
+ if (args.mode === "sse") {
350
+ return runSse(args.port, args.version);
351
+ }
352
+ // Notify clients we're ready (some hosts log this)
353
+ process.stderr.write(`great-cto mcp v${args.version} (stdio) — ${TOOLS.length} tools\n`);
354
+ return runStdio();
355
+ }