@omnitype-code/cli 0.1.0 → 0.1.2

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.
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-installs OmniType model-detection hooks into AI tools that support
4
+ * a preToolUse/postToolUse hook system: Claude Code, Cursor, Windsurf, Codex, Cline.
5
+ *
6
+ * Hooks write {model, tool, ts, file} to ~/.omnitype/active-model.json.
7
+ * Including the target file path lets the detector use path-matching instead
8
+ * of a short TTL — eliminating the race between human and AI edits.
9
+ *
10
+ * Each installer is idempotent — safe to call on every startup.
11
+ * Fails silently so it never breaks the CLI for the user.
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.installAllToolHooks = installAllToolHooks;
48
+ const fs = __importStar(require("fs"));
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ const OMNITYPE_DIR = path.join(os.homedir(), '.omnitype');
52
+ // Reads stdin JSON, extracts model + file path, writes sentinel.
53
+ // `modelField` is the tool-specific key for model in the hook payload.
54
+ function buildHookCommand(tool, modelField) {
55
+ return (`node -e "let b='';process.stdin.on('data',c=>b+=c);` +
56
+ `process.stdin.on('end',()=>{` +
57
+ `try{const j=JSON.parse(b),` +
58
+ `m=j['${modelField}']||j.model,` +
59
+ `fs=require('fs'),p=require('path'),os=require('os'),` +
60
+ `dir=p.join(os.homedir(),'.omnitype');` +
61
+ `if(!m)return;` +
62
+ `let file;try{file=j?.tool_input?.path||j?.tool_input?.file_path;}catch{}` +
63
+ `fs.mkdirSync(dir,{recursive:true});` +
64
+ `fs.writeFileSync(p.join(dir,'active-model.json'),` +
65
+ `JSON.stringify(Object.assign({model:m,tool:'${tool}',ts:Date.now()},file&&{file})))}catch{}})"`);
66
+ }
67
+ // Claude doesn't expose model in hook stdin — reads from env/settings instead.
68
+ // Still reads stdin to extract the target file path for path-gated matching.
69
+ const CLAUDE_HOOK_CMD = `node -e "let b='';process.stdin.on('data',c=>b+=c);process.stdin.on('end',()=>{` +
70
+ `const fs=require('fs'),os=require('os'),p=require('path');` +
71
+ `const dir=p.join(os.homedir(),'.omnitype');` +
72
+ `let m=process.env.CLAUDE_MODEL||process.env.ANTHROPIC_MODEL;` +
73
+ `if(!m){try{const s=JSON.parse(fs.readFileSync(p.join(os.homedir(),'.claude','settings.json'),'utf8'));` +
74
+ `m=s.model||s.defaultModel;}catch{}}` +
75
+ `if(!m)return;` +
76
+ `let file;try{const j=JSON.parse(b);file=j?.tool_input?.path||j?.tool_input?.file_path;}catch{}` +
77
+ `fs.mkdirSync(dir,{recursive:true});` +
78
+ `fs.writeFileSync(p.join(dir,'active-model.json'),` +
79
+ `JSON.stringify(Object.assign({model:m,tool:'claude-code',ts:Date.now()},file&&{file})))})"`;
80
+ function readJson(filePath) {
81
+ try {
82
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
83
+ }
84
+ catch {
85
+ return {};
86
+ }
87
+ }
88
+ function writeJsonAtomic(filePath, data) {
89
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
90
+ const tmp = filePath + '.tmp';
91
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
92
+ fs.renameSync(tmp, filePath);
93
+ }
94
+ // ── Claude Code ───────────────────────────────────────────────────────────────
95
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
96
+ const CLAUDE_SETTINGS = path.join(CLAUDE_DIR, 'settings.json');
97
+ const CLAUDE_HOOK_ENTRY = {
98
+ matcher: 'Write|Edit|MultiEdit|NotebookEdit',
99
+ hooks: [{ type: 'command', command: CLAUDE_HOOK_CMD }],
100
+ };
101
+ function installClaudeHooks() {
102
+ if (!fs.existsSync(CLAUDE_DIR))
103
+ return;
104
+ const s = readJson(CLAUDE_SETTINGS);
105
+ if (!s.hooks)
106
+ s.hooks = {};
107
+ if (!Array.isArray(s.hooks.PreToolUse))
108
+ s.hooks.PreToolUse = [];
109
+ const already = s.hooks.PreToolUse.some((h) => typeof h?.hooks?.[0]?.command === 'string' && h.hooks[0].command.includes('.omnitype'));
110
+ if (!already) {
111
+ s.hooks.PreToolUse.push(CLAUDE_HOOK_ENTRY);
112
+ writeJsonAtomic(CLAUDE_SETTINGS, s);
113
+ }
114
+ }
115
+ // ── Cursor ────────────────────────────────────────────────────────────────────
116
+ const CURSOR_HOOKS_PATH = path.join(os.homedir(), '.cursor', 'hooks.json');
117
+ const CURSOR_CMD = buildHookCommand('cursor', 'model');
118
+ function installCursorHooks() {
119
+ if (!fs.existsSync(path.join(os.homedir(), '.cursor')))
120
+ return;
121
+ const settings = readJson(CURSOR_HOOKS_PATH);
122
+ if (!settings.hooks)
123
+ settings.hooks = {};
124
+ let changed = false;
125
+ for (const event of ['preToolUse', 'postToolUse']) {
126
+ if (!Array.isArray(settings.hooks[event]))
127
+ settings.hooks[event] = [];
128
+ const already = settings.hooks[event].some((h) => typeof h?.command === 'string' && h.command.includes('.omnitype'));
129
+ if (!already) {
130
+ settings.hooks[event].push({ command: CURSOR_CMD });
131
+ changed = true;
132
+ }
133
+ }
134
+ if (!settings.version)
135
+ settings.version = 1;
136
+ if (changed)
137
+ writeJsonAtomic(CURSOR_HOOKS_PATH, settings);
138
+ }
139
+ // ── Windsurf ──────────────────────────────────────────────────────────────────
140
+ const WINDSURF_HOOK_PATHS = [
141
+ path.join(os.homedir(), '.codeium', 'windsurf', 'hooks.json'),
142
+ path.join(os.homedir(), '.codeium', 'hooks.json'),
143
+ ];
144
+ const WINDSURF_EVENTS = ['pre_write_code', 'post_write_code', 'pre_run_command', 'post_run_command'];
145
+ const WINDSURF_CMD = buildHookCommand('windsurf', 'model_name');
146
+ function installWindsurfHooks() {
147
+ if (!fs.existsSync(path.join(os.homedir(), '.codeium')))
148
+ return;
149
+ for (const hookPath of WINDSURF_HOOK_PATHS) {
150
+ if (!fs.existsSync(hookPath) && !fs.existsSync(path.dirname(hookPath)))
151
+ continue;
152
+ const settings = readJson(hookPath);
153
+ if (!settings.hooks)
154
+ settings.hooks = {};
155
+ let changed = false;
156
+ for (const event of WINDSURF_EVENTS) {
157
+ if (!Array.isArray(settings.hooks[event]))
158
+ settings.hooks[event] = [];
159
+ const already = settings.hooks[event].some((h) => typeof h?.command === 'string' && h.command.includes('.omnitype'));
160
+ if (!already) {
161
+ settings.hooks[event].push({ command: WINDSURF_CMD });
162
+ changed = true;
163
+ }
164
+ }
165
+ if (changed)
166
+ writeJsonAtomic(hookPath, settings);
167
+ }
168
+ }
169
+ // ── Codex ─────────────────────────────────────────────────────────────────────
170
+ const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
171
+ const CODEX_CMD = buildHookCommand('codex', 'model');
172
+ function installCodexHooks() {
173
+ if (!fs.existsSync(path.join(os.homedir(), '.codex')))
174
+ return;
175
+ const settings = readJson(CODEX_HOOKS_PATH);
176
+ let changed = false;
177
+ for (const event of ['PreToolUse', 'PostToolUse']) {
178
+ if (!Array.isArray(settings[event]))
179
+ settings[event] = [];
180
+ const already = settings[event].some((h) => typeof h?.command === 'string' && h.command.includes('.omnitype'));
181
+ if (!already) {
182
+ settings[event].push({ type: 'command', command: CODEX_CMD });
183
+ changed = true;
184
+ }
185
+ }
186
+ if (changed)
187
+ writeJsonAtomic(CODEX_HOOKS_PATH, settings);
188
+ }
189
+ // ── Cline ─────────────────────────────────────────────────────────────────────
190
+ // Cline looks for executable scripts in ~/Documents/Cline/Hooks/ named after
191
+ // hook events. We drop a PreToolUse script that reads stdin JSON and writes
192
+ // the sentinel only for file-modifying tools.
193
+ const CLINE_HOOKS_DIR = path.join(os.homedir(), 'Documents', 'Cline', 'Hooks');
194
+ const CLINE_HOOK_SCRIPT = `#!/usr/bin/env node
195
+ const FILE_WRITE_TOOLS = new Set(['write_to_file','apply_diff','insert_content','search_and_replace']);
196
+ let buf = '';
197
+ process.stdin.on('data', c => buf += c);
198
+ process.stdin.on('end', () => {
199
+ try {
200
+ const j = JSON.parse(buf);
201
+ if (!FILE_WRITE_TOOLS.has(j.toolName)) return;
202
+ const model = j.model || j.preToolUse?.model || '';
203
+ const fs = require('fs'), p = require('path'), os = require('os');
204
+ const dir = p.join(os.homedir(), '.omnitype');
205
+ fs.mkdirSync(dir, { recursive: true });
206
+ fs.writeFileSync(p.join(dir, 'active-model.json'), JSON.stringify({ model, tool: 'cline', ts: Date.now() }));
207
+ } catch {}
208
+ });
209
+ `;
210
+ function installClineHooks() {
211
+ if (!fs.existsSync(CLINE_HOOKS_DIR))
212
+ return;
213
+ const scriptPath = path.join(CLINE_HOOKS_DIR, 'PreToolUse');
214
+ const already = fs.existsSync(scriptPath) &&
215
+ fs.readFileSync(scriptPath, 'utf8').includes('.omnitype');
216
+ if (!already) {
217
+ fs.writeFileSync(scriptPath, CLINE_HOOK_SCRIPT, 'utf8');
218
+ fs.chmodSync(scriptPath, 0o755);
219
+ }
220
+ }
221
+ // ── Public API ────────────────────────────────────────────────────────────────
222
+ function installAllToolHooks() {
223
+ try {
224
+ installClaudeHooks();
225
+ }
226
+ catch { }
227
+ try {
228
+ installCursorHooks();
229
+ }
230
+ catch { }
231
+ try {
232
+ installWindsurfHooks();
233
+ }
234
+ catch { }
235
+ try {
236
+ installCodexHooks();
237
+ }
238
+ catch { }
239
+ try {
240
+ installClineHooks();
241
+ }
242
+ catch { }
243
+ }
@@ -0,0 +1,233 @@
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
+ const SPECS = [
135
+ {
136
+ tool: 'claude-code',
137
+ dirs: () => {
138
+ const base = process.env.CLAUDE_CONFIG_DIR ?? path.join(HOME, '.claude');
139
+ const alt = path.join(HOME, '.config', 'claude');
140
+ return [path.join(base, 'projects'), path.join(alt, 'projects')];
141
+ },
142
+ ext: '.jsonl',
143
+ extract: extractJsonl,
144
+ },
145
+ {
146
+ tool: 'codex',
147
+ dirs: () => [path.join(HOME, '.codex')],
148
+ ext: '.jsonl',
149
+ extract: extractJsonl,
150
+ },
151
+ {
152
+ tool: 'gemini-cli',
153
+ dirs: () => [path.join(HOME, '.gemini', 'tmp')],
154
+ ext: '.jsonl',
155
+ extract: extractJsonl,
156
+ },
157
+ {
158
+ tool: 'amp',
159
+ dirs: () => {
160
+ const xdg = process.env.XDG_DATA_HOME;
161
+ const local = process.env.LOCALAPPDATA;
162
+ if (process.platform === 'win32')
163
+ return local ? [path.join(local, 'amp', 'threads')] : [];
164
+ return [path.join(xdg ?? path.join(HOME, '.local', 'share'), 'amp', 'threads')];
165
+ },
166
+ ext: '.json',
167
+ extract: extractAmp,
168
+ },
169
+ {
170
+ tool: 'windsurf',
171
+ dirs: () => [path.join(HOME, '.codeium', 'windsurf', 'conversations')],
172
+ ext: '.jsonl',
173
+ extract: extractJsonl,
174
+ },
175
+ ];
176
+ // ── Directory scanner: finds the newest file with the given extension ─────────
177
+ function newestFile(dirs, ext) {
178
+ let best;
179
+ function scan(dir, depth = 0) {
180
+ if (depth > 6)
181
+ return;
182
+ let entries;
183
+ try {
184
+ entries = fs.readdirSync(dir, { withFileTypes: true });
185
+ }
186
+ catch {
187
+ return;
188
+ }
189
+ for (const e of entries) {
190
+ const full = path.join(dir, e.name);
191
+ if (e.isDirectory())
192
+ scan(full, depth + 1);
193
+ else if (e.isFile() && e.name.endsWith(ext)) {
194
+ try {
195
+ const mtime = fs.statSync(full).mtimeMs;
196
+ if (!best || mtime > best.mtime)
197
+ best = { path: full, mtime };
198
+ }
199
+ catch { }
200
+ }
201
+ }
202
+ }
203
+ for (const dir of dirs)
204
+ scan(dir);
205
+ return best;
206
+ }
207
+ // ── Public API ────────────────────────────────────────────────────────────────
208
+ let _cache;
209
+ let _cacheAt = 0;
210
+ /**
211
+ * Returns the model from the most recently modified AI session transcript,
212
+ * across all supported tools. Result is cached for 60 s.
213
+ */
214
+ function scanTranscripts() {
215
+ if (_cache && Date.now() - _cacheAt < CACHE_TTL)
216
+ return _cache;
217
+ let best;
218
+ for (const spec of SPECS) {
219
+ const file = newestFile(spec.dirs(), spec.ext);
220
+ if (!file)
221
+ continue;
222
+ if (best && file.mtime <= best.mtime)
223
+ continue;
224
+ const model = spec.extract(file.path);
225
+ if (!model)
226
+ continue;
227
+ best = { model, tool: spec.tool, mtime: file.mtime };
228
+ }
229
+ _cache = best;
230
+ _cacheAt = Date.now();
231
+ return best;
232
+ }
233
+ 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,6 +91,8 @@ 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 Cursor, Windsurf, Codex if present.
95
+ (0, ToolHookInstallers_1.installAllToolHooks)();
93
96
  const watchPath = path.resolve(opts.watchPath);
94
97
  const projectName = opts.projectName;
95
98
  const branch = opts.branch ?? gitBranch(watchPath);
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@omnitype-code/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OmniType CLI — editor-agnostic code provenance tracking",
5
+ "license": "MIT",
5
6
  "bin": {
6
7
  "omnitype": "./dist/index.js"
7
8
  },