@omnitype-code/cli 0.1.1 → 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.
- package/CHANGELOG.md +50 -0
- package/dist/core/ModelDetector.js +84 -113
- package/dist/core/ToolHookInstallers.js +243 -0
- package/dist/core/TranscriptScanner.js +233 -0
- package/dist/daemon.js +3 -0
- package/package.json +1 -1
- package/src/core/ModelDetector.ts +88 -130
- package/src/core/ToolHookInstallers.ts +192 -0
- package/src/core/TranscriptScanner.ts +204 -0
- package/src/daemon.ts +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the **@omnitype-code/cli** package are documented here.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## [0.1.2] — 2026-05-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Sentinel race condition** — Replaced 10s time-window TTL with path-gated matching. Sentinel is only trusted if `sentinel.file === changedPath`, eliminating cross-file attribution contamination.
|
|
11
|
+
- **TranscriptScanner** — Added as a detection tier between env vars and IDE config parsing. Reads the 50KB tail of the most recent JSONL session file per tool, with a 60s cache.
|
|
12
|
+
- **Claude Code hook** — Added to `installAllToolHooks` — was missing from the CLI installer in v0.1.1.
|
|
13
|
+
- **Hook model fallbacks removed** — Hook writer now skips write if the model cannot be resolved. No more hardcoded `claude-sonnet-4-6` fallback.
|
|
14
|
+
- **`SENTINEL_MAX_AGE_MS`** raised from 10s to 30s for the strict (no file field) fallback path.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Detection pipeline order is now: sentinel (universal) → sentinel (hooks) → env vars → transcript scan → IDE configs → ps → lsof.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Cline hook installer** — drops a `PreToolUse` script into `~/Documents/Cline/Hooks/` on startup. Idempotent, fires only for file-modifying tools.
|
|
21
|
+
- `installAllToolHooks` now covers Claude Code, Cursor, Windsurf, Codex, and Cline.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [0.1.1] — 2026-05-14
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`omnitype signal --model <name>`** — Manually report active AI models from scripts or unsupported tools.
|
|
29
|
+
- **`omnitype blame <file>`** — ANSI-colored per-line attribution with model names.
|
|
30
|
+
- **`omnitype daemon`** — Editor-agnostic file watching for Cursor, Windsurf, JetBrains, Neovim, and any other editor. Auto-yields to the VS Code extension for live-tracked workspaces (30-second yield guard).
|
|
31
|
+
- **`omnitype hooks install`** — Installs a pre-push git hook that writes git notes and syncs attribution to the cloud.
|
|
32
|
+
- **`omnitype syncNow`** — Manual provenance sync command.
|
|
33
|
+
- **Smart Claude hook** — Automatically detects model preferences from `~/.claude/settings.json`.
|
|
34
|
+
- **Protocol v1.1 support** — Writes `file` and `genId` fields in sentinel output.
|
|
35
|
+
- **Rich CLI UI** — Colors, progress spinners, and structured error reporting.
|
|
36
|
+
- **Org-only cloud storage** — Individual users remain local-only. Cloud sync gates on organization membership.
|
|
37
|
+
- **Provenance routes modularized** — Internal route handlers split into separate files.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## [0.1.0] — 2026-05-13
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Initial CLI package.
|
|
45
|
+
- Git hook manager for push coverage — hooks written per-repo on install.
|
|
46
|
+
- AI model attribution tracking wired into personal vs. org project storage.
|
|
47
|
+
- File deletion tracking with per-line timestamps in provenance metadata.
|
|
48
|
+
- Cloud sync to S3 blobs with MongoDB manifest references.
|
|
49
|
+
|
|
50
|
+
---
|
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* CLI ModelDetector — editor-agnostic model detection.
|
|
4
|
-
*
|
|
5
|
-
* Detection tiers:
|
|
6
|
-
* 1. Universal sentinel (~/.omnitype/active-model.json)
|
|
7
|
-
* 2. Hooks sentinel file (~/.claude/provenance-hook.json)
|
|
8
|
-
* 3. Host IDE config (Cursor/Windsurf/Zed settings.json — scanned generically)
|
|
9
|
-
* 4. Config files (per-tool config paths: .aider.conf.yml, etc.)
|
|
10
|
-
* 5. Environment variables (CLAUDE_MODEL, AIDER_MODEL, etc.)
|
|
11
|
-
* 6. Process detection (lsof / ps — identifies the writing process)
|
|
12
|
-
*/
|
|
13
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
3
|
if (k2 === undefined) k2 = k;
|
|
15
4
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -49,116 +38,111 @@ const fs = __importStar(require("fs"));
|
|
|
49
38
|
const os = __importStar(require("os"));
|
|
50
39
|
const path = __importStar(require("path"));
|
|
51
40
|
const child_process_1 = require("child_process");
|
|
41
|
+
const TranscriptScanner_1 = require("./TranscriptScanner");
|
|
52
42
|
const UNKNOWN = { model: 'unknown', tool: 'unknown', confidence: 'low' };
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
const SENTINEL_MAX_AGE_MS = 30000; // 30 s — covers slow multi-file AI diffs
|
|
44
|
+
const UNIVERSAL_PATH = path.join(os.homedir(), '.omnitype', 'active-model.json');
|
|
45
|
+
const HOOKS_PATH = path.join(os.homedir(), '.claude', 'provenance-hook.json');
|
|
46
|
+
// Matches model identifier strings from all major providers.
|
|
47
|
+
const MODEL_PATTERN = /\b(claude-[\w.-]+|gpt-[\w.-]+|o[134](?:-[\w.-]+)?|gemini-[\w.-]+|gemma[\w.-]*|llama-[\w.-]+|mistral[\w.-]*|codestral[\w.-]*|deepseek[\w.-]*|qwen[\w.-]+|command[\w.-]*|phi[\w.-]+|grok[\w.-]*|kimi[\w.-]*|moonshot[\w.-]*)\b/i;
|
|
48
|
+
// Each tool lists its OWN env vars only. Shared vars (OPENAI_MODEL) are NOT duplicated
|
|
49
|
+
// across tools — the first matching entry wins, so ambiguous vars are assigned to
|
|
50
|
+
// the most common owner (openai). Tools with a dedicated var (CODEX_MODEL) are checked
|
|
51
|
+
// earlier so they can claim edits even when OPENAI_MODEL is also set.
|
|
58
52
|
const ENV_VARS = [
|
|
59
|
-
{ vars: ['
|
|
53
|
+
{ vars: ['CLAUDE_CODE_MODEL', 'CLAUDE_MODEL', 'ANTHROPIC_MODEL'], tool: 'claude-code' },
|
|
60
54
|
{ vars: ['AIDER_MODEL'], tool: 'aider' },
|
|
55
|
+
{ vars: ['CODEX_MODEL'], tool: 'codex' }, // before generic OPENAI_MODEL
|
|
61
56
|
{ vars: ['OPENAI_MODEL', 'OPENAI_API_MODEL'], tool: 'openai' },
|
|
62
|
-
{ vars: ['GEMINI_MODEL'
|
|
57
|
+
{ vars: ['GEMINI_MODEL'], tool: 'gemini-cli' },
|
|
63
58
|
{ vars: ['OLLAMA_MODEL'], tool: 'ollama' },
|
|
64
59
|
{ vars: ['COPILOT_MODEL'], tool: 'copilot' },
|
|
65
60
|
{ vars: ['LLM_MODEL'], tool: 'openhands' },
|
|
66
61
|
{ vars: ['TABBY_MODEL'], tool: 'tabby' },
|
|
67
62
|
];
|
|
68
|
-
const KNOWN_FORKS = [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
63
|
+
const KNOWN_FORKS = ['Cursor', 'Windsurf', 'PearAI', 'Void', 'Trae', 'Zed', 'Antigravity'];
|
|
64
|
+
const AUTO_SENTINELS = new Set(['auto', 'cursor-auto', 'windsurf-auto', 'default', 'best']);
|
|
65
|
+
// Host IDE processes: always running — their presence does NOT mean they caused the edit.
|
|
66
|
+
const HOST_IDE_PROCS = new Set(['cursor', 'windsurf', 'antigravity', 'pearai', 'void', 'trae', 'zed']);
|
|
67
|
+
const PROC_MAP = [
|
|
73
68
|
{ match: 'claude', tool: 'claude-code' },
|
|
74
69
|
{ match: 'aider', tool: 'aider' },
|
|
75
|
-
{ match: 'cursor', tool: 'cursor' },
|
|
76
|
-
{ match: 'windsurf', tool: 'windsurf' },
|
|
77
|
-
{ match: 'zed', tool: 'zed' },
|
|
78
|
-
{ match: 'pearai', tool: 'pearai' },
|
|
79
|
-
{ match: 'void', tool: 'void' },
|
|
80
|
-
{ match: 'tabby', tool: 'tabby' },
|
|
81
70
|
{ match: 'goose', tool: 'goose' },
|
|
82
|
-
{ match: '
|
|
83
|
-
{ match: '
|
|
71
|
+
{ match: 'codex', tool: 'codex' },
|
|
72
|
+
{ match: 'tabby', tool: 'tabby' },
|
|
84
73
|
];
|
|
85
74
|
class ModelDetector {
|
|
86
75
|
detect(changedFilePath) {
|
|
87
|
-
return (this.
|
|
88
|
-
this.
|
|
89
|
-
this._fromIdeConfigs() ??
|
|
76
|
+
return (this._sentinel(UNIVERSAL_PATH, changedFilePath) ??
|
|
77
|
+
this._sentinel(HOOKS_PATH, changedFilePath) ??
|
|
90
78
|
this._fromEnv() ??
|
|
91
|
-
this.
|
|
79
|
+
this._fromTranscripts() ??
|
|
80
|
+
this._fromIdeConfigs() ??
|
|
81
|
+
this._fromPs() ??
|
|
92
82
|
(changedFilePath ? this._fromLsof(changedFilePath) : undefined) ??
|
|
93
83
|
UNKNOWN);
|
|
94
84
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (Date.now() - stat.mtimeMs > SENTINEL_MAX_AGE_MS)
|
|
99
|
-
return undefined;
|
|
100
|
-
const data = JSON.parse(fs.readFileSync(UNIVERSAL_SENTINEL_PATH, 'utf8'));
|
|
101
|
-
if (!data?.model || data.model === 'unknown')
|
|
102
|
-
return undefined;
|
|
103
|
-
return { model: data.model, tool: data.tool ?? 'unknown-tool', confidence: 'deterministic' };
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
85
|
+
_fromTranscripts() {
|
|
86
|
+
const r = (0, TranscriptScanner_1.scanTranscripts)();
|
|
87
|
+
if (!r)
|
|
106
88
|
return undefined;
|
|
107
|
-
}
|
|
89
|
+
return { model: r.model, tool: r.tool, confidence: 'high' };
|
|
108
90
|
}
|
|
109
|
-
|
|
91
|
+
_sentinel(filePath, changedPath) {
|
|
110
92
|
try {
|
|
111
|
-
const
|
|
112
|
-
|
|
93
|
+
const mtime = fs.statSync(filePath).mtimeMs;
|
|
94
|
+
const d = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
95
|
+
if (!d?.model || d.model === 'unknown')
|
|
113
96
|
return undefined;
|
|
114
|
-
|
|
115
|
-
|
|
97
|
+
if (d.file && changedPath) {
|
|
98
|
+
// File-path gated: trust only if the sentinel targets this exact file.
|
|
99
|
+
// Use a generous TTL since path specificity eliminates cross-file contamination.
|
|
100
|
+
if (d.file !== changedPath)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (Date.now() - mtime > 120000)
|
|
103
|
+
return undefined; // 2 min max
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// No path info: fall back to strict TTL to minimise false attribution.
|
|
107
|
+
if (Date.now() - mtime > SENTINEL_MAX_AGE_MS)
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
if (!d?.model || d.model === 'unknown')
|
|
116
111
|
return undefined;
|
|
117
|
-
return { model:
|
|
112
|
+
return { model: d.model, tool: d.tool ?? 'unknown-tool', confidence: 'deterministic' };
|
|
118
113
|
}
|
|
119
114
|
catch {
|
|
120
115
|
return undefined;
|
|
121
116
|
}
|
|
122
117
|
}
|
|
123
|
-
_fromIdeConfigs() {
|
|
124
|
-
for (const appName of KNOWN_FORKS) {
|
|
125
|
-
const scan = this._scanIdeSettings(appName);
|
|
126
|
-
if (scan) {
|
|
127
|
-
return {
|
|
128
|
-
model: scan.model,
|
|
129
|
-
tool: appName.toLowerCase(),
|
|
130
|
-
confidence: scan.isAuto ? 'medium' : 'high'
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
118
|
_fromEnv() {
|
|
137
|
-
for (const { vars, tool } of ENV_VARS)
|
|
119
|
+
for (const { vars, tool } of ENV_VARS)
|
|
138
120
|
for (const v of vars) {
|
|
139
121
|
const val = process.env[v];
|
|
140
|
-
if (val && MODEL_PATTERN.test(val))
|
|
122
|
+
if (val && MODEL_PATTERN.test(val))
|
|
141
123
|
return { model: val, tool, confidence: 'high' };
|
|
142
|
-
}
|
|
143
124
|
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
_fromIdeConfigs() {
|
|
128
|
+
for (const appName of KNOWN_FORKS) {
|
|
129
|
+
const scan = this._scanIdeSettings(appName);
|
|
130
|
+
if (scan)
|
|
131
|
+
return { model: scan.model, tool: appName.toLowerCase(), confidence: scan.isAuto ? 'medium' : 'high' };
|
|
144
132
|
}
|
|
145
133
|
return undefined;
|
|
146
134
|
}
|
|
147
|
-
|
|
135
|
+
_fromPs() {
|
|
148
136
|
if (process.platform === 'win32')
|
|
149
137
|
return undefined;
|
|
150
138
|
try {
|
|
151
139
|
const lines = (0, child_process_1.execFileSync)('ps', ['ax', '-o', 'args='], { timeout: 800, encoding: 'utf8' }).split('\n');
|
|
152
|
-
for (const line of lines)
|
|
153
|
-
for (const
|
|
154
|
-
if (line.toLowerCase().includes(
|
|
155
|
-
|
|
156
|
-
return { model: `${entry.tool}-default`, tool: entry.tool, confidence: 'low' };
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
140
|
+
for (const line of lines)
|
|
141
|
+
for (const { match, tool } of PROC_MAP)
|
|
142
|
+
if (line.toLowerCase().includes(match) && !HOST_IDE_PROCS.has(tool))
|
|
143
|
+
return { model: `${tool}-default`, tool, confidence: 'low' };
|
|
160
144
|
}
|
|
161
|
-
catch {
|
|
145
|
+
catch { }
|
|
162
146
|
return undefined;
|
|
163
147
|
}
|
|
164
148
|
_fromLsof(filePath) {
|
|
@@ -170,53 +154,42 @@ class ModelDetector {
|
|
|
170
154
|
if (!line.startsWith('c'))
|
|
171
155
|
continue;
|
|
172
156
|
const cmd = line.slice(1).toLowerCase();
|
|
173
|
-
for (const
|
|
174
|
-
if (cmd.includes(
|
|
175
|
-
return { model: `${
|
|
176
|
-
}
|
|
177
|
-
}
|
|
157
|
+
for (const { match, tool } of PROC_MAP)
|
|
158
|
+
if (cmd.includes(match) && !HOST_IDE_PROCS.has(tool))
|
|
159
|
+
return { model: `${tool}-default`, tool, confidence: 'low' };
|
|
178
160
|
}
|
|
179
161
|
}
|
|
180
|
-
catch {
|
|
162
|
+
catch { }
|
|
181
163
|
return undefined;
|
|
182
164
|
}
|
|
183
165
|
_scanIdeSettings(appName) {
|
|
184
|
-
for (const p of this.
|
|
166
|
+
for (const p of this._idePaths(appName)) {
|
|
185
167
|
try {
|
|
186
|
-
const
|
|
187
|
-
const json = JSON.parse(raw);
|
|
188
|
-
const flat = this._flattenObject(json);
|
|
189
|
-
const AUTO_SENTINELS = new Set(['auto', 'cursor-auto', 'windsurf-auto', 'default', 'best']);
|
|
168
|
+
const flat = this._flatten(JSON.parse(fs.readFileSync(p, 'utf8')));
|
|
190
169
|
const candidates = [];
|
|
191
170
|
for (const [key, value] of Object.entries(flat)) {
|
|
192
|
-
if (typeof value !== 'string' || !
|
|
193
|
-
continue;
|
|
194
|
-
if (!key.toLowerCase().includes('model'))
|
|
171
|
+
if (typeof value !== 'string' || !key.toLowerCase().includes('model'))
|
|
195
172
|
continue;
|
|
196
173
|
if (AUTO_SENTINELS.has(value.toLowerCase())) {
|
|
197
174
|
candidates.push({ key, value: `${appName.toLowerCase()}-auto` });
|
|
198
175
|
continue;
|
|
199
176
|
}
|
|
200
|
-
if (MODEL_PATTERN.test(value))
|
|
177
|
+
if (MODEL_PATTERN.test(value))
|
|
201
178
|
candidates.push({ key, value: value.toLowerCase() });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (candidates.length > 0) {
|
|
205
|
-
const real = candidates.filter(c => !c.value.endsWith('-auto'));
|
|
206
|
-
const pick = real.length > 0
|
|
207
|
-
? real.sort((a, b) => b.key.length - a.key.length)[0]
|
|
208
|
-
: candidates.sort((a, b) => b.key.length - a.key.length)[0];
|
|
209
|
-
return { model: pick.value, isAuto: pick.value.endsWith('-auto') };
|
|
210
179
|
}
|
|
180
|
+
if (!candidates.length)
|
|
181
|
+
continue;
|
|
182
|
+
const real = candidates.filter(c => !c.value.endsWith('-auto'));
|
|
183
|
+
const pick = (real.length ? real : candidates).sort((a, b) => b.key.length - a.key.length)[0];
|
|
184
|
+
return { model: pick.value, isAuto: pick.value.endsWith('-auto') };
|
|
211
185
|
}
|
|
212
|
-
catch {
|
|
186
|
+
catch { }
|
|
213
187
|
}
|
|
214
188
|
return undefined;
|
|
215
189
|
}
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
return dirCandidates.map(dir => {
|
|
190
|
+
_idePaths(appName) {
|
|
191
|
+
const id = appName.toLowerCase().replace(/\s+/g, '-');
|
|
192
|
+
return [...new Set([appName, id])].map(dir => {
|
|
220
193
|
switch (process.platform) {
|
|
221
194
|
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support', dir, 'User', 'settings.json');
|
|
222
195
|
case 'win32': return path.join(process.env.APPDATA ?? '', dir, 'User', 'settings.json');
|
|
@@ -224,18 +197,16 @@ class ModelDetector {
|
|
|
224
197
|
}
|
|
225
198
|
});
|
|
226
199
|
}
|
|
227
|
-
|
|
200
|
+
_flatten(obj, prefix = '', depth = 0) {
|
|
228
201
|
if (depth > 5)
|
|
229
202
|
return {};
|
|
230
203
|
const out = {};
|
|
231
204
|
for (const [k, v] of Object.entries(obj)) {
|
|
232
205
|
const key = prefix ? `${prefix}.${k}` : k;
|
|
233
|
-
if (v !== null && typeof v === 'object' && !Array.isArray(v))
|
|
234
|
-
Object.assign(out, this.
|
|
235
|
-
|
|
236
|
-
else {
|
|
206
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v))
|
|
207
|
+
Object.assign(out, this._flatten(v, key, depth + 1));
|
|
208
|
+
else
|
|
237
209
|
out[key] = v;
|
|
238
|
-
}
|
|
239
210
|
}
|
|
240
211
|
return out;
|
|
241
212
|
}
|
|
@@ -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
|
+
}
|