@omnitype-code/cli 0.1.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/src/index.ts ADDED
@@ -0,0 +1,537 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs';
6
+ import * as os from 'os';
7
+ import inquirer from 'inquirer';
8
+ import chalk from 'chalk';
9
+ import { ApiClient } from './core/ApiClient';
10
+ import { ModelDetector } from './core/ModelDetector';
11
+ import { installHooks, uninstallHooks, commitScan } from './hooks';
12
+ import { startDaemon } from './daemon';
13
+ import { runBlame } from './blame';
14
+ import { fetchNotes, pushNotes } from './core/GitNotes';
15
+ import { UI, COLORS } from './core/UI';
16
+
17
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
18
+ const program = new Command();
19
+
20
+ program
21
+ .name('omnitype')
22
+ .description('Code provenance tracking — works with any editor or AI tool')
23
+ .version(pkg.version);
24
+
25
+ // ── omnitype login ──────────────────────────────────────────────────────────
26
+ program
27
+ .command('login')
28
+ .description('Sign in to OmniType')
29
+ .option('--email <email>', 'Email address')
30
+ .option('--password <password>', 'Password')
31
+ .action(async (opts) => {
32
+ console.log(UI.banner());
33
+
34
+ const api = new ApiClient();
35
+
36
+ const answers = await inquirer.prompt([
37
+ {
38
+ type: 'input',
39
+ name: 'email',
40
+ message: 'Email address:',
41
+ when: !opts.email,
42
+ validate: (input: string) => input.includes('@') || 'Please enter a valid email',
43
+ },
44
+ {
45
+ type: 'password',
46
+ name: 'password',
47
+ message: 'Password:',
48
+ mask: '*',
49
+ when: !opts.password,
50
+ }
51
+ ]);
52
+
53
+ const email = opts.email || answers.email;
54
+ const password = opts.password || answers.password;
55
+
56
+ const spinner = UI.spinner('Authenticating with OmniType Cloud...');
57
+ try {
58
+ const username = await api.login(email, password);
59
+ spinner.succeed(`Welcome back, ${UI.bold(username)}!`);
60
+ UI.success('You are now signed in.');
61
+ } catch (err: any) {
62
+ spinner.fail('Authentication failed');
63
+ UI.error(err.message || String(err));
64
+ process.exit(1);
65
+ }
66
+ });
67
+
68
+ // ── omnitype logout ─────────────────────────────────────────────────────────
69
+ program
70
+ .command('logout')
71
+ .description('Sign out')
72
+ .action(() => {
73
+ new ApiClient().logout();
74
+ UI.success('Signed out successfully.');
75
+ });
76
+
77
+ // ── omnitype status ─────────────────────────────────────────────────────────
78
+ program
79
+ .command('status')
80
+ .description('Show current auth and model detection status')
81
+ .action(() => {
82
+ const api = new ApiClient();
83
+ const detection = new ModelDetector().detect();
84
+
85
+ let content = '';
86
+
87
+ // Account Section
88
+ if (api.isSignedIn) {
89
+ content += `${chalk.bold('Account:')} ${chalk.cyan(api.username)}\n`;
90
+ content += `${chalk.bold('Server:')} ${UI.dim(api.apiUrl)}\n`;
91
+ } else {
92
+ content += `${chalk.bold('Account:')} ${chalk.red('Not signed in')} ${UI.dim('(run: omnitype login)')}\n`;
93
+ }
94
+
95
+ content += `\n`;
96
+
97
+ // Detection Section
98
+ content += `${chalk.bold('Current Context:')}\n`;
99
+ const modelColor = detection.model.includes('claude') ? '#D97757' : (detection.model.includes('gpt') ? '#10A37F' : COLORS.ai);
100
+ content += ` ${chalk.bold('Model:')} ${chalk.hex(modelColor)(detection.model)}\n`;
101
+ content += ` ${chalk.bold('Tool:')} ${chalk.white(detection.tool)}\n`;
102
+
103
+ const confColors: Record<string, any> = {
104
+ deterministic: chalk.green('Deterministic'),
105
+ high: chalk.green('High'),
106
+ medium: chalk.yellow('Medium'),
107
+ low: chalk.red('Low'),
108
+ };
109
+ content += ` ${chalk.bold('Conf:')} ${confColors[detection.confidence] || detection.confidence}\n`;
110
+
111
+ // Repo Section
112
+ try {
113
+ const gitBranch = require('child_process').execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
114
+ const gitRepo = path.basename(process.cwd());
115
+ content += `\n`;
116
+ content += `${chalk.bold('Repository:')}\n`;
117
+ content += ` ${chalk.bold('Project:')} ${gitRepo}\n`;
118
+ content += ` ${chalk.bold('Branch:')} ${chalk.magenta(gitBranch)}`;
119
+ } catch {}
120
+
121
+ console.log(UI.box(content, `${UI.logo()} Status`));
122
+ });
123
+
124
+ // ── omnitype daemon ─────────────────────────────────────────────────────────
125
+ program
126
+ .command('daemon')
127
+ .description('Watch a directory and track provenance in real time')
128
+ .option('-p, --path <dir>', 'Directory to watch (default: cwd)', process.cwd())
129
+ .option('-n, --project <name>', 'Project name (default: directory name)')
130
+ .option('-b, --branch <name>', 'Branch name (default: detected from git)')
131
+ .action((opts) => {
132
+ const watchPath = path.resolve(opts.path ?? process.cwd());
133
+ const projectName = opts.project ?? path.basename(watchPath);
134
+
135
+ UI.info(`Starting OmniType Sentinel for ${UI.bold(projectName)}...`);
136
+ UI.dim(`Watching: ${watchPath}`);
137
+
138
+ startDaemon({ watchPath, projectName, branch: opts.branch });
139
+ });
140
+
141
+ // ── omnitype hooks ──────────────────────────────────────────────────────────
142
+ program
143
+ .command('hooks')
144
+ .description('Manage git hooks for commit-level provenance tracking')
145
+ .addCommand(
146
+ new Command('install')
147
+ .description('Install post-commit hook in the current repo')
148
+ .option('--repo <path>', 'Repo path', process.cwd())
149
+ .action((opts) => {
150
+ try {
151
+ installHooks(opts.repo);
152
+ UI.success('Git hooks installed successfully.');
153
+ } catch (err) {
154
+ UI.error(String(err));
155
+ process.exit(1);
156
+ }
157
+ })
158
+ )
159
+ .addCommand(
160
+ new Command('uninstall')
161
+ .description('Remove omnitype hook from post-commit')
162
+ .option('--repo <path>', 'Repo path', process.cwd())
163
+ .action((opts) => {
164
+ try {
165
+ uninstallHooks(opts.repo);
166
+ UI.success('Git hooks removed.');
167
+ } catch (err) {
168
+ UI.error(String(err));
169
+ process.exit(1);
170
+ }
171
+ })
172
+ );
173
+
174
+ // ── omnitype blame ──────────────────────────────────────────────────────────
175
+ program
176
+ .command('blame <file>')
177
+ .description('Enhanced git blame with AI/model attribution overlay')
178
+ .option('--no-color', 'Disable color output')
179
+ .option('--stats', 'Show attribution summary after blame output')
180
+ .option('--repo <path>', 'Repo root path (default: auto-detected)')
181
+ .action(async (file, opts) => {
182
+ await runBlame({ file, repoPath: opts.repo, noColor: !opts.color, showStats: opts.stats });
183
+ });
184
+
185
+ // ── omnitype notes fetch/push ───────────────────────────────────────────────
186
+ program
187
+ .command('notes')
188
+ .description('Sync git notes with remote')
189
+ .addCommand(
190
+ new Command('fetch')
191
+ .description('Fetch attribution notes from remote')
192
+ .option('--remote <name>', 'Remote name', 'origin')
193
+ .option('--repo <path>', 'Repo path', process.cwd())
194
+ .action(async (opts) => {
195
+ const spinner = UI.spinner(`Fetching notes from ${opts.remote}...`);
196
+ try {
197
+ await fetchNotes(opts.repo, opts.remote);
198
+ spinner.succeed('Notes fetched.');
199
+ } catch (err) {
200
+ spinner.fail('Fetch failed');
201
+ UI.error(String(err));
202
+ }
203
+ })
204
+ )
205
+ .addCommand(
206
+ new Command('push')
207
+ .description('Push attribution notes to remote')
208
+ .option('--remote <name>', 'Remote name', 'origin')
209
+ .option('--repo <path>', 'Repo path', process.cwd())
210
+ .action(async (opts) => {
211
+ const spinner = UI.spinner(`Pushing notes to ${opts.remote}...`);
212
+ try {
213
+ await pushNotes(opts.repo, opts.remote);
214
+ spinner.succeed('Notes pushed.');
215
+ } catch (err) {
216
+ spinner.fail('Push failed');
217
+ UI.error(String(err));
218
+ }
219
+ })
220
+ );
221
+
222
+ // ── omnitype commit-scan ────────────────────────────────────────────────────
223
+ program
224
+ .command('commit-scan')
225
+ .description('Scan the latest commit and push provenance (called by git hook)')
226
+ .option('--repo <path>', 'Repo path', process.cwd())
227
+ .action(async (opts) => {
228
+ try {
229
+ await commitScan(path.resolve(opts.repo));
230
+ } catch (err) {
231
+ UI.error(String(err));
232
+ }
233
+ });
234
+
235
+ // ── omnitype setup-claude-hook ──────────────────────────────────────────────
236
+ program
237
+ .command('setup-claude-hook')
238
+ .description('Install OmniType model-detection hook into ~/.claude/settings.json')
239
+ .option('--print', 'Print the hook JSON instead of writing it')
240
+ .action((opts) => {
241
+ const cfgPath = path.join(os.homedir(), '.claude', 'settings.json');
242
+ const hookEntry = {
243
+ matcher: 'Write|Edit|MultiEdit|NotebookEdit',
244
+ hooks: [{
245
+ type: 'command',
246
+ command: `node -e "const fs=require('fs'),os=require('os'),p=require('path');const d=p.join(os.homedir(),'.omnitype');fs.mkdirSync(d,{recursive:true});let m=process.env.CLAUDE_MODEL||process.env.ANTHROPIC_MODEL;if(!m){try{const s=JSON.parse(fs.readFileSync(p.join(os.homedir(),'.claude','settings.json'),'utf8'));m=s.model||s.defaultModel;}catch{}}fs.writeFileSync(p.join(d,'active-model.json'),JSON.stringify({model:m||'claude-sonnet-4-6',tool:'claude-code',ts:Date.now()}))"`,
247
+ }],
248
+ };
249
+
250
+ if (opts.print) {
251
+ console.log(JSON.stringify({ hooks: { PreToolUse: [hookEntry] } }, null, 2));
252
+ return;
253
+ }
254
+
255
+ let settings: Record<string, any> = {};
256
+ try { settings = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch {}
257
+ settings.hooks = settings.hooks ?? {};
258
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse ?? [];
259
+
260
+ const already = (settings.hooks.PreToolUse as any[]).some(
261
+ (h: any) => typeof h?.hooks?.[0]?.command === 'string' && h.hooks[0].command.includes('.omnitype')
262
+ );
263
+ if (already) {
264
+ UI.info('OmniType hook is already installed in ~/.claude/settings.json');
265
+ return;
266
+ }
267
+
268
+ settings.hooks.PreToolUse.push(hookEntry);
269
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
270
+ fs.writeFileSync(cfgPath, JSON.stringify(settings, null, 2));
271
+ UI.success('OmniType hook installed in ~/.claude/settings.json');
272
+ UI.info('Claude Code will now report its model on every file edit.');
273
+ });
274
+
275
+ // ── omnitype setup-vscode-hook ──────────────────────────────────────────────
276
+ /**
277
+ * VS Code and every fork (Cursor, Windsurf, Antigravity, PearAI, Void, Zed…)
278
+ * share the same User settings.json path. They also all support a
279
+ * "runOnSave"-style task via the built-in task runner AND they all support
280
+ * the `terminal.integrated.env.*` setting to inject env vars.
281
+ *
282
+ * The most reliable universal hook is writing a VS Code keybinding-free
283
+ * globalTask using the `runOn: default` flag — but that only fires on folder
284
+ * open. Instead we write a compact snippet into the User settings.json that
285
+ * registers a shell command via the `emeraldwalk.runonsave`-compatible
286
+ * `omnitype.signal` approach using VS Code's native `tasks` global config.
287
+ *
288
+ * Simpler and actually universal: write a one-liner into the fork's
289
+ * User/settings.json under `terminal.integrated.env` is NOT right either.
290
+ *
291
+ * The REAL universal answer: write the OmniType signal command into the
292
+ * fork's global keybindings OR — even simpler — write a `.vscode/tasks.json`
293
+ * that the user can invoke. But the most frictionless path is to write
294
+ * into the fork's User/settings.json the `files.saveParticipants` equivalent.
295
+ *
296
+ * After research: the only truly hook-based, no-extension-required mechanism
297
+ * that works across ALL VS Code forks is writing a global User task with
298
+ * `"runOn": "folderOpen"` plus a file watcher script. But that fires once.
299
+ *
300
+ * The correct answer: write a sentinel-refresh shell script that the fork
301
+ * launches on startup via `terminal.integrated.shellIntegration.enabled` +
302
+ * a `.profile`/`shellrc` entry — but that's invasive.
303
+ *
304
+ * REAL correct answer: the OmniType VS Code extension IS the universal hook.
305
+ * This command installs it into every detected VS Code fork via their CLI.
306
+ * For forks without a CLI, it prints the VSIX install path.
307
+ */
308
+ program
309
+ .command('setup-vscode-hook')
310
+ .description('Install OmniType into every detected VS Code fork (Cursor, Windsurf, Antigravity, etc.)')
311
+ .option('--fork <name>', 'Target a specific fork by name (e.g. cursor, windsurf, antigravity)')
312
+ .option('--print', 'Print install commands without running them')
313
+ .action(async (opts) => {
314
+ // Known VS Code fork CLI binaries → display name + settings path segment
315
+ const FORKS: Array<{ cli: string; name: string; dir: string }> = [
316
+ { cli: 'code', name: 'VS Code', dir: 'Code' },
317
+ { cli: 'cursor', name: 'Cursor', dir: 'Cursor' },
318
+ { cli: 'windsurf', name: 'Windsurf', dir: 'Windsurf' },
319
+ { cli: 'antigravity', name: 'Antigravity', dir: 'Antigravity' },
320
+ { cli: 'pearai', name: 'PearAI', dir: 'PearAI' },
321
+ { cli: 'void', name: 'Void', dir: 'Void' },
322
+ { cli: 'trae', name: 'Trae', dir: 'Trae' },
323
+ ];
324
+
325
+ const { execSync } = require('child_process');
326
+
327
+ function hasCli(binary: string): boolean {
328
+ try { execSync(`which ${binary} 2>/dev/null || where ${binary} 2>nul`, { stdio: 'pipe' }); return true; }
329
+ catch { return false; }
330
+ }
331
+
332
+ function settingsPath(dir: string): string {
333
+ switch (process.platform) {
334
+ case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support', dir, 'User', 'settings.json');
335
+ case 'win32': return path.join(process.env.APPDATA ?? '', dir, 'User', 'settings.json');
336
+ default: return path.join(os.homedir(), '.config', dir, 'User', 'settings.json');
337
+ }
338
+ }
339
+
340
+ // The sentinel snippet written into settings.json.
341
+ // Uses VS Code's `terminal.integrated.env` to inject OMNITYPE_TOOL so the
342
+ // fork's own terminal sessions know which tool to signal. More importantly,
343
+ // writes a task definition the user can run — but the real value is that
344
+ // OmniType's VS Code extension auto-detects the fork and handles attribution.
345
+ // The settings entry below also sets `omnitype.enabled: true` which the
346
+ // OmniType extension reads to confirm the user has explicitly opted in.
347
+ const SETTINGS_SNIPPET: Record<string, unknown> = {
348
+ 'omnitype.enabled': true,
349
+ 'omnitype.autoSignal': true,
350
+ };
351
+
352
+ const targets = opts.fork
353
+ ? FORKS.filter(f => f.cli === opts.fork || f.name.toLowerCase() === opts.fork.toLowerCase())
354
+ : FORKS;
355
+
356
+ const detected: typeof FORKS = [];
357
+ const missing: typeof FORKS = [];
358
+
359
+ for (const fork of targets) {
360
+ if (hasCli(fork.cli)) detected.push(fork);
361
+ else {
362
+ // Also check if the settings file exists even without a CLI
363
+ const sp = settingsPath(fork.dir);
364
+ if (fs.existsSync(sp)) detected.push(fork);
365
+ else missing.push(fork);
366
+ }
367
+ }
368
+
369
+ if (detected.length === 0) {
370
+ UI.info('No supported VS Code forks detected. Install one of: ' +
371
+ FORKS.map(f => f.cli).join(', '));
372
+ return;
373
+ }
374
+
375
+ let anyInstalled = false;
376
+ for (const fork of detected) {
377
+ const sp = settingsPath(fork.dir);
378
+
379
+ // Merge settings snippet
380
+ let settings: Record<string, any> = {};
381
+ try { settings = JSON.parse(fs.readFileSync(sp, 'utf8')); } catch {}
382
+
383
+ const alreadyEnabled = settings['omnitype.enabled'] === true;
384
+ if (alreadyEnabled) {
385
+ UI.info(`${fork.name}: OmniType already enabled.`);
386
+ continue;
387
+ }
388
+
389
+ if (opts.print) {
390
+ console.log(`\n# ${fork.name} (${sp})`);
391
+ console.log(JSON.stringify(SETTINGS_SNIPPET, null, 2));
392
+ continue;
393
+ }
394
+
395
+ Object.assign(settings, SETTINGS_SNIPPET);
396
+ fs.mkdirSync(path.dirname(sp), { recursive: true });
397
+ fs.writeFileSync(sp, JSON.stringify(settings, null, 2));
398
+ UI.success(`${fork.name}: OmniType enabled in settings.json`);
399
+
400
+ // Also try CLI extension install if available
401
+ if (hasCli(fork.cli)) {
402
+ try {
403
+ execSync(`${fork.cli} --install-extension omnitype.omnitype-vscode --force 2>/dev/null`, { stdio: 'pipe' });
404
+ UI.success(`${fork.name}: Extension installed via ${fork.cli} CLI`);
405
+ } catch {
406
+ UI.dim(`${fork.name}: Install manually: ${fork.cli} --install-extension omnitype.omnitype-vscode`);
407
+ }
408
+ }
409
+ anyInstalled = true;
410
+ }
411
+
412
+ if (anyInstalled) {
413
+ console.log('');
414
+ UI.success('OmniType is now active in your VS Code fork(s).');
415
+ UI.info('Every AI edit will be attributed automatically — no restart needed.');
416
+ }
417
+
418
+ if (missing.length > 0 && !opts.fork) {
419
+ UI.dim(`Not detected: ${missing.map(f => f.name).join(', ')}`);
420
+ }
421
+ });
422
+
423
+ // ── omnitype signal ──────────────────────────────────────────────────────────
424
+ program
425
+ .command('signal')
426
+ .description('Report the active AI model to the OmniType sentinel')
427
+ .requiredOption('-m, --model <name>', 'Model name (e.g., gpt-4o, claude-3-5-sonnet)')
428
+ .option('-t, --tool <name>', 'Tool name (e.g., aider, continue, my-script)', 'cli-signal')
429
+ .option('--ts <ms>', 'Override timestamp (Unix ms)')
430
+ .action((opts) => {
431
+ const dir = path.join(os.homedir(), '.omnitype');
432
+ fs.mkdirSync(dir, { recursive: true });
433
+ const payload = {
434
+ model: opts.model,
435
+ tool: opts.tool,
436
+ ts: opts.ts ? parseInt(opts.ts) : Date.now(),
437
+ };
438
+ fs.writeFileSync(path.join(dir, 'active-model.json'), JSON.stringify(payload));
439
+ UI.success(`Signalled ${UI.bold(opts.model)} via ${opts.tool}`);
440
+ });
441
+
442
+ // ── omnitype whoami ──────────────────────────────────────────────────────────
443
+ program
444
+ .command('whoami')
445
+ .description('Show logged-in user info')
446
+ .action(async () => {
447
+ const api = new ApiClient();
448
+ if (!api.isSignedIn) {
449
+ UI.error('Not signed in. Run: omnitype login');
450
+ process.exit(1);
451
+ }
452
+ const spinner = UI.spinner('Fetching profile…');
453
+ try {
454
+ const profile = await api.getProfile();
455
+ spinner.stop();
456
+ let content = '';
457
+ content += ` ${chalk.bold('Username:')} ${chalk.hex(COLORS.primary)(profile.username ?? api.username)}\n`;
458
+ if (profile.email) content += ` ${chalk.bold('Email:')} ${chalk.white(profile.email)}\n`;
459
+ if (profile.full_name) content += ` ${chalk.bold('Name:')} ${chalk.white(profile.full_name)}\n`;
460
+ if (profile.created_at) content += ` ${chalk.bold('Member:')} ${UI.dim(new Date(profile.created_at).toLocaleDateString())}\n`;
461
+ console.log(UI.box(content, `${UI.logo()} — whoami`));
462
+ } catch (e: any) {
463
+ spinner.stop();
464
+ UI.error(e.message);
465
+ process.exit(1);
466
+ }
467
+ });
468
+
469
+ // ── omnitype stats ───────────────────────────────────────────────────────────
470
+ program
471
+ .command('stats')
472
+ .description('Show your personal provenance stats across all projects')
473
+ .option('-n, --top <n>', 'Show top N projects', '10')
474
+ .action(async (opts) => {
475
+ const api = new ApiClient();
476
+ if (!api.isSignedIn) {
477
+ UI.error('Not signed in. Run: omnitype login');
478
+ process.exit(1);
479
+ }
480
+ const spinner = UI.spinner('Fetching stats…');
481
+ try {
482
+ const projects: any[] = await api.getPersonalStats();
483
+ spinner.stop();
484
+
485
+ if (!projects.length) {
486
+ UI.info('No provenance data yet. Run the daemon or push some commits.');
487
+ return;
488
+ }
489
+
490
+ const topN = parseInt(opts.top, 10);
491
+ const sorted = [...projects].sort((a, b) => (b.total_chars ?? 0) - (a.total_chars ?? 0)).slice(0, topN);
492
+
493
+ // Totals across all projects
494
+ const totals = projects.reduce((acc, p) => {
495
+ acc.ai += p.ai_chars ?? 0;
496
+ acc.user += p.user_chars ?? 0;
497
+ acc.paste += p.paste_chars ?? 0;
498
+ acc.total += p.total_chars ?? 0;
499
+ return acc;
500
+ }, { ai: 0, user: 0, paste: 0, total: 0 });
501
+
502
+ let content = '';
503
+ content += ` ${chalk.bold('Total projects:')} ${chalk.white(projects.length)}\n`;
504
+ content += ` ${chalk.bold('Total chars:')} ${chalk.white(totals.total.toLocaleString())}\n`;
505
+ content += `\n`;
506
+ content += ` ${UI.label('AI', COLORS.ai)} ${UI.bar(totals.ai, totals.total, 16, COLORS.ai)} ${UI.pct(totals.ai, totals.total)}\n`;
507
+ content += ` ${UI.label('You', COLORS.user)} ${UI.bar(totals.user, totals.total, 16, COLORS.user)} ${UI.pct(totals.user, totals.total)}\n`;
508
+ content += ` ${UI.label('Paste', COLORS.paste)} ${UI.bar(totals.paste, totals.total, 16, COLORS.paste)} ${UI.pct(totals.paste, totals.total)}\n`;
509
+ content += `\n ${chalk.bold(`Top ${topN} projects:`)}\n`;
510
+
511
+ for (const p of sorted) {
512
+ const name = chalk.hex(COLORS.primary)(p.project_name.padEnd(24));
513
+ const aiPct = UI.pct(p.ai_chars ?? 0, p.total_chars ?? 1);
514
+ const bar = UI.bar(p.ai_chars ?? 0, p.total_chars ?? 1, 12, COLORS.ai);
515
+ const files = UI.dim(`${p.file_count ?? 0}f`);
516
+ content += ` ${name} ${bar} ${aiPct} ${files}\n`;
517
+
518
+ // Model breakdown if available
519
+ if (p.model_breakdown && Object.keys(p.model_breakdown).length) {
520
+ const top = Object.entries(p.model_breakdown as Record<string, number>)
521
+ .sort(([, a], [, b]) => b - a)
522
+ .slice(0, 2)
523
+ .map(([m, c]) => `${chalk.hex(COLORS.secondary)(m)} ${UI.dim(c.toLocaleString())}`)
524
+ .join(' ');
525
+ content += ` ${' '.repeat(26)}${top}\n`;
526
+ }
527
+ }
528
+
529
+ console.log(UI.box(content, `${UI.logo()} Stats`));
530
+ } catch (e: any) {
531
+ spinner.stop();
532
+ UI.error(e.message);
533
+ process.exit(1);
534
+ }
535
+ });
536
+
537
+ program.parseAsync(process.argv);
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }