great-cto 2.8.6 → 2.9.1

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/adapt.js CHANGED
@@ -77,6 +77,44 @@ Configure Claude Code to launch:
77
77
  npx great-cto mcp
78
78
  \`\`\`
79
79
 
80
+ ## Subagent routing (do not default to general-purpose)
81
+
82
+ When dispatching the **Agent** tool, pick the right \`subagent_type\` based on
83
+ what's being changed. \`general-purpose\` is a fallback, **not** the default —
84
+ using it for pattern-matched work silently skips specialist review.
85
+
86
+ | Trigger (file pattern OR topic in prompt) | Use \`subagent_type:\` |
87
+ |---|---|
88
+ | \`migrations/\`, \`schema.sql\`, ORM migration files | \`db-migration-reviewer\` |
89
+ | \`auth/\`, OAuth/SAML/JWT, login flow, password reset | \`security-officer\` |
90
+ | Payment endpoints, \`stripe.\`, webhooks, refund flow | \`pci-reviewer\` |
91
+ | Prompts in \`prompts/\`, RAG, tool definitions, LLM-facing strings | \`ai-security-reviewer\` |
92
+ | Eval suites, golden citation tests, prompt regression | \`ai-eval-engineer\` |
93
+ | Play Store, App Store, iOS/Android release | \`mobile-store-reviewer\` |
94
+ | API contract: OpenAPI, GraphQL schema, webhook signatures | \`api-platform-reviewer\` |
95
+ | Voice/IVR/telephony, Twilio, recording-consent, TCPA | \`voice-ai-reviewer\` |
96
+ | Clinical / SaMD / FDA / FHIR / PHI | \`ai-clinical-reviewer\`, \`fda-reviewer\` |
97
+ | Lending, ECOA, FCRA, NMLS, adverse action | \`lending-credit-reviewer\` |
98
+ | HR-AI, hiring, AEDT, resume screening | \`hr-ai-reviewer\` |
99
+ | Infra-as-code: Terraform / Helm / CDK / Pulumi | \`infra-reviewer\` |
100
+ | Performance regression, hot path, p99 budgets | \`performance-engineer\` |
101
+ | Browser extension manifest, MV3 permissions | \`web-store-reviewer\` |
102
+ | New feature implementation (TDD) | \`senior-dev\` |
103
+ | Architecture decisions, ADRs, scaling questions | \`architect\` |
104
+ | Planning a feature into tasks (Beads), dependency graph | \`pm\` |
105
+ | QA report after impl, coverage + acceptance | \`qa-engineer\` |
106
+ | Deploy / canary / rollback / SLO | \`devops\` |
107
+ | Production incident triage, P0 postmortem | \`l3-support\` |
108
+ | Pattern extraction from session, retro to \`lessons.md\` | \`continuous-learner\` |
109
+
110
+ Rule of thumb: if a file pattern or topic matches above, use that specialist
111
+ **before** falling back to \`general-purpose\`. Specialist agents catch
112
+ domain-specific bugs (race conditions in migrations, prompt-injection vectors,
113
+ TCPA violations) that a generalist won't flag.
114
+
115
+ When in doubt, run two agents in parallel: the specialist + general-purpose,
116
+ and reconcile their outputs.
117
+
80
118
  ## Style + conventions
81
119
 
82
120
  - Tests **before** implementation (RED → GREEN → REFACTOR). Coverage 80%+ default.
package/dist/main.js CHANGED
@@ -10,16 +10,18 @@
10
10
  // 7. bootstrap .great_cto/PROJECT.md
11
11
  // 8. print next steps
12
12
  import { resolve } from "node:path";
13
- import { banner, bold, cyan, dim, error, green, log, step, warn, yellow, confirm } from "./ui.js";
13
+ import { banner, bold, cyan, dim, error, gray, green, log, step, warn, yellow, confirm } from "./ui.js";
14
14
  import { detect } from "./detect.js";
15
15
  import { pickArchetype, suggestCompliance } from "./archetypes.js";
16
16
  import { install, findInstalledVersions } from "./installer.js";
17
17
  import { enableGreatCto } from "./settings.js";
18
18
  import { bootstrap } from "./bootstrap.js";
19
19
  import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
20
- import { readFileSync } from "node:fs";
20
+ import { isTelemetryEnabled, sendInstallPing, telemetrySubcommand, computeAnonId, } from "./telemetry.js";
21
+ import { readFileSync, writeFileSync, mkdirSync, existsSync as fsExistsSync } from "node:fs";
21
22
  import { dirname, join } from "node:path";
22
23
  import { fileURLToPath } from "node:url";
24
+ import { homedir } from "node:os";
23
25
  function getCliVersion() {
24
26
  try {
25
27
  const here = dirname(fileURLToPath(import.meta.url));
@@ -94,6 +96,8 @@ function parseArgs(argv) {
94
96
  args.command = "webhook";
95
97
  else if (a === "report")
96
98
  args.command = "report";
99
+ else if (a === "telemetry")
100
+ args.command = "telemetry";
97
101
  // Slash-commands surfaced as CLI subcommands so users get a clear hint
98
102
  // instead of a confusing usage error. These work only in the chat plugin.
99
103
  else if (a === "start" || a === "audit" || a === "inbox" || a === "digest" ||
@@ -394,6 +398,13 @@ ${bold("Claude Code adapter:")}
394
398
  great-cto adapt --dry-run Preview what would be written
395
399
  ${dim("Idempotent — re-run after editing .great_cto/PROJECT.md")}
396
400
 
401
+ ${bold("Telemetry (opt-IN, off by default):")}
402
+ great-cto telemetry status Show current state + endpoint + anon_id
403
+ great-cto telemetry on Enable anonymous usage events
404
+ great-cto telemetry off Disable (also: DO_NOT_TRACK=1)
405
+ great-cto telemetry whoami Print your anon_id (8 hex chars)
406
+ ${dim("Privacy: docs/PRIVACY.md · no code, no repo names, no PII")}
407
+
397
408
  ${bold("Webhook server (preview):")}
398
409
  great-cto serve --port 3142 Webhook receiver (logs to ~/.great_cto/webhook-events.log)
399
410
  ${dim("Endpoints: POST /webhook/github, POST /webhook/generic, GET /events, GET /healthz")}
@@ -750,6 +761,19 @@ async function runInit(args) {
750
761
  log("");
751
762
  log(green(bold("✓ great_cto is ready.")));
752
763
  log("");
764
+ // ── 6. opt-IN telemetry prompt ───────────────────────────
765
+ // Aggressive opt-IN promo (Option A from telemetry strategy).
766
+ // Shows up only when interactive + not CI + not already decided + DO_NOT_TRACK unset.
767
+ await promoteTelemetryOptIn({ archetype: archetype, cliVersion: getCliVersion(), yes: args.yes });
768
+ // If telemetry is enabled (either via prior opt-in or env var), fire a fresh
769
+ // install-ping so re-runs of `init` (after upgrades) count toward WAU/MAU.
770
+ if (isTelemetryEnabled()) {
771
+ await sendInstallPing({
772
+ cliVersion: getCliVersion(),
773
+ archetype: archetype,
774
+ consent: true,
775
+ }).catch(() => { });
776
+ }
753
777
  log(bold("Next steps:"));
754
778
  log(` 1. ${dim("Restart Claude Code to pick up the plugin.")}`);
755
779
  log(` 2. ${dim("Edit")} ${cyan(".great_cto/PROJECT.md")} ${dim("to refine goals and compliance.")}`);
@@ -761,6 +785,82 @@ async function runInit(args) {
761
785
  log("");
762
786
  return 0;
763
787
  }
788
+ // ── Telemetry opt-IN prompt ────────────────────────────────────────────────
789
+ // Shows after a successful init when interactive. NOT shown if:
790
+ // - --yes / -y was used (skip-confirmation mode)
791
+ // - DO_NOT_TRACK=1 (respect honest signal)
792
+ // - Already opted in or explicitly opted out (no nag)
793
+ // - CI environment
794
+ // - Non-TTY stdin (piped install)
795
+ async function promoteTelemetryOptIn(opts) {
796
+ // Skip prompts in non-interactive mode.
797
+ if (opts.yes)
798
+ return;
799
+ if (!process.stdin.isTTY)
800
+ return;
801
+ if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true")
802
+ return;
803
+ if (process.env.CI || process.env.GITHUB_ACTIONS)
804
+ return;
805
+ // If user already made a decision (either way), respect it — don't re-ask.
806
+ const cfgFile = join(homedir(), ".great_cto", "telemetry.json");
807
+ if (fsExistsSync(cfgFile)) {
808
+ try {
809
+ const cfg = JSON.parse(readFileSync(cfgFile, "utf8"));
810
+ if (cfg.enabled === true || cfg.enabled === false)
811
+ return;
812
+ }
813
+ catch { /* malformed — ask again */ }
814
+ }
815
+ // The honest, brand-aligned ask.
816
+ log(dim("─".repeat(60)));
817
+ log(bold("Help great_cto learn from how you use it?"));
818
+ log("");
819
+ log(dim("Anonymous usage data (default: off). Helps cross-project"));
820
+ log(dim("lessons promote to a global pattern library."));
821
+ log("");
822
+ log(dim("Here is exactly what would be sent — one event per command:"));
823
+ log("");
824
+ log(gray(` {`));
825
+ log(gray(` "version": "${opts.cliVersion}",`));
826
+ log(gray(` "command": "init",`));
827
+ log(gray(` "archetype": "${opts.archetype}",`));
828
+ log(gray(` "node": "${process.version.replace(/^v/, "")}",`));
829
+ log(gray(` "os": "${process.platform}",`));
830
+ log(gray(` "exit_code": 0,`));
831
+ log(gray(` "duration_ms": 1234,`));
832
+ log(gray(` "anon_id": "${computeAnonId()}" ${dim("// 8 hex chars, not reversible")}`));
833
+ log(gray(` }`));
834
+ log("");
835
+ log(dim("No code, no repo names, no file paths, no PII."));
836
+ log(dim("Toggle anytime: " + cyan("npx great-cto telemetry off")));
837
+ log(dim("Privacy: " + cyan("github.com/avelikiy/great_cto/blob/main/docs/PRIVACY.md")));
838
+ log("");
839
+ const yes = await confirm(bold("Enable anonymous telemetry?"), false);
840
+ log("");
841
+ // Persist the decision either way so we never re-ask.
842
+ try {
843
+ const dir = join(homedir(), ".great_cto");
844
+ mkdirSync(dir, { recursive: true });
845
+ writeFileSync(join(dir, "telemetry.json"), JSON.stringify({ enabled: yes, decided_at: new Date().toISOString() }, null, 2) + "\n");
846
+ }
847
+ catch { /* best-effort */ }
848
+ if (yes) {
849
+ log(green(`✓ Telemetry enabled. Thank you.`));
850
+ log(dim(` See your anon_id anytime: ${cyan("npx great-cto telemetry whoami")}`));
851
+ log("");
852
+ // Send the first event — the install-ping itself.
853
+ await sendInstallPing({
854
+ cliVersion: opts.cliVersion,
855
+ archetype: opts.archetype,
856
+ consent: true,
857
+ });
858
+ }
859
+ else {
860
+ log(dim(`Telemetry off. ${cyan("npx great-cto telemetry on")} to change later.`));
861
+ log("");
862
+ }
863
+ }
764
864
  async function main() {
765
865
  const rawArgv = process.argv.slice(2);
766
866
  const args = parseArgs(rawArgv);
@@ -775,6 +875,12 @@ async function main() {
775
875
  log(`Run ${cyan("great-cto --help")} for usage.`);
776
876
  process.exit(2);
777
877
  }
878
+ if (args.command === "telemetry") {
879
+ const sub = rawArgv[rawArgv.indexOf("telemetry") + 1];
880
+ const { exitCode, output } = telemetrySubcommand(sub);
881
+ process.stdout.write(output);
882
+ process.exit(exitCode);
883
+ }
778
884
  if (args.command === "scan") {
779
885
  try {
780
886
  const code = await runScan(args, rawArgv);
@@ -11,6 +11,7 @@
11
11
  // - slack: posts as Slack incoming-webhook JSON ({text, blocks?})
12
12
  // - discord: Discord webhook JSON ({content, embeds?})
13
13
  // - pagerduty: Events API v2 ({routing_key, event_action, payload})
14
+ // - resend: HTML email via Resend API ({from, to, subject, html})
14
15
  // - generic: arbitrary JSON POST
15
16
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
16
17
  import { homedir } from "node:os";
@@ -39,6 +40,62 @@ function formatDiscord(ev) {
39
40
  embeds: ev.body ? [{ description: ev.body, color }] : undefined,
40
41
  };
41
42
  }
43
+ function emojiForLevel(level) {
44
+ return level === "critical" ? "🚨"
45
+ : level === "error" ? "❌"
46
+ : level === "warning" ? "⏸️"
47
+ : "ℹ️";
48
+ }
49
+ function escapeHtml(s) {
50
+ return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
51
+ }
52
+ /**
53
+ * Build the Resend API payload — POSTed to https://api.resend.com/emails
54
+ * with Authorization: Bearer <api_key>.
55
+ *
56
+ * Body fields from meta:
57
+ * - project, link (CTA URL), action (CTA label)
58
+ * - kv: Record<string,string> for the metric table
59
+ * - severity (optional override for color)
60
+ */
61
+ function formatResend(ev, hook) {
62
+ const accent = ev.level === "critical" ? "#dc2626"
63
+ : ev.level === "error" ? "#ea580c"
64
+ : ev.level === "warning" ? "#d97706"
65
+ : "#00d97e";
66
+ const emoji = emojiForLevel(ev.level);
67
+ const meta = (ev.meta ?? {});
68
+ const project = typeof meta.project === "string" ? meta.project : "great_cto";
69
+ const link = typeof meta.link === "string" ? meta.link : "";
70
+ const action = typeof meta.action === "string" ? meta.action : "View in board";
71
+ const kv = (meta.kv ?? {});
72
+ const tableRows = Object.entries(kv)
73
+ .map(([k, v]) => `<tr><td style="padding:6px 12px;color:#6b7280;font-size:12px;font-family:ui-monospace,monospace;text-transform:uppercase;letter-spacing:.05em">${escapeHtml(k)}</td><td style="padding:6px 12px;color:#111827;font-size:14px;font-weight:500">${escapeHtml(v)}</td></tr>`)
74
+ .join("");
75
+ const html = `<!doctype html><html><body style="margin:0;background:#f9fafb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111827">
76
+ <div style="max-width:560px;margin:32px auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden">
77
+ <div style="padding:20px 24px;border-bottom:1px solid #e5e7eb;background:#0a0e0c;color:#ffffff">
78
+ <div style="font-size:11px;font-family:ui-monospace,monospace;letter-spacing:.1em;color:#9ca3af">${escapeHtml(project.toUpperCase())} · GREATCTO</div>
79
+ <div style="font-size:20px;font-weight:600;margin-top:6px;color:${accent}">${emoji} ${escapeHtml(ev.title)}</div>
80
+ </div>
81
+ ${ev.body ? `<div style="padding:20px 24px;font-size:14px;line-height:1.55;color:#374151">${escapeHtml(ev.body).replace(/\n/g, "<br>")}</div>` : ""}
82
+ ${tableRows ? `<table style="width:100%;border-top:1px solid #e5e7eb;border-collapse:collapse">${tableRows}</table>` : ""}
83
+ ${link ? `<div style="padding:24px;text-align:center;border-top:1px solid #e5e7eb">
84
+ <a href="${escapeHtml(link)}" style="display:inline-block;background:${accent};color:#ffffff;text-decoration:none;padding:10px 22px;border-radius:8px;font-weight:600;font-size:14px">${escapeHtml(action)}</a>
85
+ </div>` : ""}
86
+ <div style="padding:14px 24px;background:#f9fafb;font-size:11px;color:#9ca3af;font-family:ui-monospace,monospace">
87
+ Sent by great_cto · ${escapeHtml(ev.name)} · ${new Date().toISOString()}<br>
88
+ Unsubscribe: edit ~/.great_cto/webhooks.json or the Notifications tab in the board
89
+ </div>
90
+ </div></body></html>`;
91
+ const to = (hook.to ?? "").split(",").map(s => s.trim()).filter(Boolean);
92
+ return {
93
+ from: hook.from ?? "GreatCTO <notifications@greatcto.systems>",
94
+ to,
95
+ subject: `${emoji} ${ev.title}`,
96
+ html,
97
+ };
98
+ }
42
99
  function formatPagerDuty(ev, routingKey) {
43
100
  // PagerDuty Events API v2
44
101
  const severity = ev.level === "critical" ? "critical"
@@ -65,6 +122,7 @@ function buildPayload(hook, ev) {
65
122
  const key = hook.headers?.routing_key ?? "";
66
123
  return formatPagerDuty(ev, key);
67
124
  }
125
+ case "resend": return formatResend(ev, hook);
68
126
  case "generic":
69
127
  default: return { event: ev.name, ...ev };
70
128
  }
@@ -79,7 +137,18 @@ async function deliver(hook, ev, attempt = 0) {
79
137
  };
80
138
  // Don't leak routing_key as HTTP header — PagerDuty wants it in body
81
139
  delete headers.routing_key;
82
- const res = await fetch(hook.url, { method: "POST", headers, body });
140
+ // Resend: Bearer auth + URL override (config can leave url blank).
141
+ let url = hook.url;
142
+ if (hook.format === "resend") {
143
+ if (!hook.apiKey)
144
+ throw new Error("resend: apiKey is required");
145
+ if (!hook.to)
146
+ throw new Error("resend: 'to' email is required");
147
+ headers["Authorization"] = `Bearer ${hook.apiKey}`;
148
+ if (!url)
149
+ url = "https://api.resend.com/emails";
150
+ }
151
+ const res = await fetch(url, { method: "POST", headers, body });
83
152
  if (!res.ok) {
84
153
  throw new Error(`HTTP ${res.status} ${res.statusText}`);
85
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.8.6",
3
+ "version": "2.9.1",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -86,4 +86,4 @@
86
86
  "engines": {
87
87
  "node": ">=18.17.0"
88
88
  }
89
- }
89
+ }