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 +38 -0
- package/dist/main.js +108 -2
- package/dist/webhook-dispatch.js +70 -1
- package/package.json +2 -2
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 {
|
|
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);
|
package/dist/webhook-dispatch.js
CHANGED
|
@@ -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 => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
-
|
|
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.
|
|
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
|
+
}
|