chati-dev 3.3.1 → 4.0.0
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/README.md +4 -4
- package/TERMS_OF_USE.md +56 -0
- package/framework/config.yaml +23 -3
- package/framework/constitution.md +12 -10
- package/framework/context/governance.md +2 -0
- package/framework/context/root.md +1 -1
- package/framework/intelligence/context-engine.md +22 -17
- package/package.json +3 -2
- package/scripts/doctor/checks/agents.js +77 -0
- package/scripts/doctor/checks/constitution.js +41 -0
- package/scripts/doctor/checks/domain-alignment.js +58 -0
- package/scripts/doctor/checks/prism-layers.js +84 -0
- package/scripts/doctor/checks/registry.js +55 -0
- package/scripts/doctor/checks/schemas.js +61 -0
- package/scripts/doctor/fixes/reference-fix.js +100 -0
- package/scripts/doctor/fixes/registry-fix.js +56 -0
- package/scripts/doctor/index.js +212 -0
- package/scripts/health-check.js +8 -8
- package/src/autonomy/surface-criteria.js +226 -0
- package/src/context/bracket-tracker.js +44 -13
- package/src/context/domain-loader.js +22 -0
- package/src/context/engine.js +18 -7
- package/src/context/formatter.js +21 -1
- package/src/context/layers/l5-keywords.js +53 -0
- package/src/installer/templates.js +1 -1
- package/src/intelligence/context-status.js +20 -9
- package/src/intelligence/decision-engine.js +253 -0
- package/src/orchestrator/pipeline-manager.js +80 -9
- package/src/telemetry/config.js +1 -1
- package/src/telemetry/index.js +1 -1
- package/src/telemetry/schema.js +60 -11
- package/src/terminal/prompt-builder.js +341 -1
- package/src/terminal/run-agent.js +61 -1
- package/src/terminal/run-parallel.js +77 -5
- package/src/terminal/spawner.js +13 -0
- package/src/utils/feature-flags.js +106 -0
- package/src/wizard/i18n.js +8 -3
- package/src/wizard/index.js +35 -9
- package/src/wizard/questions.js +19 -10
|
@@ -18,7 +18,10 @@ 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 {
|
|
21
|
+
import { getRateLimiter } from './rate-limiter.js';
|
|
22
|
+
import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
|
|
23
|
+
import { sendEvents } from '../telemetry/sender.js';
|
|
24
|
+
import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
|
|
22
25
|
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
24
27
|
// CLI argument parsing (no external deps)
|
|
@@ -62,6 +65,9 @@ async function main() {
|
|
|
62
65
|
const projectDir = args['project-dir'] || process.cwd();
|
|
63
66
|
const timeout = parseInt(args.timeout, 10) || 600_000; // default 10 minutes
|
|
64
67
|
|
|
68
|
+
// Initialize telemetry for this process
|
|
69
|
+
initCollector(isTelemetryEnabled(projectDir));
|
|
70
|
+
|
|
65
71
|
// Load session state if available
|
|
66
72
|
let sessionState = {};
|
|
67
73
|
try {
|
|
@@ -94,6 +100,20 @@ async function main() {
|
|
|
94
100
|
process.exit(1);
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
// Wait for rate limit slot before spawning
|
|
104
|
+
const spawnProvider = promptResult.provider || args.provider || 'claude';
|
|
105
|
+
const limiter = getRateLimiter(spawnProvider);
|
|
106
|
+
if (!limiter.canSpawn()) {
|
|
107
|
+
const rateLimitStart = Date.now();
|
|
108
|
+
await limiter.waitForSlot();
|
|
109
|
+
const rateLimitWait = Date.now() - rateLimitStart;
|
|
110
|
+
telemetryTrack('rate_limit_wait', {
|
|
111
|
+
agent: args.agent,
|
|
112
|
+
provider: spawnProvider,
|
|
113
|
+
waitMs: rateLimitWait,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
// Spawn the agent terminal
|
|
98
118
|
const startTime = Date.now();
|
|
99
119
|
let handle;
|
|
@@ -117,6 +137,13 @@ async function main() {
|
|
|
117
137
|
try {
|
|
118
138
|
await waitForExit(handle, timeout);
|
|
119
139
|
} catch (err) {
|
|
140
|
+
telemetryTrack('error_occurred', {
|
|
141
|
+
errorType: 'agent_failure',
|
|
142
|
+
agent: args.agent,
|
|
143
|
+
provider: promptResult.provider || args.provider || 'claude',
|
|
144
|
+
phase: sessionState?.phase || 'unknown',
|
|
145
|
+
});
|
|
146
|
+
await flushAndSend(projectDir);
|
|
120
147
|
outputError(`Terminal execution failed: ${err.message}`);
|
|
121
148
|
process.exit(2);
|
|
122
149
|
}
|
|
@@ -156,6 +183,23 @@ async function main() {
|
|
|
156
183
|
pipelineType: sessionState?.isQuickFlow ? 'quick-flow' : 'standard',
|
|
157
184
|
});
|
|
158
185
|
|
|
186
|
+
// Track token usage if cost data is available
|
|
187
|
+
if (costRecord.inputTokens !== undefined) {
|
|
188
|
+
telemetryTrack('token_usage', {
|
|
189
|
+
sessionId: sessionState?.sessionId || 'unknown',
|
|
190
|
+
agent: args.agent,
|
|
191
|
+
provider: costRecord.provider,
|
|
192
|
+
model: costRecord.model || 'unknown',
|
|
193
|
+
inputTokens: costRecord.inputTokens || 0,
|
|
194
|
+
outputTokens: costRecord.outputTokens || 0,
|
|
195
|
+
totalTokens: (costRecord.inputTokens || 0) + (costRecord.outputTokens || 0),
|
|
196
|
+
estimatedCostUsd: costRecord.cost || 0,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Flush telemetry before exit
|
|
201
|
+
await flushAndSend(projectDir);
|
|
202
|
+
|
|
159
203
|
// Parse the handoff from stdout
|
|
160
204
|
const parsed = parseAgentOutput(stdout);
|
|
161
205
|
|
|
@@ -193,6 +237,22 @@ async function main() {
|
|
|
193
237
|
// Helpers
|
|
194
238
|
// ---------------------------------------------------------------------------
|
|
195
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Flush buffered telemetry events and send to the backend.
|
|
242
|
+
* Fire-and-forget — never blocks process exit on failure.
|
|
243
|
+
*/
|
|
244
|
+
async function flushAndSend(projectDir) {
|
|
245
|
+
const events = telemetryFlush();
|
|
246
|
+
if (events.length === 0) return;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const tConfig = getTelemetryConfig(projectDir);
|
|
250
|
+
await sendEvents(events, { ...tConfig, version: tConfig.chatiVersion || 'unknown' });
|
|
251
|
+
} catch {
|
|
252
|
+
// Silently fail — telemetry must never block agent execution
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
196
256
|
/**
|
|
197
257
|
* Wait for a terminal handle's process to exit.
|
|
198
258
|
*/
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import { fileURLToPath } from 'url';
|
|
18
18
|
import { buildAgentPrompt } from './prompt-builder.js';
|
|
19
|
-
import { spawnParallelGroup } from './spawner.js';
|
|
19
|
+
import { spawnParallelGroup, spawnTerminal } from './spawner.js';
|
|
20
20
|
import { TerminalMonitor } from './monitor.js';
|
|
21
21
|
import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './collector.js';
|
|
22
22
|
import { parseAgentOutput } from './handoff-parser.js';
|
|
23
23
|
import { estimateTokens, COST_PER_1K } from './cost-tracker.js';
|
|
24
|
+
import { getRateLimiter } from './rate-limiter.js';
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// CLI argument parsing
|
|
@@ -119,13 +120,29 @@ async function main() {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
//
|
|
123
|
+
// Check rate limit capacity before spawning
|
|
124
|
+
const groupProvider = configs[0]?.provider || 'claude';
|
|
125
|
+
const limiter = getRateLimiter(groupProvider);
|
|
126
|
+
const rateStats = limiter.getStats();
|
|
127
|
+
const availableSlots = rateStats.limit - rateStats.used;
|
|
128
|
+
if (availableSlots < configs.length) {
|
|
129
|
+
console.error(`[chati] Rate limiter: ${availableSlots} slots available for ${configs.length} agents. Spawns may be throttled.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Spawn all terminals in parallel (with sequential fallback)
|
|
123
133
|
let group;
|
|
134
|
+
let fallbackUsed = false;
|
|
124
135
|
try {
|
|
125
136
|
group = spawnParallelGroup(configs);
|
|
126
137
|
} catch (err) {
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
console.error(`[chati] Parallel spawn failed: ${err.message}. Falling back to sequential execution.`);
|
|
139
|
+
try {
|
|
140
|
+
group = await sequentialFallback(configs, timeout);
|
|
141
|
+
fallbackUsed = true;
|
|
142
|
+
} catch (fallbackErr) {
|
|
143
|
+
outputError(`Sequential fallback also failed: ${fallbackErr.message}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
// Monitor until completion
|
|
@@ -218,6 +235,7 @@ async function main() {
|
|
|
218
235
|
timeSaved: elapsed * (agents.length - 1),
|
|
219
236
|
},
|
|
220
237
|
costEstimate: costEstimates,
|
|
238
|
+
fallbackUsed,
|
|
221
239
|
};
|
|
222
240
|
|
|
223
241
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
@@ -228,6 +246,60 @@ async function main() {
|
|
|
228
246
|
// Helpers
|
|
229
247
|
// ---------------------------------------------------------------------------
|
|
230
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Sequential fallback: spawn agents one at a time when parallel spawning fails.
|
|
251
|
+
* Produces the same group structure as spawnParallelGroup for transparent handling.
|
|
252
|
+
*
|
|
253
|
+
* @param {object[]} configs - Agent spawn configurations
|
|
254
|
+
* @param {number} timeout - Per-agent timeout in ms
|
|
255
|
+
* @returns {Promise<{ groupId: string, terminals: object[] }>}
|
|
256
|
+
*/
|
|
257
|
+
async function sequentialFallback(configs, timeout) {
|
|
258
|
+
const groupId = `seq-fallback-${Date.now()}`;
|
|
259
|
+
const terminals = [];
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < configs.length; i++) {
|
|
262
|
+
const cfg = configs[i];
|
|
263
|
+
const terminal = spawnTerminal({
|
|
264
|
+
agent: cfg.agent,
|
|
265
|
+
taskId: cfg.taskId,
|
|
266
|
+
model: cfg.model,
|
|
267
|
+
provider: cfg.provider,
|
|
268
|
+
prompt: cfg.prompt,
|
|
269
|
+
workingDir: cfg.workingDir,
|
|
270
|
+
timeout: cfg.timeout || timeout,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Wait for this terminal to finish before spawning the next
|
|
274
|
+
await new Promise((resolve) => {
|
|
275
|
+
const timer = setTimeout(() => {
|
|
276
|
+
terminal.kill?.();
|
|
277
|
+
resolve();
|
|
278
|
+
}, (cfg.timeout || timeout) + 5_000);
|
|
279
|
+
|
|
280
|
+
terminal.onExit?.(() => {
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
resolve();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// If terminal doesn't have onExit, resolve after a short poll
|
|
286
|
+
if (!terminal.onExit) {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
resolve();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
terminals.push(terminal);
|
|
293
|
+
|
|
294
|
+
// Small delay between spawns to avoid rate limit pressure
|
|
295
|
+
if (i < configs.length - 1) {
|
|
296
|
+
await new Promise(r => setTimeout(r, 500));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { groupId, terminals };
|
|
301
|
+
}
|
|
302
|
+
|
|
231
303
|
/**
|
|
232
304
|
* Determine the next sequential agent after a parallel group.
|
|
233
305
|
* GROUP 1 (detail+architect+ux) → phases
|
|
@@ -256,4 +328,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
256
328
|
});
|
|
257
329
|
}
|
|
258
330
|
|
|
259
|
-
export { parseArgs, determineNextAgent };
|
|
331
|
+
export { parseArgs, determineNextAgent, sequentialFallback };
|
package/src/terminal/spawner.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { spawn } from 'child_process';
|
|
11
11
|
import { validateWriteScopes, buildIsolationEnv } from './isolation.js';
|
|
12
12
|
import { getProvider } from './cli-registry.js';
|
|
13
|
+
import { getRateLimiter } from './rate-limiter.js';
|
|
13
14
|
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Constants
|
|
@@ -234,6 +235,10 @@ export function spawnTerminal(config) {
|
|
|
234
235
|
timeout,
|
|
235
236
|
};
|
|
236
237
|
|
|
238
|
+
// Record spawn in rate limiter for throttling
|
|
239
|
+
const providerForRate = config.provider || 'claude';
|
|
240
|
+
getRateLimiter(providerForRate).recordSpawn();
|
|
241
|
+
|
|
237
242
|
// Capture output (capped at ~10MB to prevent unbounded memory growth)
|
|
238
243
|
const MAX_BUFFER_CHUNKS = 10_000;
|
|
239
244
|
if (child.stdout) {
|
|
@@ -297,6 +302,14 @@ export function spawnParallelGroup(configs) {
|
|
|
297
302
|
throw new Error(`Write scope conflicts detected: ${details}`);
|
|
298
303
|
}
|
|
299
304
|
|
|
305
|
+
// Preemptive rate limit capacity check
|
|
306
|
+
const groupProvider = configs[0]?.provider || 'claude';
|
|
307
|
+
const limiter = getRateLimiter(groupProvider);
|
|
308
|
+
const stats = limiter.getStats();
|
|
309
|
+
if (stats.used + configs.length > stats.limit) {
|
|
310
|
+
console.error(`[chati] Rate limit warning: ${stats.used}/${stats.limit} slots used, requesting ${configs.length} more`);
|
|
311
|
+
}
|
|
312
|
+
|
|
300
313
|
const groupId = `group-${Date.now()}`;
|
|
301
314
|
const terminals = configs.map(cfg => spawnTerminal(cfg));
|
|
302
315
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Feature flag reader for chati.dev framework.
|
|
3
|
+
*
|
|
4
|
+
* Reads the `features:` section from config.yaml and returns boolean
|
|
5
|
+
* values for each feature toggle. All features default to false when
|
|
6
|
+
* not explicitly set.
|
|
7
|
+
*
|
|
8
|
+
* Uses lightweight regex-based parsing consistent with config-parser.js.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* All known feature flags with their default values.
|
|
16
|
+
* New features start as false (opt-in).
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULTS = {
|
|
19
|
+
hybrid_budget: false,
|
|
20
|
+
anti_dash: false,
|
|
21
|
+
rate_limiter_integration: false,
|
|
22
|
+
l5_keywords: false,
|
|
23
|
+
prompt_size_guard: false,
|
|
24
|
+
ids_decision_engine: false,
|
|
25
|
+
surface_criteria: false,
|
|
26
|
+
parallel_fallback: false,
|
|
27
|
+
tool_mesh: false,
|
|
28
|
+
tech_presets: false,
|
|
29
|
+
doctor_autofix: false,
|
|
30
|
+
brandbook: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a specific feature is enabled.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} projectDir - Project root directory (contains chati.dev/)
|
|
37
|
+
* @param {string} featureName - Feature flag name (e.g., 'hybrid_budget')
|
|
38
|
+
* @returns {boolean} True if feature is enabled, false otherwise
|
|
39
|
+
*/
|
|
40
|
+
export function isFeatureEnabled(projectDir, featureName) {
|
|
41
|
+
const features = getEnabledFeatures(projectDir);
|
|
42
|
+
return features[featureName] ?? DEFAULTS[featureName] ?? false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all feature flags with their current values.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} projectDir - Project root directory (contains chati.dev/)
|
|
49
|
+
* @returns {Record<string, boolean>} Map of feature name to enabled status
|
|
50
|
+
*/
|
|
51
|
+
export function getEnabledFeatures(projectDir) {
|
|
52
|
+
const configPath = join(projectDir, 'chati.dev', 'config.yaml');
|
|
53
|
+
if (!existsSync(configPath)) {
|
|
54
|
+
return { ...DEFAULTS };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
58
|
+
return parseFeaturesSection(raw);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse the features section from raw config.yaml content.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} raw - Raw YAML content
|
|
65
|
+
* @returns {Record<string, boolean>} Parsed feature flags merged with defaults
|
|
66
|
+
*/
|
|
67
|
+
export function parseFeaturesSection(raw) {
|
|
68
|
+
const result = { ...DEFAULTS };
|
|
69
|
+
|
|
70
|
+
// Find the features: block
|
|
71
|
+
const featuresMatch = raw.match(/^features:\s*\n((?:\s+\w[\w]*:\s*.+\n?)*)/m);
|
|
72
|
+
if (!featuresMatch) return result;
|
|
73
|
+
|
|
74
|
+
const block = featuresMatch[1];
|
|
75
|
+
|
|
76
|
+
// Extract each key: value pair
|
|
77
|
+
const linePattern = /^\s+(\w[\w]*):\s*(true|false)\s*$/gm;
|
|
78
|
+
let match;
|
|
79
|
+
while ((match = linePattern.exec(block)) !== null) {
|
|
80
|
+
const key = match[1];
|
|
81
|
+
const value = match[2] === 'true';
|
|
82
|
+
if (key in result) {
|
|
83
|
+
result[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the list of known feature flag names.
|
|
92
|
+
*
|
|
93
|
+
* @returns {string[]} Array of feature flag names
|
|
94
|
+
*/
|
|
95
|
+
export function getFeatureNames() {
|
|
96
|
+
return Object.keys(DEFAULTS);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get default values for all feature flags.
|
|
101
|
+
*
|
|
102
|
+
* @returns {Record<string, boolean>} Default feature flag values
|
|
103
|
+
*/
|
|
104
|
+
export function getDefaults() {
|
|
105
|
+
return { ...DEFAULTS };
|
|
106
|
+
}
|
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
|
@@ -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.
|
|
312
|
+
const accepted = await p.confirm({
|
|
313
|
+
message: t('installer.tos_accept'),
|
|
311
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;
|