great-cto 2.7.0 → 2.8.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/archetypes.js +129 -1
- package/dist/detect.js +120 -0
- package/dist/main.js +8 -36
- package/dist/packs.js +132 -0
- package/dist/telemetry.js +177 -111
- package/package.json +1 -1
package/dist/archetypes.js
CHANGED
|
@@ -180,6 +180,17 @@ const RULES = [
|
|
|
180
180
|
const stackJoined = d.stack.join(" ").toLowerCase();
|
|
181
181
|
if (/(?:^| )agent[-_](?:runtime|product|loop|kit|sdk)/.test(stackJoined))
|
|
182
182
|
s += 2;
|
|
183
|
+
// Voice-AI agent: telephony provider + STT + TTS + LLM = autonomous voice agent
|
|
184
|
+
const voiceProviders = ["twilio", "vonage", "livekit"];
|
|
185
|
+
const sttProviders = ["deepgram"];
|
|
186
|
+
const ttsProviders = ["elevenlabs", "hume"];
|
|
187
|
+
const hasVoice = voiceProviders.some((s) => d.stack.includes(s));
|
|
188
|
+
const hasStt = sttProviders.some((s) => d.stack.includes(s));
|
|
189
|
+
const hasTts = ttsProviders.some((s) => d.stack.includes(s));
|
|
190
|
+
if (hasVoice && hasLlm && (hasStt || hasTts))
|
|
191
|
+
s += 7; // strong: full voice-agent stack
|
|
192
|
+
else if (hasVoice && hasLlm)
|
|
193
|
+
s += 4; // medium: voice + LLM
|
|
183
194
|
return s;
|
|
184
195
|
},
|
|
185
196
|
reason: (d) => {
|
|
@@ -197,7 +208,10 @@ const RULES = [
|
|
|
197
208
|
const vdb = ["pinecone", "weaviate", "chroma", "qdrant"].filter((s) => d.stack.includes(s));
|
|
198
209
|
if (vdb.length)
|
|
199
210
|
bits.push(`vector DB (${vdb.join(",")})`);
|
|
200
|
-
|
|
211
|
+
const voice = ["twilio", "vonage", "livekit", "deepgram", "elevenlabs", "hume"].filter((s) => d.stack.includes(s));
|
|
212
|
+
if (voice.length)
|
|
213
|
+
bits.push(`voice stack (${voice.join(",")})`);
|
|
214
|
+
return `agent-product detected — ${bits.join(", ") || "agent signals"} — agent-eval + isolation + prompt-injection gates required`;
|
|
201
215
|
},
|
|
202
216
|
},
|
|
203
217
|
// ── ai-system ────────────────────────────────────
|
|
@@ -285,6 +299,15 @@ const RULES = [
|
|
|
285
299
|
s += 9;
|
|
286
300
|
if (d.stack.includes("fintech"))
|
|
287
301
|
s += 7;
|
|
302
|
+
// Emerging-markets payment providers
|
|
303
|
+
if (d.stack.includes("razorpay"))
|
|
304
|
+
s += 9;
|
|
305
|
+
if (d.stack.includes("paystack"))
|
|
306
|
+
s += 9;
|
|
307
|
+
if (d.stack.includes("flutterwave"))
|
|
308
|
+
s += 9;
|
|
309
|
+
if (d.stack.includes("mercadopago"))
|
|
310
|
+
s += 9;
|
|
288
311
|
if (d.readmeKeywords.includes("fintech"))
|
|
289
312
|
s += 2;
|
|
290
313
|
return s;
|
|
@@ -299,6 +322,14 @@ const RULES = [
|
|
|
299
322
|
bits.push("Dwolla");
|
|
300
323
|
if (d.stack.includes("teller"))
|
|
301
324
|
bits.push("Teller");
|
|
325
|
+
if (d.stack.includes("razorpay"))
|
|
326
|
+
bits.push("Razorpay (India)");
|
|
327
|
+
if (d.stack.includes("paystack"))
|
|
328
|
+
bits.push("Paystack (Nigeria)");
|
|
329
|
+
if (d.stack.includes("flutterwave"))
|
|
330
|
+
bits.push("Flutterwave (Africa)");
|
|
331
|
+
if (d.stack.includes("mercadopago"))
|
|
332
|
+
bits.push("MercadoPago (LATAM)");
|
|
302
333
|
return `fintech integration: ${bits.join(", ")} — SOX, PCI, KYC/AML compliance gates`;
|
|
303
334
|
},
|
|
304
335
|
},
|
|
@@ -1196,3 +1227,100 @@ export function suggestCompliance(d, archetype) {
|
|
|
1196
1227
|
c.add("sox");
|
|
1197
1228
|
return Array.from(c).sort();
|
|
1198
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Which review agents fire for each archetype. The reviewer name maps to
|
|
1232
|
+
* `agents/<name>.md` in the great_cto plugin. Naming aliases exist for
|
|
1233
|
+
* historical reasons (pci-reviewer covers fintech, firmware-reviewer
|
|
1234
|
+
* covers iot-embedded, etc.) — documented in
|
|
1235
|
+
* docs/agents/REVIEWER-NAMING.md (gap A2 closure).
|
|
1236
|
+
*
|
|
1237
|
+
* `greenfield` has no reviewers — pipeline runs in nano mode only.
|
|
1238
|
+
*/
|
|
1239
|
+
export const REVIEWERS_BY_ARCHETYPE = {
|
|
1240
|
+
"web-service": ["security-officer"],
|
|
1241
|
+
"mobile-app": ["mobile-store-reviewer", "security-officer"],
|
|
1242
|
+
"ai-system": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
|
|
1243
|
+
"agent-product": ["ai-security-reviewer", "ai-prompt-architect", "ai-eval-engineer"],
|
|
1244
|
+
"mlops": ["mlops-reviewer", "ai-security-reviewer"],
|
|
1245
|
+
"data-platform": ["data-platform-reviewer"],
|
|
1246
|
+
"streaming": ["streaming-reviewer"],
|
|
1247
|
+
"infra": ["infra-reviewer"],
|
|
1248
|
+
"library": ["library-reviewer"],
|
|
1249
|
+
"cli-tool": ["cli-reviewer"],
|
|
1250
|
+
"commerce": ["pci-reviewer", "security-officer"],
|
|
1251
|
+
"marketplace": ["marketplace-reviewer", "pci-reviewer"],
|
|
1252
|
+
"fintech": ["pci-reviewer", "regulated-reviewer"],
|
|
1253
|
+
"healthcare": ["healthcare-reviewer", "security-officer"],
|
|
1254
|
+
"web3": ["oracle-reviewer"],
|
|
1255
|
+
"iot-embedded": ["firmware-reviewer"],
|
|
1256
|
+
"regulated": ["regulated-reviewer"],
|
|
1257
|
+
"devtools": ["devtools-reviewer"],
|
|
1258
|
+
"browser-extension": ["web-store-reviewer"],
|
|
1259
|
+
"game": ["game-reviewer"],
|
|
1260
|
+
"cms": ["cms-reviewer"],
|
|
1261
|
+
"enterprise-saas": ["enterprise-saas-reviewer"],
|
|
1262
|
+
"edtech": ["edtech-reviewer"],
|
|
1263
|
+
"gov-public": ["gov-reviewer", "security-officer"],
|
|
1264
|
+
"insurance": ["insurance-reviewer", "regulated-reviewer"],
|
|
1265
|
+
"greenfield": [],
|
|
1266
|
+
};
|
|
1267
|
+
/**
|
|
1268
|
+
* Which human gates the pipeline opens for each archetype at the medium
|
|
1269
|
+
* project_size. Smaller sizes skip gates (see `gatesFor()` below); larger
|
|
1270
|
+
* sizes may add compliance.
|
|
1271
|
+
*
|
|
1272
|
+
* NOTE: nano → only [plan]. Enterprise → always adds compliance.
|
|
1273
|
+
*/
|
|
1274
|
+
export const GATES_BY_ARCHETYPE = {
|
|
1275
|
+
"web-service": ["plan", "qa", "ship"],
|
|
1276
|
+
"mobile-app": ["plan", "qa", "ship"],
|
|
1277
|
+
"ai-system": ["plan", "cost", "qa", "security", "ship"],
|
|
1278
|
+
"agent-product": ["plan", "cost", "qa", "security", "ship"],
|
|
1279
|
+
"mlops": ["plan", "cost", "qa", "security", "ship"],
|
|
1280
|
+
"data-platform": ["plan", "qa", "ship"],
|
|
1281
|
+
"streaming": ["plan", "qa", "ship"],
|
|
1282
|
+
"infra": ["plan", "qa", "ship"],
|
|
1283
|
+
"library": ["plan", "qa", "ship"],
|
|
1284
|
+
"cli-tool": ["plan", "qa", "ship"],
|
|
1285
|
+
"commerce": ["plan", "qa", "security", "ship", "compliance"],
|
|
1286
|
+
"marketplace": ["plan", "qa", "security", "ship", "compliance"],
|
|
1287
|
+
"fintech": ["plan", "qa", "security", "ship", "compliance"],
|
|
1288
|
+
"healthcare": ["plan", "qa", "security", "ship", "compliance"],
|
|
1289
|
+
"web3": ["plan", "qa", "oracle-review", "security", "ship"],
|
|
1290
|
+
"iot-embedded": ["plan", "qa", "security", "ship"],
|
|
1291
|
+
"regulated": ["plan", "qa", "security", "ship", "compliance"],
|
|
1292
|
+
"devtools": ["plan", "qa", "ship"],
|
|
1293
|
+
"browser-extension": ["plan", "qa", "ship"],
|
|
1294
|
+
"game": ["plan", "qa", "ship"],
|
|
1295
|
+
"cms": ["plan", "qa", "ship"],
|
|
1296
|
+
"enterprise-saas": ["plan", "qa", "security", "ship", "compliance"],
|
|
1297
|
+
"edtech": ["plan", "qa", "edtech-review", "security", "ship", "compliance"],
|
|
1298
|
+
"gov-public": ["plan", "qa", "gov-review", "security", "ship", "compliance"],
|
|
1299
|
+
"insurance": ["plan", "qa", "insurance-review", "security", "ship", "compliance"],
|
|
1300
|
+
"greenfield": ["plan"],
|
|
1301
|
+
};
|
|
1302
|
+
/**
|
|
1303
|
+
* Returns the subset of gates the pipeline will actually open for a
|
|
1304
|
+
* given archetype + project_size. Used by the orchestrator before
|
|
1305
|
+
* opening any gate to skip unnecessary human checkpoints on smaller
|
|
1306
|
+
* projects (e.g. nano always skips QA).
|
|
1307
|
+
*/
|
|
1308
|
+
export function gatesFor(archetype, size) {
|
|
1309
|
+
const all = GATES_BY_ARCHETYPE[archetype] ?? [];
|
|
1310
|
+
if (size === "nano")
|
|
1311
|
+
return all.filter((g) => g === "plan");
|
|
1312
|
+
if (size === "small")
|
|
1313
|
+
return all.filter((g) => g === "plan" || g === "ship");
|
|
1314
|
+
if (size === "medium")
|
|
1315
|
+
return all;
|
|
1316
|
+
// large + enterprise → ensure compliance is included
|
|
1317
|
+
return all.includes("compliance") ? all : [...all, "compliance"];
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Returns the ordered list of reviewers for an archetype. Empty for
|
|
1321
|
+
* `greenfield`. Used by the orchestrator to spawn the right
|
|
1322
|
+
* archetype-specific review stages after senior-dev.
|
|
1323
|
+
*/
|
|
1324
|
+
export function reviewersFor(archetype) {
|
|
1325
|
+
return REVIEWERS_BY_ARCHETYPE[archetype] ?? [];
|
|
1326
|
+
}
|
package/dist/detect.js
CHANGED
|
@@ -188,6 +188,23 @@ export function detect(dir) {
|
|
|
188
188
|
stack.add("teller");
|
|
189
189
|
sig("fintech", "teller");
|
|
190
190
|
}
|
|
191
|
+
// Emerging-markets payment providers (Wave-1 pack signals)
|
|
192
|
+
if (has("razorpay") || has("razorpay-node")) {
|
|
193
|
+
stack.add("razorpay");
|
|
194
|
+
sig("fintech", "razorpay");
|
|
195
|
+
}
|
|
196
|
+
if (has("@paystackhq/paystack-node") || has("paystack")) {
|
|
197
|
+
stack.add("paystack");
|
|
198
|
+
sig("fintech", "paystack");
|
|
199
|
+
}
|
|
200
|
+
if (has("@flutterwave/flutterwave-node-v3") || has("flutterwave")) {
|
|
201
|
+
stack.add("flutterwave");
|
|
202
|
+
sig("fintech", "flutterwave");
|
|
203
|
+
}
|
|
204
|
+
if (has("mercadopago")) {
|
|
205
|
+
stack.add("mercadopago");
|
|
206
|
+
sig("fintech", "mercadopago");
|
|
207
|
+
}
|
|
191
208
|
// Stripe Connect/Issuing → fintech (not just commerce)
|
|
192
209
|
if (has("stripe") && (pkg.name?.includes("bank") || pkg.name?.includes("card"))) {
|
|
193
210
|
sig("fintech", "stripe-connect");
|
|
@@ -206,6 +223,59 @@ export function detect(dir) {
|
|
|
206
223
|
stack.add("hl7");
|
|
207
224
|
sig("healthcare", "hl7");
|
|
208
225
|
}
|
|
226
|
+
if (has("dicom-parser") || has("cornerstone-core") || has("dcmjs")) {
|
|
227
|
+
stack.add("dicom");
|
|
228
|
+
sig("healthcare", "dicom");
|
|
229
|
+
}
|
|
230
|
+
// Voice / telephony (Wave-1 pack signals)
|
|
231
|
+
if (has("twilio") || has("@twilio/voice-sdk")) {
|
|
232
|
+
stack.add("twilio");
|
|
233
|
+
sig("voice", "twilio");
|
|
234
|
+
}
|
|
235
|
+
if (has("@vonage/server-sdk") || has("nexmo")) {
|
|
236
|
+
stack.add("vonage");
|
|
237
|
+
sig("voice", "vonage");
|
|
238
|
+
}
|
|
239
|
+
if (has("livekit-server-sdk") || has("livekit-client")) {
|
|
240
|
+
stack.add("livekit");
|
|
241
|
+
sig("voice", "livekit");
|
|
242
|
+
}
|
|
243
|
+
if (has("@deepgram/sdk")) {
|
|
244
|
+
stack.add("deepgram");
|
|
245
|
+
sig("voice", "deepgram");
|
|
246
|
+
}
|
|
247
|
+
if (has("elevenlabs") || has("@elevenlabs/elevenlabs-js")) {
|
|
248
|
+
stack.add("elevenlabs");
|
|
249
|
+
sig("voice", "elevenlabs");
|
|
250
|
+
}
|
|
251
|
+
if (has("hume") || has("hume-ai")) {
|
|
252
|
+
stack.add("hume");
|
|
253
|
+
sig("voice", "hume");
|
|
254
|
+
}
|
|
255
|
+
// HR / recruiting (Wave-1 pack signals)
|
|
256
|
+
if (has("greenhouse-io") || has("@greenhouse/api")) {
|
|
257
|
+
stack.add("greenhouse");
|
|
258
|
+
sig("hr", "greenhouse");
|
|
259
|
+
}
|
|
260
|
+
if (has("lever-api")) {
|
|
261
|
+
stack.add("lever");
|
|
262
|
+
sig("hr", "lever");
|
|
263
|
+
}
|
|
264
|
+
if (has("ashby-api")) {
|
|
265
|
+
stack.add("ashby");
|
|
266
|
+
sig("hr", "ashby");
|
|
267
|
+
}
|
|
268
|
+
// API platform (Wave-1 pack signals)
|
|
269
|
+
if (has("fastify"))
|
|
270
|
+
stack.add("fastify");
|
|
271
|
+
if (has("@trpc/server") || has("@trpc/client"))
|
|
272
|
+
stack.add("trpc");
|
|
273
|
+
if (has("@apollo/server") || has("apollo-server"))
|
|
274
|
+
stack.add("graphql");
|
|
275
|
+
if (has("graphql") || has("graphql-yoga"))
|
|
276
|
+
stack.add("graphql");
|
|
277
|
+
if (has("openapi3-ts") || has("@apidevtools/swagger-parser"))
|
|
278
|
+
stack.add("openapi");
|
|
209
279
|
// Auth
|
|
210
280
|
if (has("next-auth") || has("@auth/core"))
|
|
211
281
|
stack.add("auth");
|
|
@@ -1043,6 +1113,56 @@ function mineReadmeKeywords(dir) {
|
|
|
1043
1113
|
if (terms.some((t) => text.includes(t)))
|
|
1044
1114
|
kws.add(bucket);
|
|
1045
1115
|
}
|
|
1116
|
+
// Wave 1-3 pack-trigger raw terms — emitted verbatim so packs.ts
|
|
1117
|
+
// can substring-match them. Keep in sync with packs.ts SIGNALS.keywords.
|
|
1118
|
+
// Single tokens + multi-word phrases supported.
|
|
1119
|
+
const packTerms = [
|
|
1120
|
+
// voice-pack
|
|
1121
|
+
"voice", "telephony", "ivr", "tts", "stt", "speech-to-text", "text-to-speech",
|
|
1122
|
+
"outbound call", "inbound call", "voice agent",
|
|
1123
|
+
// clinical-pack + clinical-trials-pack
|
|
1124
|
+
"clinical", "patient", "ehr", "emr", "phi", "diagnosis", "diagnos", "triage",
|
|
1125
|
+
"radiolog", "patholog", "samd", "scribe", "telehealth-ai", "medical record",
|
|
1126
|
+
"cds", "clinical decision support",
|
|
1127
|
+
"clinical trial", "ctms", "edc", "epro", "econsent", "esource",
|
|
1128
|
+
"randomization", "rtsm", "irt", "decentralized trial", "ind submission",
|
|
1129
|
+
"21 cfr 11", "cdisc", "sdtm", "adam", "irb",
|
|
1130
|
+
// hr-ai-pack
|
|
1131
|
+
"recruit", "hiring", "candidate", "resume", "interview", "ats", "talent acquisition",
|
|
1132
|
+
"performance review", "workforce scheduling", "employee evaluation", "aedt",
|
|
1133
|
+
// api-platform-pack
|
|
1134
|
+
"public api", "partner api", "developer portal", "api key", "webhook", "sdk",
|
|
1135
|
+
"rest api", "graphql api", "openapi",
|
|
1136
|
+
// lending-pack
|
|
1137
|
+
"loan", "lending", "credit decision", "underwrit", "bnpl", "buy now pay later",
|
|
1138
|
+
"buy-now-pay-later", "payroll advance", "ewa", "line of credit", "fico",
|
|
1139
|
+
"credit score", "fcra", "nmls", "financing", "adverse action",
|
|
1140
|
+
// robotics-pack
|
|
1141
|
+
"robot", "cobot", "manipulator", "end-effector", "amr", "agv", "autonomous mobile",
|
|
1142
|
+
"surgical robot", "ros 2", "ros2", "drone", "uav",
|
|
1143
|
+
// em-fintech-pack
|
|
1144
|
+
"india", "nigeria", "brazil", "indonesia", "philippines", "mexico", "kenya",
|
|
1145
|
+
"m-pesa", "mpesa", "upi", "pix", "gcash", "ovo", "dana", "rbi", "cbn", "bsp",
|
|
1146
|
+
"ojk", "mas", "bcb", "condusef", "cross-border", "remittance", "local rails",
|
|
1147
|
+
// climate-pack
|
|
1148
|
+
"carbon", "emission", "ghg", "mrv", "scope 1", "scope 2", "scope 3", "verra",
|
|
1149
|
+
"gold standard", "puro", "sbti", "cdp", "csrd", "cbam", "ghgrp", "offset",
|
|
1150
|
+
"credit retir", "removal", "biogenic",
|
|
1151
|
+
"dna synthesis", "gene synthesis", "oligonucleotide", "protein design",
|
|
1152
|
+
"esm", "alphafold", "rfdiffusion", "pathogen", "select agent", "gain-of-function",
|
|
1153
|
+
"dual-use", "bsl-3", "bsl-4", "biocontainment", "bwc", "p3co", "igsc", "cloud lab",
|
|
1154
|
+
// drug-discovery-pack
|
|
1155
|
+
"drug discovery", "binding affinity", "admet", "toxicity prediction",
|
|
1156
|
+
"generative chem", "generative protein", "antibody design", "mrna design",
|
|
1157
|
+
"virtual screening", "docking", "fep", "chembl", "bindingdb", "pdbbind",
|
|
1158
|
+
"glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
|
|
1159
|
+
"lab automation", "robotic biology", "liquid handler", "hamilton", "tecan",
|
|
1160
|
+
"beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila",
|
|
1161
|
+
];
|
|
1162
|
+
for (const term of packTerms) {
|
|
1163
|
+
if (text.includes(term))
|
|
1164
|
+
kws.add(term);
|
|
1165
|
+
}
|
|
1046
1166
|
return Array.from(kws).sort();
|
|
1047
1167
|
}
|
|
1048
1168
|
function safeGlob(dir, pattern, kind = "file") {
|
package/dist/main.js
CHANGED
|
@@ -16,7 +16,6 @@ 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
|
-
import { resolveTelemetryConsent, sendInstallPing, sendUsagePing } from "./telemetry.js";
|
|
20
19
|
import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
|
|
21
20
|
import { readFileSync } from "node:fs";
|
|
22
21
|
import { dirname, join } from "node:path";
|
|
@@ -44,9 +43,9 @@ function parseArgs(argv) {
|
|
|
44
43
|
force: false,
|
|
45
44
|
archetype: null,
|
|
46
45
|
version: null,
|
|
47
|
-
noTelemetry: false,
|
|
48
46
|
useLlm: false,
|
|
49
47
|
noLlm: false,
|
|
48
|
+
positional: [],
|
|
50
49
|
};
|
|
51
50
|
const rest = [];
|
|
52
51
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -69,8 +68,6 @@ function parseArgs(argv) {
|
|
|
69
68
|
args.boardPort = parseInt(argv[++i] ?? "3141", 10);
|
|
70
69
|
else if (a === "--no-open")
|
|
71
70
|
args.boardNoOpen = true;
|
|
72
|
-
else if (a === "--no-telemetry")
|
|
73
|
-
args.noTelemetry = true;
|
|
74
71
|
else if (a === "--use-llm")
|
|
75
72
|
args.useLlm = true;
|
|
76
73
|
else if (a === "--no-llm")
|
|
@@ -118,6 +115,7 @@ function parseArgs(argv) {
|
|
|
118
115
|
rest.push(a);
|
|
119
116
|
}
|
|
120
117
|
args.dir = resolve(args.dir);
|
|
118
|
+
args.positional = rest;
|
|
121
119
|
return args;
|
|
122
120
|
}
|
|
123
121
|
/**
|
|
@@ -406,8 +404,6 @@ ${bold("Options:")}
|
|
|
406
404
|
even when heuristic confidence is high
|
|
407
405
|
--no-llm Skip LLM suggestion (run heuristic only)
|
|
408
406
|
Or set ${cyan("GREATCTO_NO_LLM=1")}
|
|
409
|
-
--no-telemetry Skip anonymous install ping
|
|
410
|
-
Or set ${cyan("GREATCTO_NO_TELEMETRY=1")}
|
|
411
407
|
-h, --help Show this help
|
|
412
408
|
-v, --version Show great-cto CLI version
|
|
413
409
|
|
|
@@ -742,17 +738,6 @@ async function runInit(args) {
|
|
|
742
738
|
if (!bs.created) {
|
|
743
739
|
log(` ${dim("PROJECT.md already exists at")} ${bs.projectMdPath} ${dim("— kept as-is")}`);
|
|
744
740
|
}
|
|
745
|
-
// ── telemetry (opt-in, fire-and-forget) ─────────────────
|
|
746
|
-
try {
|
|
747
|
-
const consent = resolveTelemetryConsent(args.noTelemetry);
|
|
748
|
-
// Don't await — finish CLI banner first; ping flies in background
|
|
749
|
-
void sendInstallPing({
|
|
750
|
-
cliVersion: getCliVersion(),
|
|
751
|
-
archetype: archetype,
|
|
752
|
-
consent,
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
catch { /* never block install on telemetry */ }
|
|
756
741
|
// ── done ─────────────────────────────────────────────────
|
|
757
742
|
log("");
|
|
758
743
|
log(green(bold("✓ great_cto is ready.")));
|
|
@@ -768,19 +753,6 @@ async function runInit(args) {
|
|
|
768
753
|
log("");
|
|
769
754
|
return 0;
|
|
770
755
|
}
|
|
771
|
-
/**
|
|
772
|
-
* Exit with telemetry ping for a subcommand. Fire-and-forget — we wait up
|
|
773
|
-
* to 200ms for the ping to land before exiting so it doesn't get killed by
|
|
774
|
-
* process termination. Honours all telemetry opt-out signals.
|
|
775
|
-
*/
|
|
776
|
-
async function exitWithTelemetry(subcommand, code) {
|
|
777
|
-
try {
|
|
778
|
-
const promise = sendUsagePing({ cliVersion: getCliVersion(), subcommand, exitCode: code });
|
|
779
|
-
await Promise.race([promise, new Promise(r => setTimeout(r, 200))]);
|
|
780
|
-
}
|
|
781
|
-
catch { /* never block exit */ }
|
|
782
|
-
process.exit(code);
|
|
783
|
-
}
|
|
784
756
|
async function main() {
|
|
785
757
|
const rawArgv = process.argv.slice(2);
|
|
786
758
|
const args = parseArgs(rawArgv);
|
|
@@ -832,7 +804,7 @@ async function main() {
|
|
|
832
804
|
try {
|
|
833
805
|
const { runCi, parseCiArgs } = await import("./ci.js");
|
|
834
806
|
const code = await runCi(parseCiArgs(rawArgv));
|
|
835
|
-
|
|
807
|
+
process.exit(code);
|
|
836
808
|
}
|
|
837
809
|
catch (e) {
|
|
838
810
|
error(e.message);
|
|
@@ -846,7 +818,7 @@ async function main() {
|
|
|
846
818
|
const portArg = rawArgv.indexOf("--port");
|
|
847
819
|
const port = portArg >= 0 ? parseInt(rawArgv[portArg + 1] ?? "8765", 10) : 8765;
|
|
848
820
|
const code = await runMcp({ mode: sse ? "sse" : "stdio", port, version: getCliVersion() });
|
|
849
|
-
|
|
821
|
+
process.exit(code);
|
|
850
822
|
}
|
|
851
823
|
catch (e) {
|
|
852
824
|
error(e.message);
|
|
@@ -868,7 +840,7 @@ async function main() {
|
|
|
868
840
|
dryRun: rawArgv.includes("--dry-run"),
|
|
869
841
|
cwd: args.dir,
|
|
870
842
|
});
|
|
871
|
-
|
|
843
|
+
process.exit(code);
|
|
872
844
|
}
|
|
873
845
|
catch (e) {
|
|
874
846
|
error(e.message);
|
|
@@ -883,7 +855,7 @@ async function main() {
|
|
|
883
855
|
noLog: rawArgv.includes("--no-log"),
|
|
884
856
|
insecure: rawArgv.includes("--insecure"),
|
|
885
857
|
});
|
|
886
|
-
|
|
858
|
+
process.exit(code);
|
|
887
859
|
}
|
|
888
860
|
catch (e) {
|
|
889
861
|
error(e.message);
|
|
@@ -899,7 +871,7 @@ async function main() {
|
|
|
899
871
|
process.exit(2);
|
|
900
872
|
}
|
|
901
873
|
const code = await runWebhookCli(parsed);
|
|
902
|
-
|
|
874
|
+
process.exit(code);
|
|
903
875
|
}
|
|
904
876
|
catch (e) {
|
|
905
877
|
error(e.message);
|
|
@@ -930,7 +902,7 @@ async function main() {
|
|
|
930
902
|
process.exit(2);
|
|
931
903
|
}
|
|
932
904
|
const code = await runReport(parsed);
|
|
933
|
-
|
|
905
|
+
process.exit(code);
|
|
934
906
|
}
|
|
935
907
|
catch (e) {
|
|
936
908
|
error(e.message);
|
package/dist/packs.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Domain pack detection — overlay packs that ride on top of base archetypes.
|
|
2
|
+
// Wave 1-3 (2026-05): voice, clinical, hr-ai, api-platform, lending, clinical-trials,
|
|
3
|
+
// robotics, em-fintech, climate, drug-discovery.
|
|
4
|
+
//
|
|
5
|
+
// A pack is a regulatory/domain overlay that triggers one or more specialist
|
|
6
|
+
// reviewers (agents/{name}-reviewer.md) when its signals appear in stack,
|
|
7
|
+
// dependencies, or README keywords. Detection is deliberately broad — the
|
|
8
|
+
// reviewer agents themselves do the final scope decision via their Step 0 grep.
|
|
9
|
+
// Reviewer registry — keep in sync with agents/*-reviewer.md and
|
|
10
|
+
// skills/great_cto/packs/*-pack.md.
|
|
11
|
+
const PACK_REVIEWERS = {
|
|
12
|
+
"voice-pack": ["voice-ai-reviewer"],
|
|
13
|
+
"clinical-pack": ["ai-clinical-reviewer", "fda-reviewer"],
|
|
14
|
+
"hr-ai-pack": ["hr-ai-reviewer"],
|
|
15
|
+
"api-platform-pack": ["api-platform-reviewer"],
|
|
16
|
+
"lending-pack": ["lending-credit-reviewer"],
|
|
17
|
+
"clinical-trials-pack": ["clinical-trials-reviewer", "bio-data-reviewer"],
|
|
18
|
+
"robotics-pack": ["robotics-safety-reviewer"],
|
|
19
|
+
"em-fintech-pack": ["emerging-markets-fintech-reviewer"],
|
|
20
|
+
"climate-pack": ["climate-mrv-reviewer", "biosecurity-reviewer"],
|
|
21
|
+
"drug-discovery-pack": ["drug-discovery-ml-reviewer", "glp-glab-reviewer", "lab-automation-reviewer"],
|
|
22
|
+
};
|
|
23
|
+
const PACK_GATES = {
|
|
24
|
+
"voice-pack": ["gate:voice-compliance"],
|
|
25
|
+
"clinical-pack": ["gate:samd-class", "gate:clinical-validation", "gate:ide-approval"],
|
|
26
|
+
"hr-ai-pack": ["gate:aedt-audit"],
|
|
27
|
+
"api-platform-pack": ["gate:api-contract"],
|
|
28
|
+
"lending-pack": ["gate:fair-lending"],
|
|
29
|
+
"clinical-trials-pack": ["gate:irb-ready", "gate:part11-validation", "gate:deidentification"],
|
|
30
|
+
"robotics-pack": ["gate:hara-signoff", "gate:functional-safety-test"],
|
|
31
|
+
"em-fintech-pack": ["gate:license-strategy"],
|
|
32
|
+
"climate-pack": ["gate:mrv-methodology", "gate:durc-signoff", "gate:open-weights-release"],
|
|
33
|
+
"drug-discovery-pack": ["gate:model-card-signoff", "gate:csv-validation", "gate:iq-oq-pq"],
|
|
34
|
+
};
|
|
35
|
+
// Trigger signals — stack tokens OR README keywords.
|
|
36
|
+
const SIGNALS = {
|
|
37
|
+
"voice-pack": {
|
|
38
|
+
stack: ["twilio", "vonage", "livekit", "deepgram", "elevenlabs", "whisper", "hume"],
|
|
39
|
+
keywords: ["voice", "telephony", "ivr", "call-center", "tts", "stt", "speech-to-text", "text-to-speech", "phone", "outbound call", "inbound call", "voice agent"],
|
|
40
|
+
},
|
|
41
|
+
"clinical-pack": {
|
|
42
|
+
stack: ["fhir", "hl7"],
|
|
43
|
+
keywords: ["clinical", "patient", "ehr", "emr", "phi", "hipaa", "diagnos", "triage", "radiolog", "patholog", "samd", "scribe", "telehealth-ai", "medical record", "cds", "clinical decision support"],
|
|
44
|
+
},
|
|
45
|
+
"hr-ai-pack": {
|
|
46
|
+
stack: ["greenhouse", "lever", "ashby", "workday"],
|
|
47
|
+
// "candidate" alone is too generic (SaMD/predicate-candidate clash); use compound terms.
|
|
48
|
+
keywords: ["recruit", "hiring", "candidate screening", "candidate evaluation", "candidate ranking", "resume", "ats", "talent acquisition", "performance review", "workforce scheduling", "employee evaluation", "aedt"],
|
|
49
|
+
},
|
|
50
|
+
"api-platform-pack": {
|
|
51
|
+
stack: ["openapi", "graphql", "grpc", "fastify", "trpc"],
|
|
52
|
+
keywords: ["public api", "partner api", "developer portal", "api key", "webhook", "sdk", "rest api", "graphql api", "openapi"],
|
|
53
|
+
},
|
|
54
|
+
"lending-pack": {
|
|
55
|
+
stack: ["plaid"],
|
|
56
|
+
// "ecoa" removed: collides with clinical "eCOA" (Electronic Clinical Outcome Assessment).
|
|
57
|
+
// Lending-specific signals (loan, fcra, nmls, underwrit, fico, ...) are unambiguous.
|
|
58
|
+
keywords: ["loan", "lending", "credit decision", "underwrit", "bnpl", "buy now pay later", "buy-now-pay-later", "payroll advance", "ewa", "line of credit", "fico", "credit score", "fcra", "nmls", "financing", "adverse action"],
|
|
59
|
+
},
|
|
60
|
+
"clinical-trials-pack": {
|
|
61
|
+
stack: ["fhir", "hl7", "dicom", "redcap"],
|
|
62
|
+
// "ecoa" removed: ambiguous with lending ECOA (Reg B). Other clinical-trial
|
|
63
|
+
// signals (ctms, edc, epro, econsent, cdisc, irb, ...) are unambiguous.
|
|
64
|
+
keywords: ["clinical trial", "ctms", "edc", "epro", "econsent", "esource", "randomization", "rtsm", "irt", "decentralized trial", "ind submission", "21 cfr 11", "cdisc", "sdtm", "adam", "irb"],
|
|
65
|
+
},
|
|
66
|
+
"robotics-pack": {
|
|
67
|
+
stack: ["ros", "ros2", "moveit", "gazebo", "px4"],
|
|
68
|
+
keywords: ["robot", "cobot", "manipulator", "end-effector", "amr", "agv", "autonomous mobile", "surgical robot", "ros 2", "drone", "uav"],
|
|
69
|
+
},
|
|
70
|
+
"em-fintech-pack": {
|
|
71
|
+
stack: [],
|
|
72
|
+
keywords: ["india", "nigeria", "brazil", "indonesia", "philippines", "mexico", "kenya", "m-pesa", "mpesa", "upi", "pix", "gcash", "ovo", "dana", "rbi", "cbn", "bsp", "ojk", "mas", "bcb", "condusef", "cross-border", "remittance", "local rails"],
|
|
73
|
+
},
|
|
74
|
+
"climate-pack": {
|
|
75
|
+
stack: [],
|
|
76
|
+
keywords: ["carbon", "emission", "ghg", "mrv", "scope 1", "scope 2", "scope 3", "verra", "gold standard", "puro", "sbti", "cdp", "csrd", "cbam", "ghgrp", "offset", "credit retir", "removal", "biogenic",
|
|
77
|
+
// biosec triggers
|
|
78
|
+
"dna synthesis", "gene synthesis", "oligonucleotide", "protein design", "esm", "alphafold", "rfdiffusion", "pathogen", "select agent", "gain-of-function", "dual-use", "bsl-3", "bsl-4", "biocontainment", "bwc", "p3co", "igsc", "cloud lab"],
|
|
79
|
+
},
|
|
80
|
+
"drug-discovery-pack": {
|
|
81
|
+
stack: ["rdkit"],
|
|
82
|
+
keywords: ["drug discovery", "binding affinity", "admet", "toxicity prediction", "generative chem", "generative protein", "antibody design", "mrna design", "virtual screening", "docking", "fep", "alphafold", "rfdiffusion", "chembl", "bindingdb", "pdbbind",
|
|
83
|
+
// GLP / lab-automation triggers
|
|
84
|
+
"glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
|
|
85
|
+
"lab automation", "cloud lab", "robotic biology", "liquid handler", "hamilton", "tecan", "beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila"],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
/** Return packs whose signals match the detection result. Deterministic + sorted. */
|
|
89
|
+
export function suggestPacks(d) {
|
|
90
|
+
const matches = [];
|
|
91
|
+
const stackLower = new Set(d.stack.map((s) => s.toLowerCase()));
|
|
92
|
+
const kwLower = d.readmeKeywords.map((k) => k.toLowerCase());
|
|
93
|
+
for (const pack of Object.keys(SIGNALS)) {
|
|
94
|
+
const { stack, keywords } = SIGNALS[pack];
|
|
95
|
+
const matchedStack = stack.filter((s) => stackLower.has(s.toLowerCase()));
|
|
96
|
+
// Exact-match on README-mined raw tokens. detect.ts emits the same vocabulary,
|
|
97
|
+
// so kwLower.includes(kw) avoids false-positive substring fuzz
|
|
98
|
+
// (e.g. "lending" matching pack trigger "ind").
|
|
99
|
+
const matchedKeywords = keywords.filter((kw) => kwLower.includes(kw));
|
|
100
|
+
const signals = [...matchedStack, ...matchedKeywords];
|
|
101
|
+
if (signals.length > 0) {
|
|
102
|
+
matches.push({
|
|
103
|
+
pack,
|
|
104
|
+
reviewers: PACK_REVIEWERS[pack],
|
|
105
|
+
signals: signals.slice(0, 8), // cap for log readability
|
|
106
|
+
humanGates: PACK_GATES[pack],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
matches.sort((a, b) => a.pack.localeCompare(b.pack));
|
|
111
|
+
return matches;
|
|
112
|
+
}
|
|
113
|
+
/** Convenience — flatten matched packs to reviewer agent names (unique, sorted). */
|
|
114
|
+
export function suggestPackReviewers(d) {
|
|
115
|
+
const all = new Set();
|
|
116
|
+
for (const m of suggestPacks(d))
|
|
117
|
+
for (const r of m.reviewers)
|
|
118
|
+
all.add(r);
|
|
119
|
+
return Array.from(all).sort();
|
|
120
|
+
}
|
|
121
|
+
/** Convenience — flatten matched packs to human-gate ids (unique, sorted). */
|
|
122
|
+
export function suggestPackGates(d) {
|
|
123
|
+
const all = new Set();
|
|
124
|
+
for (const m of suggestPacks(d))
|
|
125
|
+
for (const g of m.humanGates)
|
|
126
|
+
all.add(g);
|
|
127
|
+
return Array.from(all).sort();
|
|
128
|
+
}
|
|
129
|
+
/** Registry of all known packs — useful for /doctor / introspection. */
|
|
130
|
+
export function listPacks() {
|
|
131
|
+
return Object.keys(SIGNALS).sort();
|
|
132
|
+
}
|
package/dist/telemetry.js
CHANGED
|
@@ -1,34 +1,68 @@
|
|
|
1
|
-
// Anonymous opt-
|
|
1
|
+
// Anonymous opt-IN telemetry — default OFF.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
6
9
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
|
9
14
|
//
|
|
10
|
-
// Opt-out:
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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 }
|
|
15
21
|
//
|
|
16
|
-
// Endpoint:
|
|
17
|
-
//
|
|
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"
|
|
18
25
|
import * as fs from "node:fs";
|
|
19
26
|
import * as path from "node:path";
|
|
20
27
|
import * as os from "node:os";
|
|
21
28
|
import * as crypto from "node:crypto";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
]);
|
|
25
50
|
function configPath() {
|
|
51
|
+
return path.join(os.homedir(), ".great_cto", "telemetry.json");
|
|
52
|
+
}
|
|
53
|
+
function legacyConfigPath() {
|
|
26
54
|
return path.join(os.homedir(), ".great_cto", "config.json");
|
|
27
55
|
}
|
|
28
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).
|
|
29
63
|
try {
|
|
30
|
-
const
|
|
31
|
-
return
|
|
64
|
+
const legacy = JSON.parse(fs.readFileSync(legacyConfigPath(), "utf8"));
|
|
65
|
+
return { enabled: legacy.telemetry, install_id: legacy.install_id };
|
|
32
66
|
}
|
|
33
67
|
catch {
|
|
34
68
|
return {};
|
|
@@ -39,121 +73,153 @@ function writeConfig(cfg) {
|
|
|
39
73
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
40
74
|
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
41
75
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return id;
|
|
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] !== "");
|
|
49
82
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
61
97
|
if (process.env.GREATCTO_NO_TELEMETRY === "1")
|
|
62
98
|
return false;
|
|
63
99
|
if (cliFlag)
|
|
64
100
|
return false;
|
|
101
|
+
if (isCI())
|
|
102
|
+
return false;
|
|
103
|
+
// Opt-in checks:
|
|
104
|
+
if (process.env.GREAT_CTO_TELEMETRY === "on")
|
|
105
|
+
return true;
|
|
65
106
|
const cfg = readConfig();
|
|
66
|
-
if (
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
log(dim(" great_cto sends one anonymous ping per install:"));
|
|
73
|
-
log(dim(" install_id, version, archetype, Node version, OS."));
|
|
74
|
-
log(dim(" No paths, no code, no PII. Disable any time:"));
|
|
75
|
-
log(dim(" great-cto --no-telemetry · or set GREATCTO_NO_TELEMETRY=1"));
|
|
76
|
-
log(dim(" or edit ~/.great_cto/config.json: { \"telemetry\": false }"));
|
|
77
|
-
log(dim("──────────────────────────────────────────────────────"));
|
|
78
|
-
log("");
|
|
79
|
-
cfg.telemetry = true;
|
|
80
|
-
cfg.telemetry_asked = true;
|
|
81
|
-
ensureInstallId(cfg);
|
|
82
|
-
writeConfig(cfg);
|
|
83
|
-
return true;
|
|
107
|
+
if (cfg.enabled === true)
|
|
108
|
+
return true;
|
|
109
|
+
if (cfg.telemetry === true)
|
|
110
|
+
return true; // legacy
|
|
111
|
+
// Default: opt-out.
|
|
112
|
+
return false;
|
|
84
113
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const cfg = readConfig();
|
|
93
|
-
const install_id = ensureInstallId(cfg);
|
|
94
|
-
const evt = {
|
|
95
|
-
install_id,
|
|
96
|
-
cli_version: opts.cliVersion,
|
|
97
|
-
archetype: opts.archetype,
|
|
98
|
-
node_version: process.version,
|
|
99
|
-
platform: process.platform,
|
|
100
|
-
arch: process.arch,
|
|
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 {
|
|
101
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(),
|
|
102
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
|
+
}
|
|
103
138
|
try {
|
|
104
139
|
const ctrl = new AbortController();
|
|
105
140
|
const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
|
|
106
141
|
await fetch(TELEMETRY_ENDPOINT, {
|
|
107
142
|
method: "POST",
|
|
108
|
-
headers: { "Content-Type": "application/json"
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
109
144
|
body: JSON.stringify(evt),
|
|
110
145
|
signal: ctrl.signal,
|
|
111
146
|
}).catch(() => { });
|
|
112
147
|
clearTimeout(timer);
|
|
113
148
|
}
|
|
114
|
-
catch {
|
|
115
|
-
// never block install on telemetry failure
|
|
116
|
-
}
|
|
149
|
+
catch { /* best-effort */ }
|
|
117
150
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*/
|
|
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. */
|
|
131
164
|
export async function sendUsagePing(opts) {
|
|
132
|
-
if (
|
|
165
|
+
if (!isTelemetryEnabled())
|
|
133
166
|
return;
|
|
134
|
-
const
|
|
135
|
-
|
|
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)
|
|
136
175
|
return;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
}
|
|
158
224
|
}
|
|
159
225
|
}
|
package/package.json
CHANGED