chati-dev 3.3.1 → 3.3.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.
@@ -0,0 +1,56 @@
1
+ # Chati.dev Terms of Use
2
+
3
+ Last updated: 2026-03-03
4
+
5
+ ## 1. Acceptance
6
+
7
+ By installing and using Chati.dev, you agree to these Terms of Use.
8
+
9
+ ## 2. Anonymous Usage Data Collection
10
+
11
+ Chati.dev collects anonymous usage data by default to improve the product. This includes:
12
+
13
+ - **Agents used** — which pipeline agents are invoked (e.g., brief, architect, dev)
14
+ - **Pipeline duration** — how long each pipeline phase takes
15
+ - **Gate scores** — quality gate evaluation scores
16
+ - **Circuit breaker events** — when safety mechanisms are triggered
17
+ - **Error types** — categorized error codes (no stack traces or file paths)
18
+
19
+ ### What is NOT collected
20
+
21
+ - No source code, file contents, or file paths
22
+ - No personal information (name, email, IP address)
23
+ - No project names or repository URLs
24
+ - No prompts, conversations, or AI responses
25
+
26
+ ### Anonymous Identification
27
+
28
+ A random UUID is generated locally for your installation. This ID cannot be traced back to you.
29
+
30
+ ## 3. Opting Out
31
+
32
+ You can disable telemetry at any time:
33
+
34
+ ```
35
+ npx chati-dev telemetry disable
36
+ ```
37
+
38
+ You can re-enable it with:
39
+
40
+ ```
41
+ npx chati-dev telemetry enable
42
+ ```
43
+
44
+ Check current status:
45
+
46
+ ```
47
+ npx chati-dev telemetry
48
+ ```
49
+
50
+ ## 4. Data Handling
51
+
52
+ Data is sent to Chati.dev telemetry servers and used solely for product improvement. Data is not sold or shared with third parties.
53
+
54
+ ## 5. License
55
+
56
+ Chati.dev is licensed under the Elastic License 2.0. See LICENSE for full terms.
@@ -1,8 +1,8 @@
1
1
  # chati.dev Configuration
2
- version: "3.3.0"
2
+ version: "3.3.2"
3
3
  installed_at: "2026-02-07T10:00:00Z"
4
- updated_at: "2026-03-02T00:00:00Z"
5
- installer_version: "3.3.0"
4
+ updated_at: "2026-03-03T00:00:00Z"
5
+ installer_version: "3.3.2"
6
6
  project_type: greenfield
7
7
  language: en
8
8
  ides: [claude-code]
@@ -27,3 +27,8 @@ providers:
27
27
  # brownfield-wu:
28
28
  # provider: gemini
29
29
  # model: pro
30
+
31
+ # Telemetry — opt-in anonymous usage tracking
32
+ telemetry:
33
+ enabled: true
34
+ anonymous_id: d0ae24af-0a62-472c-b501-2104f03aa6af
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,8 @@
21
21
  "assets/",
22
22
  "framework/",
23
23
  "scripts/",
24
- "LICENSE"
24
+ "LICENSE",
25
+ "TERMS_OF_USE.md"
25
26
  ],
26
27
  "scripts": {
27
28
  "start": "node bin/chati.js",
@@ -110,7 +110,7 @@ export function generateConfigYaml(config) {
110
110
 
111
111
  // Add telemetry config
112
112
  configData.telemetry = {
113
- enabled: config.telemetryEnabled || false,
113
+ enabled: config.telemetryEnabled !== false,
114
114
  anonymous_id: null,
115
115
  };
116
116
 
@@ -5,9 +5,10 @@
5
5
 
6
6
  import { AGENT_PIPELINE, getNextAgent } from './agent-selector.js';
7
7
  import { calculateBracket, estimateRemaining } from '../context/bracket-tracker.js';
8
- import { track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
8
+ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
9
9
  import { sendEvents } from '../telemetry/sender.js';
10
- import { getTelemetryConfig } from '../telemetry/config.js';
10
+ import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
11
+ import { getCurrentVersion } from '../upgrade/checker.js';
11
12
 
12
13
  /**
13
14
  * Pipeline phases in order.
@@ -39,7 +40,7 @@ const QA_IMPLEMENTATION_THRESHOLD = 95;
39
40
  * @returns {object} Pipeline state
40
41
  */
41
42
  export function initPipeline(options = {}) {
42
- const { isGreenfield = true, mode = 'discover' } = options;
43
+ const { isGreenfield = true, mode = 'discover', targetDir } = options;
43
44
 
44
45
  const agents = {};
45
46
  for (const agentDef of AGENT_PIPELINE) {
@@ -61,9 +62,25 @@ export function initPipeline(options = {}) {
61
62
  };
62
63
  }
63
64
 
65
+ const resolvedDir = targetDir || process.cwd();
66
+
67
+ // Initialize telemetry collector based on project config
68
+ initCollector(isTelemetryEnabled(resolvedDir));
69
+
70
+ const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
71
+
72
+ telemetryTrack('session_started', {
73
+ sessionId,
74
+ pipelineType: 'full',
75
+ mode,
76
+ });
77
+
64
78
  return {
65
79
  phase: mode,
66
80
  isGreenfield,
81
+ sessionId,
82
+ targetDir: resolvedDir,
83
+ chatiVersion: getCurrentVersion(resolvedDir) || 'unknown',
67
84
  startedAt: new Date().toISOString(),
68
85
  completedAt: null,
69
86
  agents,
@@ -96,7 +113,7 @@ const QUICK_FLOW_AGENTS = ['brief', 'dev', 'qa-implementation', 'devops'];
96
113
  * @returns {object} Pipeline state
97
114
  */
98
115
  export function initQuickFlowPipeline(options = {}) {
99
- const { isGreenfield = false, mode = 'discover' } = options;
116
+ const { isGreenfield = false, mode = 'discover', targetDir } = options;
100
117
 
101
118
  const agents = {};
102
119
  for (const agentName of QUICK_FLOW_AGENTS) {
@@ -108,10 +125,26 @@ export function initQuickFlowPipeline(options = {}) {
108
125
  };
109
126
  }
110
127
 
128
+ const resolvedDir = targetDir || process.cwd();
129
+
130
+ // Initialize telemetry collector based on project config
131
+ initCollector(isTelemetryEnabled(resolvedDir));
132
+
133
+ const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
134
+
135
+ telemetryTrack('session_started', {
136
+ sessionId,
137
+ pipelineType: 'quick-flow',
138
+ mode,
139
+ });
140
+
111
141
  return {
112
142
  phase: mode,
113
143
  isGreenfield,
114
144
  isQuickFlow: true,
145
+ sessionId,
146
+ targetDir: resolvedDir,
147
+ chatiVersion: getCurrentVersion(resolvedDir) || 'unknown',
115
148
  startedAt: new Date().toISOString(),
116
149
  completedAt: null,
117
150
  agents,
@@ -130,7 +163,7 @@ export function initQuickFlowPipeline(options = {}) {
130
163
  * @returns {object} Pipeline state
131
164
  */
132
165
  export function initStandardFlowPipeline(options = {}) {
133
- const { isGreenfield = false, mode = 'discover' } = options;
166
+ const { isGreenfield = false, mode = 'discover', targetDir } = options;
134
167
 
135
168
  const agents = {};
136
169
  for (const agentName of STANDARD_FLOW_AGENTS) {
@@ -142,10 +175,26 @@ export function initStandardFlowPipeline(options = {}) {
142
175
  };
143
176
  }
144
177
 
178
+ const resolvedDir = targetDir || process.cwd();
179
+
180
+ // Initialize telemetry collector based on project config
181
+ initCollector(isTelemetryEnabled(resolvedDir));
182
+
183
+ const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
184
+
185
+ telemetryTrack('session_started', {
186
+ sessionId,
187
+ pipelineType: 'standard',
188
+ mode,
189
+ });
190
+
145
191
  return {
146
192
  phase: mode,
147
193
  isGreenfield,
148
194
  isStandardFlow: true,
195
+ sessionId,
196
+ targetDir: resolvedDir,
197
+ chatiVersion: getCurrentVersion(resolvedDir) || 'unknown',
149
198
  startedAt: new Date().toISOString(),
150
199
  completedAt: null,
151
200
  agents,
@@ -245,13 +294,24 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
245
294
 
246
295
  // Pipeline complete
247
296
  newState.completedAt = new Date().toISOString();
297
+ const pipelineType1 = newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full';
298
+ const totalDuration1 = Date.now() - new Date(newState.startedAt).getTime();
248
299
  telemetryTrack('pipeline_completed', {
249
- pipelineType: newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full',
250
- totalDuration: Date.now() - new Date(newState.startedAt).getTime(),
300
+ pipelineType: pipelineType1,
301
+ totalDuration: totalDuration1,
251
302
  agentsRun: newState.completedAgents.length,
252
303
  finalStatus: 'completed',
253
304
  deviationCount: (newState.modeTransitions || []).length,
254
305
  });
306
+ telemetryTrack('session_completed', {
307
+ sessionId: newState.sessionId || 'unknown',
308
+ pipelineType: pipelineType1,
309
+ mode: newState.phase === 'deploy' ? 'deploy' : 'build',
310
+ finalStage: newState.phase,
311
+ duration: totalDuration1,
312
+ agentCount: newState.completedAgents.length,
313
+ success: true,
314
+ });
255
315
  const flushedEvents = telemetryFlush();
256
316
  if (flushedEvents.length > 0) {
257
317
  const tConfig = getTelemetryConfig(newState.targetDir || process.cwd());
@@ -306,13 +366,24 @@ export function advancePipeline(pipelineState, completedAgent, results = {}) {
306
366
 
307
367
  // Pipeline complete
308
368
  newState.completedAt = new Date().toISOString();
369
+ const pipelineType2 = newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full';
370
+ const totalDuration2 = Date.now() - new Date(newState.startedAt).getTime();
309
371
  telemetryTrack('pipeline_completed', {
310
- pipelineType: newState.isQuickFlow ? 'quick-flow' : newState.isStandardFlow ? 'standard' : 'full',
311
- totalDuration: Date.now() - new Date(newState.startedAt).getTime(),
372
+ pipelineType: pipelineType2,
373
+ totalDuration: totalDuration2,
312
374
  agentsRun: newState.completedAgents.length,
313
375
  finalStatus: 'completed',
314
376
  deviationCount: (newState.modeTransitions || []).length,
315
377
  });
378
+ telemetryTrack('session_completed', {
379
+ sessionId: newState.sessionId || 'unknown',
380
+ pipelineType: pipelineType2,
381
+ mode: newState.phase === 'deploy' ? 'deploy' : 'build',
382
+ finalStage: newState.phase,
383
+ duration: totalDuration2,
384
+ agentCount: newState.completedAgents.length,
385
+ success: true,
386
+ });
316
387
  const flushedEvents2 = telemetryFlush();
317
388
  if (flushedEvents2.length > 0) {
318
389
  const tConfig2 = getTelemetryConfig(newState.targetDir || process.cwd());
@@ -24,7 +24,7 @@ export function getTelemetryConfig(targetDir) {
24
24
  const configPath = join(targetDir, 'chati.dev', 'config.yaml');
25
25
 
26
26
  const defaults = {
27
- enabled: false,
27
+ enabled: true,
28
28
  anonymousId: null,
29
29
  endpoint: 'https://chati-telemetry.vercel.app/api/events',
30
30
  apiKey: '10b0b54ba4f392fa46379ba778062ab0af5ca61e79609a7dce4aadd660104b56',
@@ -5,7 +5,7 @@
5
5
  * All telemetry is opt-in and anonymous.
6
6
  */
7
7
 
8
- export { TELEMETRY_EVENTS, validateEvent } from './schema.js';
8
+ export { TELEMETRY_EVENTS, validateEvent, EVENT_PROPERTY_RULES } from './schema.js';
9
9
  export { getTelemetryConfig, isEnabled, setEnabled, getAnonymousId } from './config.js';
10
10
  export { initCollector, track, flush, getBufferSize, getStatus } from './collector.js';
11
11
  export { sendEvents, DEFAULT_ENDPOINT } from './sender.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Telemetry event schema and validation.
3
3
  *
4
- * Defines the 6 event types collected by opt-in telemetry.
4
+ * Defines the 9 event types collected by telemetry.
5
5
  * Zero PII — only anonymous usage metrics.
6
6
  */
7
7
 
@@ -16,6 +16,9 @@ export const TELEMETRY_EVENTS = [
16
16
  'pipeline_completed',
17
17
  'circuit_breaker_triggered',
18
18
  'error_occurred',
19
+ 'session_started',
20
+ 'session_completed',
21
+ 'token_usage',
19
22
  ];
20
23
 
21
24
  // ---------------------------------------------------------------------------
@@ -44,18 +47,48 @@ const EVENT_PROPERTIES = {
44
47
  error_occurred: [
45
48
  'errorType', 'agent', 'provider', 'phase',
46
49
  ],
50
+ session_started: [
51
+ 'sessionId', 'pipelineType', 'mode',
52
+ ],
53
+ session_completed: [
54
+ 'sessionId', 'pipelineType', 'mode', 'finalStage',
55
+ 'duration', 'agentCount', 'success',
56
+ ],
57
+ token_usage: [
58
+ 'sessionId', 'agent', 'provider', 'model',
59
+ 'inputTokens', 'outputTokens', 'totalTokens', 'estimatedCostUsd',
60
+ ],
47
61
  };
48
62
 
49
63
  // ---------------------------------------------------------------------------
50
- // PII blocklist fields that MUST NEVER appear in telemetry
64
+ // Property Rules (required fields + numeric validation for structured types)
51
65
  // ---------------------------------------------------------------------------
52
66
 
53
- const PII_BLOCKLIST = [
54
- 'path', 'filePath', 'fileName', 'directory', 'cwd',
55
- 'apiKey', 'token', 'secret', 'password', 'credential',
56
- 'email', 'username', 'name', 'ip', 'hostname',
57
- 'content', 'code', 'source', 'prompt', 'message',
58
- 'stackTrace', 'stack',
67
+ export const EVENT_PROPERTY_RULES = {
68
+ session_started: {
69
+ required: ['sessionId', 'pipelineType', 'mode'],
70
+ },
71
+ session_completed: {
72
+ required: ['sessionId', 'pipelineType', 'mode', 'finalStage', 'duration', 'agentCount', 'success'],
73
+ numeric: ['duration', 'agentCount'],
74
+ },
75
+ token_usage: {
76
+ required: ['sessionId', 'agent', 'provider', 'model', 'inputTokens', 'outputTokens', 'totalTokens', 'estimatedCostUsd'],
77
+ numeric: ['inputTokens', 'outputTokens', 'totalTokens', 'estimatedCostUsd'],
78
+ },
79
+ };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // PII patterns — fields that MUST NEVER appear in telemetry
83
+ // Regex patterns aligned with chati-telemetry backend (case-insensitive)
84
+ // ---------------------------------------------------------------------------
85
+
86
+ const PII_PATTERNS = [
87
+ /^(file_?)?path$/i, /^file_?name$/i, /^dir(ectory)?$/i, /^cwd$/i,
88
+ /^api_?key$/i, /^(access_?)?token$/i, /^secret$/i, /^pass(word)?$/i,
89
+ /^cred(ential)?s?$/i, /^e?mail$/i, /^user_?name$/i, /^(full_?)?name$/i,
90
+ /^ip(_?addr(ess)?)?$/i, /^host_?name$/i, /^(source_?)?code$/i,
91
+ /^prompt$/i, /^message$/i, /^content$/i, /^stack_?trace$/i,
59
92
  ];
60
93
 
61
94
  // ---------------------------------------------------------------------------
@@ -81,9 +114,9 @@ export function validateEvent(event) {
81
114
 
82
115
  const props = event.properties || {};
83
116
 
84
- // Check for PII fields
117
+ // Check for PII field names (regex-based, aligned with backend)
85
118
  for (const key of Object.keys(props)) {
86
- if (PII_BLOCKLIST.includes(key)) {
119
+ if (PII_PATTERNS.some(pattern => pattern.test(key))) {
87
120
  errors.push(`PII field detected: "${key}" — must not be included in telemetry`);
88
121
  }
89
122
  }
@@ -91,7 +124,7 @@ export function validateEvent(event) {
91
124
  // Check for PII in values (paths, emails)
92
125
  for (const [key, value] of Object.entries(props)) {
93
126
  if (typeof value === 'string') {
94
- if (value.includes('/Users/') || value.includes('/home/') || value.includes('C:\\Users\\')) {
127
+ if (/\/users\//i.test(value) || /\\users\\/i.test(value) || /\/home\//i.test(value)) {
95
128
  errors.push(`PII detected in "${key}": value contains filesystem path`);
96
129
  }
97
130
  if (value.includes('@') && value.includes('.')) {
@@ -100,5 +133,21 @@ export function validateEvent(event) {
100
133
  }
101
134
  }
102
135
 
136
+ // Property-level validation for event types with rules
137
+ const rules = EVENT_PROPERTY_RULES[event.type];
138
+ if (rules && props) {
139
+ const missing = rules.required.filter(key => !(key in props));
140
+ if (missing.length > 0) {
141
+ errors.push(`Missing required properties for "${event.type}": ${missing.join(', ')}`);
142
+ }
143
+ if (rules.numeric) {
144
+ for (const key of rules.numeric) {
145
+ if (key in props && (typeof props[key] !== 'number' || props[key] < 0)) {
146
+ errors.push(`Property "${key}" must be a non-negative number`);
147
+ }
148
+ }
149
+ }
150
+ }
151
+
103
152
  return { valid: errors.length === 0, errors };
104
153
  }
@@ -18,7 +18,9 @@ import { buildAgentPrompt } from './prompt-builder.js';
18
18
  import { spawnTerminal } from './spawner.js';
19
19
  import { parseAgentOutput } from './handoff-parser.js';
20
20
  import { createCostTracker } from './cost-tracker.js';
21
- import { track as telemetryTrack } from '../telemetry/collector.js';
21
+ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
22
+ import { sendEvents } from '../telemetry/sender.js';
23
+ import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
22
24
 
23
25
  // ---------------------------------------------------------------------------
24
26
  // CLI argument parsing (no external deps)
@@ -62,6 +64,9 @@ async function main() {
62
64
  const projectDir = args['project-dir'] || process.cwd();
63
65
  const timeout = parseInt(args.timeout, 10) || 600_000; // default 10 minutes
64
66
 
67
+ // Initialize telemetry for this process
68
+ initCollector(isTelemetryEnabled(projectDir));
69
+
65
70
  // Load session state if available
66
71
  let sessionState = {};
67
72
  try {
@@ -117,6 +122,13 @@ async function main() {
117
122
  try {
118
123
  await waitForExit(handle, timeout);
119
124
  } catch (err) {
125
+ telemetryTrack('error_occurred', {
126
+ errorType: 'agent_failure',
127
+ agent: args.agent,
128
+ provider: promptResult.provider || args.provider || 'claude',
129
+ phase: sessionState?.phase || 'unknown',
130
+ });
131
+ await flushAndSend(projectDir);
120
132
  outputError(`Terminal execution failed: ${err.message}`);
121
133
  process.exit(2);
122
134
  }
@@ -156,6 +168,23 @@ async function main() {
156
168
  pipelineType: sessionState?.isQuickFlow ? 'quick-flow' : 'standard',
157
169
  });
158
170
 
171
+ // Track token usage if cost data is available
172
+ if (costRecord.inputTokens !== undefined) {
173
+ telemetryTrack('token_usage', {
174
+ sessionId: sessionState?.sessionId || 'unknown',
175
+ agent: args.agent,
176
+ provider: costRecord.provider,
177
+ model: costRecord.model || 'unknown',
178
+ inputTokens: costRecord.inputTokens || 0,
179
+ outputTokens: costRecord.outputTokens || 0,
180
+ totalTokens: (costRecord.inputTokens || 0) + (costRecord.outputTokens || 0),
181
+ estimatedCostUsd: costRecord.cost || 0,
182
+ });
183
+ }
184
+
185
+ // Flush telemetry before exit
186
+ await flushAndSend(projectDir);
187
+
159
188
  // Parse the handoff from stdout
160
189
  const parsed = parseAgentOutput(stdout);
161
190
 
@@ -193,6 +222,22 @@ async function main() {
193
222
  // Helpers
194
223
  // ---------------------------------------------------------------------------
195
224
 
225
+ /**
226
+ * Flush buffered telemetry events and send to the backend.
227
+ * Fire-and-forget — never blocks process exit on failure.
228
+ */
229
+ async function flushAndSend(projectDir) {
230
+ const events = telemetryFlush();
231
+ if (events.length === 0) return;
232
+
233
+ try {
234
+ const tConfig = getTelemetryConfig(projectDir);
235
+ await sendEvents(events, { ...tConfig, version: tConfig.chatiVersion || 'unknown' });
236
+ } catch {
237
+ // Silently fail — telemetry must never block agent execution
238
+ }
239
+ }
240
+
196
241
  /**
197
242
  * Wait for a terminal handle's process to exit.
198
243
  */
@@ -66,9 +66,14 @@ const FALLBACK_EN = {
66
66
  quick_start_1: 'Open your IDE',
67
67
  quick_start_2: 'Type: /chati',
68
68
  quick_start_3: 'The orchestrator will guide you through the process',
69
- telemetry_consent: 'Help improve Chati.dev by sharing anonymous usage data?',
70
- telemetry_description: 'Anonymous metrics only (agents used, pipeline duration, gate scores). Zero code, files, or personal data. You can disable anytime with: npx chati-dev telemetry disable',
71
- telemetry_privacy: 'Privacy: opt-in only, anonymous UUID, no PII collected.',
69
+ tos_title: 'Terms of Use',
70
+ tos_summary: 'By continuing, you accept the Chati.dev Terms of Use (see TERMS_OF_USE.md in the package).',
71
+ tos_telemetry_what: 'Chati.dev collects anonymous usage data: agents used, pipeline duration, gate scores. No code, files, or personal data is collected.',
72
+ tos_telemetry_optout: 'You can disable telemetry anytime with: npx chati-dev telemetry disable',
73
+ tos_accept: 'Do you accept the Terms of Use?',
74
+ tos_declined: 'Terms of Use declined. Installation cancelled.',
75
+ tos_telemetry_enabled: 'Telemetry: enabled (anonymous, opt-out — disable with: npx chati-dev telemetry disable)',
76
+ tos_telemetry_disabled: 'Telemetry: disabled',
72
77
  },
73
78
  agents: {
74
79
  starting: 'Starting agent: {agent}',
@@ -3,10 +3,13 @@ import { readFileSync } from 'fs';
3
3
  import { join, dirname, basename } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { logBanner } from '../utils/logger.js';
6
- import { stepLanguage, stepProjectType, stepIDESelection, stepProviderSelection, stepEditorSelection, stepPrimaryProvider, stepConfirmation, stepTelemetryConsent } from './questions.js';
6
+ import { stepLanguage, stepProjectType, stepProviderSelection, stepEditorSelection, stepPrimaryProvider, stepConfirmation, stepTermsOfUse } from './questions.js';
7
7
  import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
9
  import { validateInstallation } from '../installer/validator.js';
10
+ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
11
+ import { sendEvents } from '../telemetry/sender.js';
12
+ import { getTelemetryConfig } from '../telemetry/config.js';
10
13
  import { t } from './i18n.js';
11
14
  import { DEFAULT_MCPS } from '../config/mcp-configs.js';
12
15
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
@@ -33,7 +36,12 @@ export async function runWizard(targetDir, options = {}) {
33
36
 
34
37
  const language = options.language || await stepLanguage();
35
38
 
36
- // Step 2: Project Type
39
+ // Step 2: Terms of Use (must accept to continue)
40
+ if (options.telemetry === undefined) {
41
+ await stepTermsOfUse();
42
+ }
43
+
44
+ // Step 3: Project Type
37
45
  const projectType = options.projectType || await stepProjectType(targetDir);
38
46
 
39
47
  // Step 3a: Provider Selection (which AI providers to use)
@@ -73,14 +81,15 @@ export async function runWizard(targetDir, options = {}) {
73
81
 
74
82
  await stepConfirmation(config);
75
83
 
76
- // Step 5: Telemetry Consent (opt-in)
77
- const telemetryEnabled = options.telemetry !== undefined ? options.telemetry : await stepTelemetryConsent();
78
- config.telemetryEnabled = telemetryEnabled;
84
+ // Telemetry: enabled by default (opt-out model via ToS)
85
+ config.telemetryEnabled = options.telemetry !== undefined ? options.telemetry : true;
79
86
 
80
- // Step 5: Installation + Validation
87
+ // Installation + Validation
81
88
  const primaryIDE = selectedIDEs.find(ide => IDE_TO_PROVIDER[ide] === primaryProvider) || selectedIDEs[0];
82
89
  const primaryIDEName = IDE_CONFIGS[primaryIDE]?.name || primaryIDE;
83
90
 
91
+ const installStart = Date.now();
92
+
84
93
  console.log();
85
94
  const installSpinner = createSpinner(t('installer.installing'));
86
95
  installSpinner.start();
@@ -112,9 +121,10 @@ export async function runWizard(targetDir, options = {}) {
112
121
  showStep(t('installer.created_memories'));
113
122
  showStep(t('installer.installed_intelligence'));
114
123
  showStep(`${t('installer.configured_mcps')} ${selectedMCPs.join(', ')}`);
115
- if (telemetryEnabled) {
116
- showStep('Telemetry: enabled (anonymous, opt-in)');
117
- }
124
+ showStep(config.telemetryEnabled
125
+ ? t('installer.tos_telemetry_enabled')
126
+ : t('installer.tos_telemetry_disabled')
127
+ );
118
128
 
119
129
  // Validation
120
130
  console.log();
@@ -161,6 +171,22 @@ export async function runWizard(targetDir, options = {}) {
161
171
 
162
172
  showQuickStart(t('installer.quick_start_title'), quickStartSteps);
163
173
 
174
+ // Track installation telemetry
175
+ initCollector(config.telemetryEnabled);
176
+ telemetryTrack('installation_completed', {
177
+ providers: cliProviders,
178
+ editors: selectedEditors,
179
+ projectType,
180
+ language,
181
+ primaryProvider,
182
+ installDuration: Date.now() - installStart,
183
+ });
184
+ const installEvents = telemetryFlush();
185
+ if (installEvents.length > 0) {
186
+ const tConfig = getTelemetryConfig(targetDir);
187
+ sendEvents(installEvents, { ...tConfig, version: VERSION });
188
+ }
189
+
164
190
  return { success: true, config, validation };
165
191
  } catch (err) {
166
192
  installSpinner.stop();
@@ -295,23 +295,32 @@ export async function stepConfirmation(config) {
295
295
  }
296
296
 
297
297
  /**
298
- * Step 5: Telemetry Consent (opt-in)
298
+ * Step: Terms of Use Acceptance
299
299
  *
300
- * Asks the user if they want to share anonymous usage metrics.
301
- * Returns true (opt-in) or false (opt-out).
300
+ * Shows Terms of Use that include telemetry data collection notice.
301
+ * User must accept to proceed. Declining aborts installation.
302
+ * Telemetry is enabled by default (opt-out model).
302
303
  */
303
- export async function stepTelemetryConsent() {
304
+ export async function stepTermsOfUse() {
304
305
  p.note(
305
- `${dim(t('installer.telemetry_description'))}\n\n${dim(t('installer.telemetry_privacy'))}`,
306
- dim('Telemetry')
306
+ `${dim(t('installer.tos_summary'))}\n\n` +
307
+ `${dim(t('installer.tos_telemetry_what'))}\n\n` +
308
+ `${dim(t('installer.tos_telemetry_optout'))}`,
309
+ t('installer.tos_title')
307
310
  );
308
311
 
309
- const consent = await p.confirm({
310
- message: t('installer.telemetry_consent'),
312
+ const accepted = await p.confirm({
313
+ message: t('installer.tos_accept'),
311
314
  initialValue: true,
312
315
  });
313
316
 
314
- if (p.isCancel(consent)) return false;
317
+ if (p.isCancel(accepted) || !accepted) {
318
+ p.cancel(t('installer.tos_declined'));
319
+ process.exit(0);
320
+ }
315
321
 
316
- return consent;
322
+ return true;
317
323
  }
324
+
325
+ /** @deprecated Use stepTermsOfUse instead */
326
+ export const stepTelemetryConsent = stepTermsOfUse;