great-cto 2.9.1 → 2.9.2

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
@@ -10,18 +10,16 @@
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, gray, green, log, step, warn, yellow, confirm } from "./ui.js";
13
+ import { banner, bold, cyan, dim, error, 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 { isTelemetryEnabled, sendInstallPing, telemetrySubcommand, computeAnonId, } from "./telemetry.js";
21
- import { readFileSync, writeFileSync, mkdirSync, existsSync as fsExistsSync } from "node:fs";
20
+ import { readFileSync } from "node:fs";
22
21
  import { dirname, join } from "node:path";
23
22
  import { fileURLToPath } from "node:url";
24
- import { homedir } from "node:os";
25
23
  function getCliVersion() {
26
24
  try {
27
25
  const here = dirname(fileURLToPath(import.meta.url));
@@ -96,8 +94,6 @@ function parseArgs(argv) {
96
94
  args.command = "webhook";
97
95
  else if (a === "report")
98
96
  args.command = "report";
99
- else if (a === "telemetry")
100
- args.command = "telemetry";
101
97
  // Slash-commands surfaced as CLI subcommands so users get a clear hint
102
98
  // instead of a confusing usage error. These work only in the chat plugin.
103
99
  else if (a === "start" || a === "audit" || a === "inbox" || a === "digest" ||
@@ -398,13 +394,6 @@ ${bold("Claude Code adapter:")}
398
394
  great-cto adapt --dry-run Preview what would be written
399
395
  ${dim("Idempotent — re-run after editing .great_cto/PROJECT.md")}
400
396
 
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
-
408
397
  ${bold("Webhook server (preview):")}
409
398
  great-cto serve --port 3142 Webhook receiver (logs to ~/.great_cto/webhook-events.log)
410
399
  ${dim("Endpoints: POST /webhook/github, POST /webhook/generic, GET /events, GET /healthz")}
@@ -761,19 +750,6 @@ async function runInit(args) {
761
750
  log("");
762
751
  log(green(bold("✓ great_cto is ready.")));
763
752
  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
- }
777
753
  log(bold("Next steps:"));
778
754
  log(` 1. ${dim("Restart Claude Code to pick up the plugin.")}`);
779
755
  log(` 2. ${dim("Edit")} ${cyan(".great_cto/PROJECT.md")} ${dim("to refine goals and compliance.")}`);
@@ -785,82 +761,6 @@ async function runInit(args) {
785
761
  log("");
786
762
  return 0;
787
763
  }
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
- }
864
764
  async function main() {
865
765
  const rawArgv = process.argv.slice(2);
866
766
  const args = parseArgs(rawArgv);
@@ -875,12 +775,6 @@ async function main() {
875
775
  log(`Run ${cyan("great-cto --help")} for usage.`);
876
776
  process.exit(2);
877
777
  }
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
- }
884
778
  if (args.command === "scan") {
885
779
  try {
886
780
  const code = await runScan(args, rawArgv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
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",
package/dist/telemetry.js DELETED
@@ -1,225 +0,0 @@
1
- // Anonymous opt-IN telemetry — default OFF.
2
- //
3
- // See docs/PRIVACY.md for the full policy. Short version:
4
- // - Default: disabled (opt-in)
5
- // - Honors DO_NOT_TRACK=1 (industry standard, https://consoledonottrack.com)
6
- // - Skipped automatically in CI environments
7
- // - No paths, no code, no PII — just {ts, version, command, archetype, node, os, exit, duration_ms, anon_id}
8
- // - anon_id is sha256(user@hostname) truncated to 8 hex chars; not reversible
9
- //
10
- // Opt-in (any one):
11
- // GREAT_CTO_TELEMETRY=on (env var)
12
- // ~/.great_cto/telemetry.json: { "enabled": true }
13
- // npx great-cto telemetry on
14
- //
15
- // Opt-out (overrides everything):
16
- // DO_NOT_TRACK=1 (highest priority)
17
- // GREAT_CTO_TELEMETRY=off
18
- // GREAT_CTO_DISABLE_TELEMETRY=1 (legacy alias from v2.x)
19
- // GREATCTO_NO_TELEMETRY=1 (legacy alias from v2.x)
20
- // ~/.great_cto/telemetry.json: { "enabled": false }
21
- //
22
- // Endpoint: https://telemetry.greatcto.systems/v1/event (Cloudflare Worker → D1)
23
- // Worker: workers/telemetry/index.ts
24
- // Schema v1: see docs/PRIVACY.md "What we collect"
25
- import * as fs from "node:fs";
26
- import * as path from "node:path";
27
- import * as os from "node:os";
28
- import * as crypto from "node:crypto";
29
- const TELEMETRY_ENDPOINT = process.env.GREAT_CTO_TELEMETRY_ENDPOINT
30
- || "https://great-cto-telemetry.alexander-velikiy.workers.dev/v1/event";
31
- // Note: workers.dev URL is the temporary default until telemetry.greatcto.systems
32
- // custom domain is bound. Override anytime with GREAT_CTO_TELEMETRY_ENDPOINT.
33
- const TELEMETRY_TIMEOUT_MS = 1000;
34
- // Allowlist — anything else is dropped client-side and server-side.
35
- const ALLOWED_COMMANDS = new Set([
36
- "init", "scan", "ci", "list-rules", "board", "register",
37
- "adapt", "mcp", "report", "serve", "webhook",
38
- "version", "help", "telemetry",
39
- ]);
40
- // Allowlist for archetype field. Match the 25 documented + "none" + "unknown".
41
- const ALLOWED_ARCHETYPES = new Set([
42
- "none", "unknown", "greenfield",
43
- "enterprise-saas", "agent-product", "ai-system", "mlops",
44
- "cli-tool", "cli", "library", "sdk", "devtools",
45
- "fintech", "regulated", "compliance",
46
- "iot-embedded", "web3", "marketplace", "cms", "edtech",
47
- "gov-public", "insurance", "data-platform", "streaming",
48
- "mobile-app", "infra", "web-service", "agent",
49
- ]);
50
- function configPath() {
51
- return path.join(os.homedir(), ".great_cto", "telemetry.json");
52
- }
53
- function legacyConfigPath() {
54
- return path.join(os.homedir(), ".great_cto", "config.json");
55
- }
56
- function readConfig() {
57
- // Try new file first.
58
- try {
59
- return JSON.parse(fs.readFileSync(configPath(), "utf8"));
60
- }
61
- catch { /* fall through */ }
62
- // Fall back to legacy config.json (read-only — never write to it).
63
- try {
64
- const legacy = JSON.parse(fs.readFileSync(legacyConfigPath(), "utf8"));
65
- return { enabled: legacy.telemetry, install_id: legacy.install_id };
66
- }
67
- catch {
68
- return {};
69
- }
70
- }
71
- function writeConfig(cfg) {
72
- const file = configPath();
73
- fs.mkdirSync(path.dirname(file), { recursive: true });
74
- fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
75
- }
76
- /** Detect CI / automation environments — never send from these. */
77
- function isCI() {
78
- const flags = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI", "BUILDKITE",
79
- "JENKINS_URL", "TF_BUILD", "DRONE", "TRAVIS", "APPVEYOR",
80
- "BITBUCKET_BUILD_NUMBER", "TEAMCITY_VERSION", "CODEBUILD_BUILD_ID"];
81
- return flags.some(f => process.env[f] != null && process.env[f] !== "");
82
- }
83
- /** Compute anon_id deterministically per machine, never reversible. */
84
- export function computeAnonId() {
85
- const seed = `great_cto/${os.userInfo().username || "?"}/${os.hostname() || "?"}`;
86
- return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 8);
87
- }
88
- /** Resolve telemetry-enabled state. Pure function, no side effects. */
89
- export function isTelemetryEnabled(cliFlag = false) {
90
- // Opt-out wins, in priority order:
91
- if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true")
92
- return false;
93
- if (process.env.GREAT_CTO_TELEMETRY === "off")
94
- return false;
95
- if (process.env.GREAT_CTO_DISABLE_TELEMETRY === "1")
96
- return false;
97
- if (process.env.GREATCTO_NO_TELEMETRY === "1")
98
- return false;
99
- if (cliFlag)
100
- return false;
101
- if (isCI())
102
- return false;
103
- // Opt-in checks:
104
- if (process.env.GREAT_CTO_TELEMETRY === "on")
105
- return true;
106
- const cfg = readConfig();
107
- if (cfg.enabled === true)
108
- return true;
109
- if (cfg.telemetry === true)
110
- return true; // legacy
111
- // Default: opt-out.
112
- return false;
113
- }
114
- function sanitize(opts) {
115
- const command = opts.command.toLowerCase();
116
- if (!ALLOWED_COMMANDS.has(command))
117
- return null;
118
- const archetypeRaw = (opts.archetype || "none").toLowerCase().trim();
119
- const archetype = ALLOWED_ARCHETYPES.has(archetypeRaw) ? archetypeRaw : "unknown";
120
- return {
121
- ts: new Date().toISOString(),
122
- version: opts.cliVersion,
123
- command,
124
- archetype,
125
- node: process.version.replace(/^v/, ""),
126
- os: process.platform,
127
- exit_code: typeof opts.exitCode === "number" ? opts.exitCode : 0,
128
- duration_ms: typeof opts.durationMs === "number" ? Math.max(0, Math.round(opts.durationMs)) : 0,
129
- anon_id: computeAnonId(),
130
- };
131
- }
132
- /** Fire-and-forget POST. Never blocks. Never throws. Never logs unless DRYRUN. */
133
- async function send(evt) {
134
- if (process.env.GREAT_CTO_TELEMETRY_DRYRUN === "1") {
135
- process.stderr.write(`[telemetry] would-send: ${JSON.stringify(evt)}\n`);
136
- return;
137
- }
138
- try {
139
- const ctrl = new AbortController();
140
- const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
141
- await fetch(TELEMETRY_ENDPOINT, {
142
- method: "POST",
143
- headers: { "Content-Type": "application/json" },
144
- body: JSON.stringify(evt),
145
- signal: ctrl.signal,
146
- }).catch(() => { });
147
- clearTimeout(timer);
148
- }
149
- catch { /* best-effort */ }
150
- }
151
- // --- Public API ------------------------------------------------------------
152
- /** First-run/install ping. Sent only when enabled. Idempotent across runs. */
153
- export async function sendInstallPing(opts) {
154
- if (!opts.consent)
155
- return;
156
- if (!isTelemetryEnabled())
157
- return;
158
- const evt = sanitize({ cliVersion: opts.cliVersion, command: "init", archetype: opts.archetype });
159
- if (!evt)
160
- return;
161
- await send(evt);
162
- }
163
- /** Per-command usage ping. Sent only when enabled. Fire-and-forget. */
164
- export async function sendUsagePing(opts) {
165
- if (!isTelemetryEnabled())
166
- return;
167
- const evt = sanitize({
168
- cliVersion: opts.cliVersion,
169
- command: opts.subcommand,
170
- archetype: opts.archetype,
171
- exitCode: opts.exitCode,
172
- durationMs: opts.durationMs,
173
- });
174
- if (!evt)
175
- return;
176
- await send(evt);
177
- }
178
- /**
179
- * Legacy shim — preserved for backwards compatibility with callers in main.ts
180
- * that pass `--no-telemetry`. With opt-IN default, consent resolution is
181
- * trivial: enabled iff isTelemetryEnabled() returns true.
182
- */
183
- export function resolveTelemetryConsent(cliFlag) {
184
- return isTelemetryEnabled(cliFlag);
185
- }
186
- // --- `npx great-cto telemetry <on|off|status|whoami>` subcommand -----------
187
- export function telemetrySubcommand(arg) {
188
- const action = (arg || "status").toLowerCase();
189
- switch (action) {
190
- case "on": {
191
- const cfg = readConfig();
192
- cfg.enabled = true;
193
- writeConfig(cfg);
194
- return { exitCode: 0, output: `✓ telemetry enabled (config: ${configPath()})\n` +
195
- ` Anonymous events go to ${TELEMETRY_ENDPOINT}\n` +
196
- ` See docs/PRIVACY.md for the full data schema.\n` };
197
- }
198
- case "off": {
199
- const cfg = readConfig();
200
- cfg.enabled = false;
201
- writeConfig(cfg);
202
- return { exitCode: 0, output: `✓ telemetry disabled (config: ${configPath()})\n` };
203
- }
204
- case "status": {
205
- const enabled = isTelemetryEnabled();
206
- const reason = enabled
207
- ? "enabled (sending events to " + TELEMETRY_ENDPOINT + ")"
208
- : isCI()
209
- ? "disabled (CI environment detected)"
210
- : process.env.DO_NOT_TRACK === "1"
211
- ? "disabled (DO_NOT_TRACK=1)"
212
- : "disabled (default; run 'great-cto telemetry on' to enable)";
213
- return { exitCode: 0, output: `telemetry: ${reason}\n` +
214
- `anon_id : ${computeAnonId()}\n` +
215
- `endpoint : ${TELEMETRY_ENDPOINT}\n` +
216
- `config : ${configPath()}\n` };
217
- }
218
- case "whoami": {
219
- return { exitCode: 0, output: computeAnonId() + "\n" };
220
- }
221
- default: {
222
- return { exitCode: 2, output: `usage: great-cto telemetry <on|off|status|whoami>\n` };
223
- }
224
- }
225
- }