@omnitype-code/cli 0.1.1 → 0.1.3
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/CHANGELOG.md +68 -0
- package/dist/core/ApiClient.js +15 -0
- package/dist/core/ModelDetector.js +116 -111
- package/dist/core/ToolDetector.js +249 -0
- package/dist/core/ToolHookInstallers.js +703 -0
- package/dist/core/TranscriptScanner.js +380 -0
- package/dist/daemon.js +17 -0
- package/dist/index.js +124 -27
- package/package.json +3 -2
- package/scripts/postinstall.js +94 -0
- package/src/core/ApiClient.ts +15 -0
- package/src/core/ModelDetector.ts +118 -126
- package/src/core/ToolDetector.ts +227 -0
- package/src/core/ToolHookInstallers.ts +580 -0
- package/src/core/TranscriptScanner.ts +320 -0
- package/src/daemon.ts +16 -1
- package/src/index.ts +134 -31
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transcript-based model detection — TypeScript port of git-ai's model_extraction.rs.
|
|
4
|
+
*
|
|
5
|
+
* Strategy: for each AI tool, find the most recently modified session file,
|
|
6
|
+
* read its tail (50 KB), scan lines in reverse for a model field, fall back
|
|
7
|
+
* to the head for tools that emit model_change events at session start.
|
|
8
|
+
*
|
|
9
|
+
* Results are cached for 60 s so directory scanning doesn't run on every edit.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.scanTranscripts = scanTranscripts;
|
|
46
|
+
exports.invalidateTranscriptCache = invalidateTranscriptCache;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const TAIL_BYTES = 51200; // 50 KB — mirrors git-ai
|
|
51
|
+
const HEAD_LINES = 20;
|
|
52
|
+
const CACHE_TTL = 60000; // re-scan directories at most once per minute
|
|
53
|
+
const HOME = os.homedir();
|
|
54
|
+
// ── Model extraction from a single JSONL line ─────────────────────────────────
|
|
55
|
+
function modelFromLine(line) {
|
|
56
|
+
const t = line.trim();
|
|
57
|
+
if (!t)
|
|
58
|
+
return undefined;
|
|
59
|
+
let j;
|
|
60
|
+
try {
|
|
61
|
+
j = JSON.parse(t);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
// Copilot CLI / Windsurf: session.model_change event
|
|
67
|
+
if (j?.type === 'session.model_change') {
|
|
68
|
+
const m = j?.data?.newModel;
|
|
69
|
+
if (typeof m === 'string' && m)
|
|
70
|
+
return m;
|
|
71
|
+
}
|
|
72
|
+
// Standard JSONL: message.model, top-level model, or modelID
|
|
73
|
+
const candidate = j?.message?.model ?? j?.model ?? j?.modelID ?? j?.data?.model;
|
|
74
|
+
if (typeof candidate === 'string' && candidate && candidate !== '<synthetic>')
|
|
75
|
+
return candidate;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
// ── JSONL extraction: tail scan + head fallback ───────────────────────────────
|
|
79
|
+
function extractJsonl(filePath) {
|
|
80
|
+
let fd;
|
|
81
|
+
try {
|
|
82
|
+
fd = fs.openSync(filePath, 'r');
|
|
83
|
+
const size = fs.fstatSync(fd).size;
|
|
84
|
+
if (!size) {
|
|
85
|
+
fs.closeSync(fd);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const readSize = Math.min(TAIL_BYTES, size);
|
|
89
|
+
const seekPos = size - readSize;
|
|
90
|
+
const buf = Buffer.alloc(readSize);
|
|
91
|
+
fs.readSync(fd, buf, 0, readSize, seekPos);
|
|
92
|
+
fs.closeSync(fd);
|
|
93
|
+
fd = undefined;
|
|
94
|
+
// Reverse-scan the tail
|
|
95
|
+
const lines = buf.toString('utf8').split('\n');
|
|
96
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
97
|
+
const m = modelFromLine(lines[i]);
|
|
98
|
+
if (m)
|
|
99
|
+
return m;
|
|
100
|
+
}
|
|
101
|
+
// Head fallback: model_change events are emitted at session start
|
|
102
|
+
if (seekPos > 0) {
|
|
103
|
+
const head = fs.readFileSync(filePath, 'utf8').split('\n');
|
|
104
|
+
for (let i = 0; i < Math.min(HEAD_LINES, head.length); i++) {
|
|
105
|
+
const m = modelFromLine(head[i]);
|
|
106
|
+
if (m)
|
|
107
|
+
return m;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { /* unreadable */ }
|
|
112
|
+
finally {
|
|
113
|
+
if (fd !== undefined)
|
|
114
|
+
try {
|
|
115
|
+
fs.closeSync(fd);
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
// ── Amp: thread JSON — model lives in messages[].usage.model ─────────────────
|
|
122
|
+
function extractAmp(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
const messages = JSON.parse(fs.readFileSync(filePath, 'utf8'))?.messages ?? [];
|
|
125
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
126
|
+
const m = messages[i]?.usage?.model;
|
|
127
|
+
if (typeof m === 'string' && m)
|
|
128
|
+
return m;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
return undefined;
|
|
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
|
+
}
|
|
237
|
+
const SPECS = [
|
|
238
|
+
{
|
|
239
|
+
tool: 'claude-code',
|
|
240
|
+
dirs: (cwd) => {
|
|
241
|
+
const base = process.env.CLAUDE_CONFIG_DIR ?? path.join(HOME, '.claude');
|
|
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
|
+
}
|
|
247
|
+
return [path.join(base, 'projects'), path.join(alt, 'projects')];
|
|
248
|
+
},
|
|
249
|
+
ext: '.jsonl',
|
|
250
|
+
extract: extractJsonl,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
tool: 'codex',
|
|
254
|
+
dirs: () => [path.join(HOME, '.codex')],
|
|
255
|
+
ext: '.jsonl',
|
|
256
|
+
extract: extractJsonl,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
tool: 'gemini-cli',
|
|
260
|
+
dirs: () => [path.join(HOME, '.gemini', 'tmp')],
|
|
261
|
+
ext: '.jsonl',
|
|
262
|
+
extract: extractJsonl,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
tool: 'amp',
|
|
266
|
+
dirs: () => {
|
|
267
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
268
|
+
const local = process.env.LOCALAPPDATA;
|
|
269
|
+
if (process.platform === 'win32')
|
|
270
|
+
return local ? [path.join(local, 'amp', 'threads')] : [];
|
|
271
|
+
return [path.join(xdg ?? path.join(HOME, '.local', 'share'), 'amp', 'threads')];
|
|
272
|
+
},
|
|
273
|
+
ext: '.json',
|
|
274
|
+
extract: extractAmp,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
tool: 'copilot',
|
|
278
|
+
dirs: (cwd) => copilotDirs(cwd),
|
|
279
|
+
ext: '.jsonl',
|
|
280
|
+
extract: extractCopilotJsonl,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
tool: 'windsurf',
|
|
284
|
+
dirs: () => [path.join(HOME, '.codeium', 'windsurf', 'conversations')],
|
|
285
|
+
ext: '.jsonl',
|
|
286
|
+
extract: extractJsonl,
|
|
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
|
+
},
|
|
318
|
+
];
|
|
319
|
+
// ── Directory scanner: finds the newest file with the given extension ─────────
|
|
320
|
+
function newestFile(dirs, ext) {
|
|
321
|
+
let best;
|
|
322
|
+
function scan(dir, depth = 0) {
|
|
323
|
+
if (depth > 6)
|
|
324
|
+
return;
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
for (const e of entries) {
|
|
333
|
+
const full = path.join(dir, e.name);
|
|
334
|
+
if (e.isDirectory())
|
|
335
|
+
scan(full, depth + 1);
|
|
336
|
+
else if (e.isFile() && e.name.endsWith(ext)) {
|
|
337
|
+
try {
|
|
338
|
+
const mtime = fs.statSync(full).mtimeMs;
|
|
339
|
+
if (!best || mtime > best.mtime)
|
|
340
|
+
best = { path: full, mtime };
|
|
341
|
+
}
|
|
342
|
+
catch { }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const dir of dirs)
|
|
347
|
+
scan(dir);
|
|
348
|
+
return best;
|
|
349
|
+
}
|
|
350
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
351
|
+
let _cache;
|
|
352
|
+
let _cacheAt = 0;
|
|
353
|
+
let _cacheCwd;
|
|
354
|
+
/**
|
|
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).
|
|
359
|
+
*/
|
|
360
|
+
function scanTranscripts(cwd) {
|
|
361
|
+
if (_cache && _cacheCwd === cwd && Date.now() - _cacheAt < CACHE_TTL)
|
|
362
|
+
return _cache;
|
|
363
|
+
let best;
|
|
364
|
+
for (const spec of SPECS) {
|
|
365
|
+
const file = newestFile(spec.dirs(cwd), spec.ext);
|
|
366
|
+
if (!file)
|
|
367
|
+
continue;
|
|
368
|
+
if (best && file.mtime <= best.mtime)
|
|
369
|
+
continue;
|
|
370
|
+
const model = spec.extract(file.path);
|
|
371
|
+
if (!model)
|
|
372
|
+
continue;
|
|
373
|
+
best = { model, tool: spec.tool, mtime: file.mtime };
|
|
374
|
+
}
|
|
375
|
+
_cache = best;
|
|
376
|
+
_cacheAt = Date.now();
|
|
377
|
+
_cacheCwd = cwd;
|
|
378
|
+
return best;
|
|
379
|
+
}
|
|
380
|
+
function invalidateTranscriptCache() { _cache = undefined; _cacheAt = 0; }
|
package/dist/daemon.js
CHANGED
|
@@ -47,6 +47,7 @@ const ModelDetector_1 = require("./core/ModelDetector");
|
|
|
47
47
|
const FileProvenance_1 = require("./core/FileProvenance");
|
|
48
48
|
const Heartbeat_1 = require("./core/Heartbeat");
|
|
49
49
|
const UI_1 = require("./core/UI");
|
|
50
|
+
const ToolHookInstallers_1 = require("./core/ToolHookInstallers");
|
|
50
51
|
const PUSH_INTERVAL_MS = 60000;
|
|
51
52
|
const IDLE_PUSH_MS = 10000;
|
|
52
53
|
const IGNORE_DIRS = /[/\\](\.git|node_modules|\.next|dist|build|__pycache__|\.venv|venv)[/\\]/;
|
|
@@ -90,7 +91,23 @@ function startDaemon(opts) {
|
|
|
90
91
|
UI_1.UI.error('Not signed in. Run: omnitype login');
|
|
91
92
|
process.exit(1);
|
|
92
93
|
}
|
|
94
|
+
// Auto-install preToolUse hooks into all supported tools.
|
|
95
|
+
(0, ToolHookInstallers_1.installAllToolHooks)();
|
|
93
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 { }
|
|
94
111
|
const projectName = opts.projectName;
|
|
95
112
|
const branch = opts.branch ?? gitBranch(watchPath);
|
|
96
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.3",
|
|
4
4
|
"description": "OmniType CLI — editor-agnostic code provenance tracking",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
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"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"boxen": "^5.1.2",
|