chati-dev 3.3.0 → 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.
- package/TERMS_OF_USE.md +56 -0
- package/framework/config.yaml +8 -3
- package/package.json +3 -2
- package/src/installer/templates.js +1 -1
- package/src/orchestrator/pipeline-manager.js +80 -9
- package/src/quality/test-runner.js +4 -4
- package/src/telemetry/config.js +1 -1
- package/src/telemetry/index.js +1 -1
- package/src/telemetry/schema.js +60 -11
- package/src/terminal/run-agent.js +46 -1
- package/src/wizard/i18n.js +8 -3
- package/src/wizard/index.js +35 -9
- package/src/wizard/questions.js +24 -15
package/TERMS_OF_USE.md
ADDED
|
@@ -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.
|
package/framework/config.yaml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# chati.dev Configuration
|
|
2
|
-
version: "3.3.
|
|
2
|
+
version: "3.3.2"
|
|
3
3
|
installed_at: "2026-02-07T10:00:00Z"
|
|
4
|
-
updated_at: "2026-03-
|
|
5
|
-
installer_version: "3.3.
|
|
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.
|
|
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",
|
|
@@ -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:
|
|
250
|
-
totalDuration:
|
|
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:
|
|
311
|
-
totalDuration:
|
|
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());
|
|
@@ -136,8 +136,7 @@ export function runTests(projectDir, options = {}) {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
const start = Date.now();
|
|
139
|
-
let rawOutput
|
|
140
|
-
let exitCode = 0;
|
|
139
|
+
let rawOutput, exitCode;
|
|
141
140
|
|
|
142
141
|
try {
|
|
143
142
|
rawOutput = execSync(command, {
|
|
@@ -146,6 +145,7 @@ export function runTests(projectDir, options = {}) {
|
|
|
146
145
|
timeout,
|
|
147
146
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
148
147
|
});
|
|
148
|
+
exitCode = 0;
|
|
149
149
|
} catch (err) {
|
|
150
150
|
exitCode = err.status || 1;
|
|
151
151
|
rawOutput = (err.stdout || '') + (err.stderr || '');
|
|
@@ -208,8 +208,7 @@ export function runLint(projectDir, options = {}) {
|
|
|
208
208
|
return { success: true, errors: 0, warnings: 0, rawOutput: 'No lint command detected' };
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
let rawOutput
|
|
212
|
-
let exitCode = 0;
|
|
211
|
+
let rawOutput, exitCode;
|
|
213
212
|
|
|
214
213
|
try {
|
|
215
214
|
rawOutput = execSync(command, {
|
|
@@ -218,6 +217,7 @@ export function runLint(projectDir, options = {}) {
|
|
|
218
217
|
timeout,
|
|
219
218
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
220
219
|
});
|
|
220
|
+
exitCode = 0;
|
|
221
221
|
} catch (err) {
|
|
222
222
|
exitCode = err.status || 1;
|
|
223
223
|
rawOutput = (err.stdout || '') + (err.stderr || '');
|
package/src/telemetry/config.js
CHANGED
|
@@ -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:
|
|
27
|
+
enabled: true,
|
|
28
28
|
anonymousId: null,
|
|
29
29
|
endpoint: 'https://chati-telemetry.vercel.app/api/events',
|
|
30
30
|
apiKey: '10b0b54ba4f392fa46379ba778062ab0af5ca61e79609a7dce4aadd660104b56',
|
package/src/telemetry/index.js
CHANGED
|
@@ -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';
|
package/src/telemetry/schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Telemetry event schema and validation.
|
|
3
3
|
*
|
|
4
|
-
* Defines the
|
|
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
|
-
//
|
|
64
|
+
// Property Rules (required fields + numeric validation for structured types)
|
|
51
65
|
// ---------------------------------------------------------------------------
|
|
52
66
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
117
|
+
// Check for PII field names (regex-based, aligned with backend)
|
|
85
118
|
for (const key of Object.keys(props)) {
|
|
86
|
-
if (
|
|
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 (
|
|
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
|
*/
|
package/src/wizard/i18n.js
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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}',
|
package/src/wizard/index.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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
|
-
//
|
|
77
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
116
|
-
|
|
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();
|
package/src/wizard/questions.js
CHANGED
|
@@ -80,7 +80,7 @@ export async function stepProviderSelection() {
|
|
|
80
80
|
|
|
81
81
|
for (const [key, config] of cliIDEs) {
|
|
82
82
|
const provider = IDE_TO_PROVIDER[key];
|
|
83
|
-
let installed
|
|
83
|
+
let installed;
|
|
84
84
|
try {
|
|
85
85
|
installed = provider ? await isProviderAvailable(provider) : false;
|
|
86
86
|
} catch {
|
|
@@ -96,7 +96,7 @@ export async function stepProviderSelection() {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
const selected = await p.multiselect({
|
|
99
|
-
message: t('installer.provider_selection_title'),
|
|
99
|
+
message: `${t('installer.provider_selection_title')} ${dim('(space to select, enter to confirm)')}`,
|
|
100
100
|
options,
|
|
101
101
|
required: true,
|
|
102
102
|
});
|
|
@@ -127,7 +127,7 @@ export async function stepEditorSelection() {
|
|
|
127
127
|
}));
|
|
128
128
|
|
|
129
129
|
const selected = await p.multiselect({
|
|
130
|
-
message: t('installer.editor_selection_title'),
|
|
130
|
+
message: `${t('installer.editor_selection_title')} ${dim('(space to select, enter to confirm)')}`,
|
|
131
131
|
options,
|
|
132
132
|
required: false,
|
|
133
133
|
});
|
|
@@ -187,7 +187,7 @@ export async function stepIDESelection() {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
const selectedIDEs = await p.multiselect({
|
|
190
|
-
message: t('installer.ide_selection_title'),
|
|
190
|
+
message: `${t('installer.ide_selection_title')} ${dim('(space to select, enter to confirm)')}`,
|
|
191
191
|
options,
|
|
192
192
|
required: true,
|
|
193
193
|
});
|
|
@@ -295,23 +295,32 @@ export async function stepConfirmation(config) {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
|
-
* Step
|
|
298
|
+
* Step: Terms of Use Acceptance
|
|
299
299
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
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
|
|
304
|
+
export async function stepTermsOfUse() {
|
|
304
305
|
p.note(
|
|
305
|
-
`${dim(t('installer.
|
|
306
|
-
dim('
|
|
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
|
|
310
|
-
message: t('installer.
|
|
311
|
-
initialValue:
|
|
312
|
+
const accepted = await p.confirm({
|
|
313
|
+
message: t('installer.tos_accept'),
|
|
314
|
+
initialValue: true,
|
|
312
315
|
});
|
|
313
316
|
|
|
314
|
-
if (p.isCancel(
|
|
317
|
+
if (p.isCancel(accepted) || !accepted) {
|
|
318
|
+
p.cancel(t('installer.tos_declined'));
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
315
321
|
|
|
316
|
-
return
|
|
322
|
+
return true;
|
|
317
323
|
}
|
|
324
|
+
|
|
325
|
+
/** @deprecated Use stepTermsOfUse instead */
|
|
326
|
+
export const stepTelemetryConsent = stepTermsOfUse;
|