@omnitype-code/cli 0.1.2 → 0.1.4
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/.omnitype/active-model.json +1 -0
- package/CHANGELOG.md +18 -0
- package/dist/__tests__/HookIntegration.test.js +255 -0
- package/dist/__tests__/ModelDetector.test.js +240 -0
- package/dist/__tests__/ModelDetectorCoverage.test.js +167 -0
- package/dist/__tests__/ToolDetector.test.js +251 -0
- package/dist/__tests__/ToolHookInstallers.test.js +272 -0
- package/dist/__tests__/ToolHookInstallersCoverage.test.js +262 -0
- package/dist/__tests__/TranscriptScanner.test.js +201 -0
- package/dist/core/ApiClient.js +15 -0
- package/dist/core/ModelDetector.js +43 -9
- package/dist/core/ToolDetector.js +249 -0
- package/dist/core/ToolHookInstallers.js +557 -55
- package/dist/core/TranscriptScanner.js +153 -6
- package/dist/daemon.js +15 -1
- package/dist/index.js +124 -27
- package/package.json +31 -2
- package/scripts/postinstall.js +94 -0
- package/src/__tests__/HookIntegration.test.ts +261 -0
- package/src/__tests__/ModelDetector.test.ts +252 -0
- package/src/__tests__/ModelDetectorCoverage.test.ts +154 -0
- package/src/__tests__/ToolDetector.test.ts +238 -0
- package/src/__tests__/ToolHookInstallers.test.ts +281 -0
- package/src/__tests__/ToolHookInstallersCoverage.test.ts +237 -0
- package/src/__tests__/TranscriptScanner.test.ts +201 -0
- package/src/core/ApiClient.ts +15 -0
- package/src/core/ModelDetector.ts +43 -9
- package/src/core/ToolDetector.ts +227 -0
- package/src/core/ToolHookInstallers.ts +492 -61
- package/src/core/TranscriptScanner.ts +125 -9
- package/src/daemon.ts +13 -2
- package/src/index.ts +134 -31
|
@@ -131,12 +131,119 @@ function extractAmp(filePath) {
|
|
|
131
131
|
catch { }
|
|
132
132
|
return undefined;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Claude Code encodes a project path as the absolute path with '/' replaced by '-'.
|
|
136
|
+
* e.g. /Users/foo/myproject → -Users-foo-myproject
|
|
137
|
+
*/
|
|
138
|
+
function claudeProjectDir(cwd, base) {
|
|
139
|
+
return path.join(base, 'projects', cwd.replace(/\//g, '-'));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Returns the VS Code workspaceStorage root, then filters subdirs to those
|
|
143
|
+
* whose workspace.json `folder` URI matches the given cwd.
|
|
144
|
+
* Falls back to returning all copilot transcript dirs if cwd is unknown.
|
|
145
|
+
*/
|
|
146
|
+
function copilotDirs(cwd) {
|
|
147
|
+
const roots = [];
|
|
148
|
+
switch (process.platform) {
|
|
149
|
+
case 'darwin':
|
|
150
|
+
roots.push(path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'workspaceStorage'));
|
|
151
|
+
break;
|
|
152
|
+
case 'win32':
|
|
153
|
+
if (process.env.APPDATA)
|
|
154
|
+
roots.push(path.join(process.env.APPDATA, 'Code', 'User', 'workspaceStorage'));
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
roots.push(path.join(HOME, '.config', 'Code', 'User', 'workspaceStorage'));
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
// Also check Insiders
|
|
161
|
+
if (process.platform === 'darwin')
|
|
162
|
+
roots.push(path.join(HOME, 'Library', 'Application Support', 'Code - Insiders', 'User', 'workspaceStorage'));
|
|
163
|
+
const result = [];
|
|
164
|
+
for (const root of roots) {
|
|
165
|
+
if (!fs.existsSync(root))
|
|
166
|
+
continue;
|
|
167
|
+
let entries;
|
|
168
|
+
try {
|
|
169
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (const e of entries) {
|
|
175
|
+
if (!e.isDirectory())
|
|
176
|
+
continue;
|
|
177
|
+
const transcriptsDir = path.join(root, e.name, 'GitHub.copilot-chat', 'transcripts');
|
|
178
|
+
if (!fs.existsSync(transcriptsDir))
|
|
179
|
+
continue;
|
|
180
|
+
if (cwd) {
|
|
181
|
+
// Read workspace.json to check if this workspace matches the cwd
|
|
182
|
+
try {
|
|
183
|
+
const wj = JSON.parse(fs.readFileSync(path.join(root, e.name, 'workspace.json'), 'utf8'));
|
|
184
|
+
const folder = (wj?.folder ?? '').replace(/^file:\/\//, '').replace(/%20/g, ' ');
|
|
185
|
+
if (folder !== cwd && !folder.startsWith(cwd + '/'))
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
result.push(transcriptsDir);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
/** Extract model from a Copilot JSONL event stream line. */
|
|
198
|
+
function extractCopilotJsonl(filePath) {
|
|
199
|
+
let fd;
|
|
200
|
+
try {
|
|
201
|
+
fd = fs.openSync(filePath, 'r');
|
|
202
|
+
const size = fs.fstatSync(fd).size;
|
|
203
|
+
if (!size) {
|
|
204
|
+
fs.closeSync(fd);
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const readSize = Math.min(TAIL_BYTES, size);
|
|
208
|
+
const buf = Buffer.alloc(readSize);
|
|
209
|
+
fs.readSync(fd, buf, 0, readSize, size - readSize);
|
|
210
|
+
fs.closeSync(fd);
|
|
211
|
+
fd = undefined;
|
|
212
|
+
const lines = buf.toString('utf8').split('\n');
|
|
213
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
214
|
+
const t = lines[i].trim();
|
|
215
|
+
if (!t)
|
|
216
|
+
continue;
|
|
217
|
+
try {
|
|
218
|
+
const j = JSON.parse(t);
|
|
219
|
+
// assistant.message carries modelId in data
|
|
220
|
+
const m = j?.data?.modelId ?? j?.data?.model ?? j?.model ?? j?.message?.model;
|
|
221
|
+
if (typeof m === 'string' && m)
|
|
222
|
+
return m;
|
|
223
|
+
}
|
|
224
|
+
catch { }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
finally {
|
|
229
|
+
if (fd !== undefined)
|
|
230
|
+
try {
|
|
231
|
+
fs.closeSync(fd);
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
134
237
|
const SPECS = [
|
|
135
238
|
{
|
|
136
239
|
tool: 'claude-code',
|
|
137
|
-
dirs: () => {
|
|
240
|
+
dirs: (cwd) => {
|
|
138
241
|
const base = process.env.CLAUDE_CONFIG_DIR ?? path.join(HOME, '.claude');
|
|
139
242
|
const alt = path.join(HOME, '.config', 'claude');
|
|
243
|
+
if (cwd) {
|
|
244
|
+
// Scope to the current workspace so other projects don't bleed in.
|
|
245
|
+
return [claudeProjectDir(cwd, base), claudeProjectDir(cwd, alt)];
|
|
246
|
+
}
|
|
140
247
|
return [path.join(base, 'projects'), path.join(alt, 'projects')];
|
|
141
248
|
},
|
|
142
249
|
ext: '.jsonl',
|
|
@@ -166,12 +273,48 @@ const SPECS = [
|
|
|
166
273
|
ext: '.json',
|
|
167
274
|
extract: extractAmp,
|
|
168
275
|
},
|
|
276
|
+
{
|
|
277
|
+
tool: 'copilot',
|
|
278
|
+
dirs: (cwd) => copilotDirs(cwd),
|
|
279
|
+
ext: '.jsonl',
|
|
280
|
+
extract: extractCopilotJsonl,
|
|
281
|
+
},
|
|
169
282
|
{
|
|
170
283
|
tool: 'windsurf',
|
|
171
284
|
dirs: () => [path.join(HOME, '.codeium', 'windsurf', 'conversations')],
|
|
172
285
|
ext: '.jsonl',
|
|
173
286
|
extract: extractJsonl,
|
|
174
287
|
},
|
|
288
|
+
{
|
|
289
|
+
tool: 'continue',
|
|
290
|
+
dirs: () => [path.join(HOME, '.continue', 'sessions')],
|
|
291
|
+
ext: '.json',
|
|
292
|
+
extract: (filePath) => {
|
|
293
|
+
try {
|
|
294
|
+
const msgs = JSON.parse(fs.readFileSync(filePath, 'utf8'))?.history ?? [];
|
|
295
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
296
|
+
const m = msgs[i]?.message?.model ?? msgs[i]?.model;
|
|
297
|
+
if (typeof m === 'string' && m)
|
|
298
|
+
return m;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch { }
|
|
302
|
+
return undefined;
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
// Copilot CLI: ~/.copilot/session-state/*/events.jsonl
|
|
307
|
+
tool: 'copilot-cli',
|
|
308
|
+
dirs: () => [path.join(HOME, '.copilot', 'session-state')],
|
|
309
|
+
ext: '.jsonl',
|
|
310
|
+
extract: extractJsonl,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
tool: 'droid',
|
|
314
|
+
dirs: () => [path.join(HOME, '.factory', 'sessions')],
|
|
315
|
+
ext: '.jsonl',
|
|
316
|
+
extract: extractJsonl,
|
|
317
|
+
},
|
|
175
318
|
];
|
|
176
319
|
// ── Directory scanner: finds the newest file with the given extension ─────────
|
|
177
320
|
function newestFile(dirs, ext) {
|
|
@@ -207,16 +350,19 @@ function newestFile(dirs, ext) {
|
|
|
207
350
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
208
351
|
let _cache;
|
|
209
352
|
let _cacheAt = 0;
|
|
353
|
+
let _cacheCwd;
|
|
210
354
|
/**
|
|
211
|
-
* Returns the model from the most recently modified AI session transcript
|
|
212
|
-
*
|
|
355
|
+
* Returns the model from the most recently modified AI session transcript.
|
|
356
|
+
* When `cwd` is provided, claude-code scanning is scoped to that workspace
|
|
357
|
+
* so activity from other projects doesn't bleed into the result.
|
|
358
|
+
* Result is cached for 60 s (cache is busted when cwd changes).
|
|
213
359
|
*/
|
|
214
|
-
function scanTranscripts() {
|
|
215
|
-
if (_cache && Date.now() - _cacheAt < CACHE_TTL)
|
|
360
|
+
function scanTranscripts(cwd) {
|
|
361
|
+
if (_cache && _cacheCwd === cwd && Date.now() - _cacheAt < CACHE_TTL)
|
|
216
362
|
return _cache;
|
|
217
363
|
let best;
|
|
218
364
|
for (const spec of SPECS) {
|
|
219
|
-
const file = newestFile(spec.dirs(), spec.ext);
|
|
365
|
+
const file = newestFile(spec.dirs(cwd), spec.ext);
|
|
220
366
|
if (!file)
|
|
221
367
|
continue;
|
|
222
368
|
if (best && file.mtime <= best.mtime)
|
|
@@ -228,6 +374,7 @@ function scanTranscripts() {
|
|
|
228
374
|
}
|
|
229
375
|
_cache = best;
|
|
230
376
|
_cacheAt = Date.now();
|
|
377
|
+
_cacheCwd = cwd;
|
|
231
378
|
return best;
|
|
232
379
|
}
|
|
233
380
|
function invalidateTranscriptCache() { _cache = undefined; _cacheAt = 0; }
|
package/dist/daemon.js
CHANGED
|
@@ -91,9 +91,23 @@ function startDaemon(opts) {
|
|
|
91
91
|
UI_1.UI.error('Not signed in. Run: omnitype login');
|
|
92
92
|
process.exit(1);
|
|
93
93
|
}
|
|
94
|
-
// Auto-install preToolUse hooks into
|
|
94
|
+
// Auto-install preToolUse hooks into all supported tools.
|
|
95
95
|
(0, ToolHookInstallers_1.installAllToolHooks)();
|
|
96
96
|
const watchPath = path.resolve(opts.watchPath);
|
|
97
|
+
// Initialize per-project .omnitype/ dir and ensure it is in .gitignore.
|
|
98
|
+
const omniDir = path.join(watchPath, '.omnitype');
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(omniDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
try {
|
|
104
|
+
const gitignore = path.join(watchPath, '.gitignore');
|
|
105
|
+
const current = fs.existsSync(gitignore) ? fs.readFileSync(gitignore, 'utf8') : '';
|
|
106
|
+
if (!current.split('\n').some(l => l.trim() === '.omnitype' || l.trim() === '.omnitype/')) {
|
|
107
|
+
fs.appendFileSync(gitignore, (current.length && !current.endsWith('\n') ? '\n' : '') + '.omnitype/\n');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
97
111
|
const projectName = opts.projectName;
|
|
98
112
|
const branch = opts.branch ?? gitBranch(watchPath);
|
|
99
113
|
console.log(UI_1.UI.box(`${chalk_1.default.bold('Project:')} ${chalk_1.default.cyan(projectName)}\n` +
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,8 @@ const daemon_1 = require("./daemon");
|
|
|
50
50
|
const blame_1 = require("./blame");
|
|
51
51
|
const GitNotes_1 = require("./core/GitNotes");
|
|
52
52
|
const UI_1 = require("./core/UI");
|
|
53
|
+
const ToolHookInstallers_1 = require("./core/ToolHookInstallers");
|
|
54
|
+
const ToolDetector_1 = require("./core/ToolDetector");
|
|
53
55
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
54
56
|
const program = new commander_1.Command();
|
|
55
57
|
program
|
|
@@ -109,40 +111,135 @@ program
|
|
|
109
111
|
.description('Show current auth and model detection status')
|
|
110
112
|
.action(() => {
|
|
111
113
|
const api = new ApiClient_1.ApiClient();
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
const cwd = process.cwd();
|
|
115
|
+
const detection = new ModelDetector_1.ModelDetector().detect(undefined, cwd);
|
|
116
|
+
const lines = [];
|
|
117
|
+
const col1 = 10;
|
|
118
|
+
const lbl = (k) => chalk_1.default.bold(chalk_1.default.hex(UI_1.COLORS.primary)(k.padEnd(col1)));
|
|
119
|
+
// Account
|
|
115
120
|
if (api.isSignedIn) {
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
lines.push(`${lbl('Account')} ${chalk_1.default.cyan(api.username)}`);
|
|
122
|
+
lines.push(`${lbl('Server')} ${UI_1.UI.dim(api.apiUrl)}`);
|
|
118
123
|
}
|
|
119
124
|
else {
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
lines.push(`${lbl('Account')} ${chalk_1.default.red('not signed in')} ${UI_1.UI.dim('→ omnitype login')}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push('');
|
|
128
|
+
// AI context
|
|
129
|
+
const toolColor = (t) => t.includes('claude') ? '#D97757' : t.includes('gpt') || t.includes('openai') ? '#10A37F'
|
|
130
|
+
: t.includes('gemini') ? '#4285F4' : t.includes('copilot') ? '#6E40C9' : UI_1.COLORS.ai;
|
|
131
|
+
const modelStr = detection.model === 'unknown'
|
|
132
|
+
? chalk_1.default.gray('none detected')
|
|
133
|
+
: chalk_1.default.bold(chalk_1.default.hex(toolColor(detection.model))(detection.model));
|
|
134
|
+
const toolStr = detection.tool === 'unknown'
|
|
135
|
+
? chalk_1.default.gray('—')
|
|
136
|
+
: chalk_1.default.hex(toolColor(detection.tool))(detection.tool);
|
|
137
|
+
const confBadge = {
|
|
138
|
+
deterministic: chalk_1.default.bgGreen.black(' HOOK '),
|
|
139
|
+
high: chalk_1.default.bgCyan.black(' HIGH '),
|
|
140
|
+
medium: chalk_1.default.bgYellow.black(' MED '),
|
|
141
|
+
low: chalk_1.default.bgRed.black(' LOW '),
|
|
133
142
|
};
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
lines.push(`${lbl('Model')} ${modelStr}`);
|
|
144
|
+
lines.push(`${lbl('Tool')} ${toolStr}`);
|
|
145
|
+
lines.push(`${lbl('Confidence')} ${confBadge[detection.confidence] ?? detection.confidence}`);
|
|
146
|
+
// Repo
|
|
136
147
|
try {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
const cp = require('child_process');
|
|
149
|
+
const branch = cp.execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
150
|
+
const remote = (() => { try {
|
|
151
|
+
return cp.execSync('git remote get-url origin', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().replace(/^https?:\/\/[^/]+\//, '').replace(/\.git$/, '');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return '';
|
|
155
|
+
} })();
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push(`${lbl('Project')} ${chalk_1.default.white(path.basename(cwd))}`);
|
|
158
|
+
lines.push(`${lbl('Branch')} ${chalk_1.default.magenta(branch)}`);
|
|
159
|
+
if (remote)
|
|
160
|
+
lines.push(`${lbl('Remote')} ${UI_1.UI.dim(remote)}`);
|
|
143
161
|
}
|
|
144
162
|
catch { }
|
|
145
|
-
console.log(UI_1.UI.box(
|
|
163
|
+
console.log(UI_1.UI.box(lines.join('\n'), `${UI_1.UI.logo()} Status`));
|
|
164
|
+
});
|
|
165
|
+
// ── omnitype doctor ─────────────────────────────────────────────────────────
|
|
166
|
+
program
|
|
167
|
+
.command('doctor')
|
|
168
|
+
.description('Check hook status for all detected AI tools')
|
|
169
|
+
.option('--fix', 'Auto-install or upgrade any missing/stale hooks')
|
|
170
|
+
.action((opts) => {
|
|
171
|
+
const api = new ApiClient_1.ApiClient();
|
|
172
|
+
const installed = (0, ToolDetector_1.detectInstalledTools)();
|
|
173
|
+
const hookStatuses = (0, ToolHookInstallers_1.checkHookStatus)();
|
|
174
|
+
const hookMap = new Map(hookStatuses.map(s => [s.tool, s.status]));
|
|
175
|
+
const col = 20;
|
|
176
|
+
const lbl = (k) => chalk_1.default.bold(k.padEnd(col));
|
|
177
|
+
const hookBadge = {
|
|
178
|
+
'installed': chalk_1.default.bgGreen.black(' HOOKED '),
|
|
179
|
+
'stale': chalk_1.default.bgYellow.black(' STALE '),
|
|
180
|
+
'not-installed': chalk_1.default.bgRed.black(' NOT HOOKED'),
|
|
181
|
+
'tool-absent': '',
|
|
182
|
+
};
|
|
183
|
+
const lines = [];
|
|
184
|
+
let needsFix = false;
|
|
185
|
+
const hookedTools = installed.filter(t => t.hookSupport === 'hooked');
|
|
186
|
+
const configOnlyTools = installed.filter(t => t.hookSupport === 'config-only');
|
|
187
|
+
const unhooked = installed.filter(t => t.hookSupport === 'no-hook');
|
|
188
|
+
// ── Tools with sentinel hook support ──
|
|
189
|
+
if (hookedTools.length > 0) {
|
|
190
|
+
lines.push(chalk_1.default.bold(chalk_1.default.hex(UI_1.COLORS.primary)('Sentinel hooks:')));
|
|
191
|
+
for (const tool of hookedTools) {
|
|
192
|
+
const status = hookMap.get(tool.id) ?? 'not-installed';
|
|
193
|
+
lines.push(` ${lbl(tool.name)} ${hookBadge[status]}`);
|
|
194
|
+
if (status === 'stale' || status === 'not-installed')
|
|
195
|
+
needsFix = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
lines.push(chalk_1.default.gray('No hookable AI tools detected on this machine.'));
|
|
200
|
+
}
|
|
201
|
+
// ── Config-only tools (model readable, no hook API) ──
|
|
202
|
+
if (configOnlyTools.length > 0) {
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push(chalk_1.default.bold(chalk_1.default.hex(UI_1.COLORS.secondary)('Config-readable (no hook API):')));
|
|
205
|
+
for (const tool of configOnlyTools) {
|
|
206
|
+
lines.push(` ${lbl(tool.name)} ${chalk_1.default.hex(UI_1.COLORS.secondary)(' DETECTED ')} ${UI_1.UI.dim('model readable from config')}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ── Detected tools we don't support yet ──
|
|
210
|
+
if (unhooked.length > 0) {
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push(chalk_1.default.bold(chalk_1.default.hex(UI_1.COLORS.warning)('Detected (not yet supported):')));
|
|
213
|
+
for (const tool of unhooked) {
|
|
214
|
+
lines.push(` ${lbl(tool.name)} ${chalk_1.default.gray('— support coming soon')}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ── Nothing detected at all ──
|
|
218
|
+
if (installed.length === 0) {
|
|
219
|
+
lines.push(chalk_1.default.gray('No AI coding tools detected on this machine.'));
|
|
220
|
+
lines.push(UI_1.UI.dim('Install Claude Code, Cursor, Windsurf, or another tool and re-run.'));
|
|
221
|
+
}
|
|
222
|
+
// ── Fix prompt ──
|
|
223
|
+
if (needsFix) {
|
|
224
|
+
lines.push('');
|
|
225
|
+
if (opts.fix) {
|
|
226
|
+
(0, ToolHookInstallers_1.installAllToolHooks)();
|
|
227
|
+
lines.push(chalk_1.default.hex(UI_1.COLORS.success)('✔ Hooks installed/upgraded. Run doctor again to verify.'));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
lines.push(chalk_1.default.yellow('→ Run with --fix to install or upgrade missing/stale hooks.'));
|
|
231
|
+
lines.push(UI_1.UI.dim(' Hooks give deterministic (100% accurate) model attribution.'));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else if (hookedTools.length > 0) {
|
|
235
|
+
lines.push('');
|
|
236
|
+
lines.push(chalk_1.default.hex(UI_1.COLORS.success)('✔ All hooks up to date.'));
|
|
237
|
+
}
|
|
238
|
+
console.log(UI_1.UI.box(lines.join('\n'), `${UI_1.UI.logo()} Doctor`));
|
|
239
|
+
// Market analysis telemetry — fire and forget
|
|
240
|
+
if (installed.length > 0) {
|
|
241
|
+
api.reportToolEnvironment(installed.map(t => ({ id: t.id, name: t.name, hookSupport: t.hookSupport })));
|
|
242
|
+
}
|
|
146
243
|
});
|
|
147
244
|
// ── omnitype daemon ─────────────────────────────────────────────────────────
|
|
148
245
|
program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnitype-code/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "OmniType CLI — editor-agnostic code provenance tracking",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,33 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"dev": "ts-node src/index.ts",
|
|
13
|
-
"prepublishOnly": "npm run build"
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"postinstall": "node scripts/postinstall.js",
|
|
15
|
+
"test": "jest"
|
|
16
|
+
},
|
|
17
|
+
"jest": {
|
|
18
|
+
"preset": "ts-jest",
|
|
19
|
+
"testEnvironment": "node",
|
|
20
|
+
"roots": [
|
|
21
|
+
"<rootDir>/src/__tests__"
|
|
22
|
+
],
|
|
23
|
+
"testMatch": [
|
|
24
|
+
"**/*.test.ts"
|
|
25
|
+
],
|
|
26
|
+
"transform": {
|
|
27
|
+
"^.+\\.tsx?$": [
|
|
28
|
+
"ts-jest",
|
|
29
|
+
{
|
|
30
|
+
"tsconfig": {
|
|
31
|
+
"strict": false,
|
|
32
|
+
"skipLibCheck": true
|
|
33
|
+
},
|
|
34
|
+
"diagnostics": {
|
|
35
|
+
"ignoreCodes": ["TS2554", "TS2345", "TS2322"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
14
40
|
},
|
|
15
41
|
"dependencies": {
|
|
16
42
|
"boxen": "^5.1.2",
|
|
@@ -25,7 +51,10 @@
|
|
|
25
51
|
"devDependencies": {
|
|
26
52
|
"@types/gradient-string": "^1.1.6",
|
|
27
53
|
"@types/inquirer": "^8.2.12",
|
|
54
|
+
"@types/jest": "^29.5.14",
|
|
28
55
|
"@types/node": "^20.0.0",
|
|
56
|
+
"jest": "^29.7.0",
|
|
57
|
+
"ts-jest": "^29.4.9",
|
|
29
58
|
"typescript": "^5.3.0"
|
|
30
59
|
}
|
|
31
60
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Runs after `npm install -g @omnitype-code/cli`.
|
|
5
|
+
// Silently installs sentinel hooks into any AI tools already on the machine,
|
|
6
|
+
// then prints a short welcome guide so the user knows what just happened.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
// ── Silent hook install + telemetry ───────────────────────────────────────
|
|
13
|
+
// We call the compiled output directly so this script works without ts-node.
|
|
14
|
+
let detectedTools = [];
|
|
15
|
+
try {
|
|
16
|
+
const { installAllToolHooks } = require('../dist/core/ToolHookInstallers');
|
|
17
|
+
installAllToolHooks();
|
|
18
|
+
} catch { /* dist not built yet (e.g. dev install) — skip */ }
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { detectInstalledTools } = require('../dist/core/ToolDetector');
|
|
22
|
+
detectedTools = detectInstalledTools();
|
|
23
|
+
} catch {}
|
|
24
|
+
|
|
25
|
+
// Fire-and-forget telemetry (only if user is already signed in from a prior install)
|
|
26
|
+
try {
|
|
27
|
+
const { ApiClient } = require('../dist/core/ApiClient');
|
|
28
|
+
const api = new ApiClient();
|
|
29
|
+
if (detectedTools.length > 0) {
|
|
30
|
+
api.reportToolEnvironment(detectedTools.map(t => ({ id: t.id, name: t.name, hookSupport: t.hookSupport })));
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
// ── Welcome guide ──────────────────────────────────────────────────────────
|
|
35
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
36
|
+
const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
37
|
+
const cyan = s => `\x1b[36m${s}\x1b[0m`;
|
|
38
|
+
const green = s => `\x1b[32m${s}\x1b[0m`;
|
|
39
|
+
const gray = s => `\x1b[90m${s}\x1b[0m`;
|
|
40
|
+
|
|
41
|
+
const box = (lines, title) => {
|
|
42
|
+
const width = Math.max(...lines.map(l => stripAnsi(l).length), stripAnsi(title).length) + 4;
|
|
43
|
+
const hr = '─'.repeat(width - 2);
|
|
44
|
+
const pad = s => {
|
|
45
|
+
const visible = stripAnsi(s).length;
|
|
46
|
+
return s + ' '.repeat(width - 2 - visible);
|
|
47
|
+
};
|
|
48
|
+
return [
|
|
49
|
+
`╭${hr}╮`,
|
|
50
|
+
`│ ${bold(cyan(title))}${' '.repeat(width - 2 - stripAnsi(title).length - 1)}│`,
|
|
51
|
+
`├${hr}┤`,
|
|
52
|
+
...lines.map(l => `│ ${pad(l)}│`),
|
|
53
|
+
`╰${hr}╯`,
|
|
54
|
+
].join('\n');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function stripAnsi(str) {
|
|
58
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hookedTools = detectedTools.filter(t => t.hookSupport === 'hooked').map(t => t.name);
|
|
62
|
+
const unhookedTools = detectedTools.filter(t => t.hookSupport === 'no-hook').map(t => t.name);
|
|
63
|
+
|
|
64
|
+
const lines = [
|
|
65
|
+
bold(green('✔ OmniType installed!')),
|
|
66
|
+
'',
|
|
67
|
+
bold('What it does:'),
|
|
68
|
+
` Tracks which AI model wrote each line of code across`,
|
|
69
|
+
` any editor — Claude Code, Cursor, Windsurf, Cline, etc.`,
|
|
70
|
+
'',
|
|
71
|
+
bold('Hooks installed for:'),
|
|
72
|
+
hookedTools.length
|
|
73
|
+
? hookedTools.map(t => ` ${green('✔')} ${t}`).join('\n')
|
|
74
|
+
: ` ${gray('No hookable AI tools detected yet.')}`,
|
|
75
|
+
...(unhookedTools.length ? [
|
|
76
|
+
'',
|
|
77
|
+
bold('Also detected (hook support coming):'),
|
|
78
|
+
unhookedTools.map(t => ` ${gray('○')} ${t}`).join('\n'),
|
|
79
|
+
] : []),
|
|
80
|
+
'',
|
|
81
|
+
bold('Next steps:'),
|
|
82
|
+
` ${cyan('omnitype login')} Sign in to sync provenance to cloud`,
|
|
83
|
+
` ${cyan('omnitype doctor')} Check hook status for all tools`,
|
|
84
|
+
` ${cyan('omnitype doctor --fix')} Install/upgrade any missing hooks`,
|
|
85
|
+
` ${cyan('omnitype status')} Show active model in current workspace`,
|
|
86
|
+
'',
|
|
87
|
+
dim('Hooks give deterministic (100% accurate) model attribution.'),
|
|
88
|
+
dim('Without them attribution falls back to heuristics.'),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Flatten nested arrays from the tools join
|
|
92
|
+
const flat = lines.flatMap(l => typeof l === 'string' ? [l] : l.split('\n'));
|
|
93
|
+
|
|
94
|
+
console.log('\n' + box(flat, 'OmniType — Code Provenance') + '\n');
|