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