@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/blame.js +242 -0
- package/dist/core/ApiClient.js +234 -0
- package/dist/core/FileProvenance.js +483 -0
- package/dist/core/GitNotes.js +120 -0
- package/dist/core/Heartbeat.js +81 -0
- package/dist/core/ModelDetector.js +243 -0
- package/dist/core/ProvenanceResolver.js +424 -0
- package/dist/core/UI.js +97 -0
- package/dist/daemon.js +194 -0
- package/dist/hooks.js +220 -0
- package/dist/index.js +536 -0
- package/package.json +30 -0
- package/src/blame.ts +240 -0
- package/src/core/ApiClient.ts +197 -0
- package/src/core/FileProvenance.ts +538 -0
- package/src/core/GitNotes.ts +141 -0
- package/src/core/Heartbeat.ts +53 -0
- package/src/core/ModelDetector.ts +216 -0
- package/src/core/ProvenanceResolver.ts +433 -0
- package/src/core/UI.ts +105 -0
- package/src/daemon.ts +171 -0
- package/src/hooks.ts +195 -0
- package/src/index.ts +537 -0
- package/tsconfig.json +15 -0
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
|
+
}
|