@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/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
|
+
}
|