@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the **@omnitype-code/cli** package are documented here.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## [0.1.3] — 2026-05-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`omnitype doctor`** — New command showing hook status for every detected tool: `installed`, `stale`, `not-installed`, or `tool-absent`. `--fix` flag auto-installs or upgrades missing hooks. Fires market-analysis telemetry (signed-in users only).
|
|
11
|
+
- **`omnitype status` workspace fix** — Status command now passes `process.cwd()` into model detection, scoping Claude Code transcript scanning to the current workspace. Sessions from other concurrently open projects no longer bleed in.
|
|
12
|
+
- **Per-project sentinel** — Hooks write to `<project_root>/.omnitype/active-model.json` (`process.cwd()` at invocation) in addition to `~/.omnitype/` fallback. `status` and `daemon` check project-local first, eliminating cross-window contamination.
|
|
13
|
+
- **`initWorkspace()` in daemon** — `startDaemon()` creates `.omnitype/` in the watched directory and appends `.omnitype/` to `.gitignore` on first run.
|
|
14
|
+
- **New hooks auto-installed** — Gemini CLI (`~/.gemini/settings.json` BeforeTool/AfterTool), Droid / Factory (`~/.factory/settings.json` PreToolUse), Firebender (`~/.firebender/hooks.json`), GitHub Copilot (`~/.copilot/hooks/omnitype.json`).
|
|
15
|
+
- **New TS plugins** — Amp (`~/.config/amp/plugins/omnitype.ts`), OpenCode (`~/.config/opencode/plugins/omnitype.ts`), Pi (`~/.pi/agent/extensions/omnitype.ts`). Each dual-writes to project-local and global paths.
|
|
16
|
+
- **New transcript readers** — `TranscriptScanner` now covers Continue (`~/.continue/sessions/`), Copilot CLI (`~/.copilot/session-state/`), and Droid (`~/.factory/sessions/`).
|
|
17
|
+
- **`ToolDetector` updated** — Gemini CLI, Droid, Firebender, Amp, OpenCode, and Pi marked `hooked`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **`HOOK_VERSION` bumped to `v4`** — All stale v3 hooks auto-replaced on next activation.
|
|
21
|
+
- **Attribution Protocol upgraded to v1.2** — See `PROTOCOL.md`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [0.1.2] — 2026-05-16
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
- **Claude Code hook** — Added to `installAllToolHooks` — was missing from the CLI installer in v0.1.1.
|
|
31
|
+
- **Hook model fallbacks removed** — Hook writer now skips write if the model cannot be resolved. No more hardcoded `claude-sonnet-4-6` fallback.
|
|
32
|
+
- **`SENTINEL_MAX_AGE_MS`** raised from 10s to 30s for the strict (no file field) fallback path.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Detection pipeline order is now: sentinel (universal) → sentinel (hooks) → env vars → transcript scan → IDE configs → ps → lsof.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **Cline hook installer** — drops a `PreToolUse` script into `~/Documents/Cline/Hooks/` on startup. Idempotent, fires only for file-modifying tools.
|
|
39
|
+
- `installAllToolHooks` now covers Claude Code, Cursor, Windsurf, Codex, and Cline.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## [0.1.1] — 2026-05-14
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- **`omnitype signal --model <name>`** — Manually report active AI models from scripts or unsupported tools.
|
|
47
|
+
- **`omnitype blame <file>`** — ANSI-colored per-line attribution with model names.
|
|
48
|
+
- **`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).
|
|
49
|
+
- **`omnitype hooks install`** — Installs a pre-push git hook that writes git notes and syncs attribution to the cloud.
|
|
50
|
+
- **`omnitype syncNow`** — Manual provenance sync command.
|
|
51
|
+
- **Smart Claude hook** — Automatically detects model preferences from `~/.claude/settings.json`.
|
|
52
|
+
- **Protocol v1.1 support** — Writes `file` and `genId` fields in sentinel output.
|
|
53
|
+
- **Rich CLI UI** — Colors, progress spinners, and structured error reporting.
|
|
54
|
+
- **Org-only cloud storage** — Individual users remain local-only. Cloud sync gates on organization membership.
|
|
55
|
+
- **Provenance routes modularized** — Internal route handlers split into separate files.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## [0.1.0] — 2026-05-13
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- Initial CLI package.
|
|
63
|
+
- Git hook manager for push coverage — hooks written per-repo on install.
|
|
64
|
+
- AI model attribution tracking wired into personal vs. org project storage.
|
|
65
|
+
- File deletion tracking with per-line timestamps in provenance metadata.
|
|
66
|
+
- Cloud sync to S3 blobs with MongoDB manifest references.
|
|
67
|
+
|
|
68
|
+
---
|
package/dist/core/ApiClient.js
CHANGED
|
@@ -193,6 +193,21 @@ class ApiClient {
|
|
|
193
193
|
if (failed.length)
|
|
194
194
|
throw new Error(`Push failed for chunks: ${failed.join(', ')}`);
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Fire-and-forget: reports which AI tools are detected on this machine.
|
|
198
|
+
* Used for market analysis — understanding which tools users have alongside OmniType.
|
|
199
|
+
* Never throws. Only sends if the user is signed in (respects opt-in via login).
|
|
200
|
+
*/
|
|
201
|
+
reportToolEnvironment(tools) {
|
|
202
|
+
if (!this.isSignedIn)
|
|
203
|
+
return;
|
|
204
|
+
const body = { tools, platform: process.platform, ts: Date.now() };
|
|
205
|
+
fetch(`${this.apiUrl}/telemetry/tools`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Authorization': `Bearer ${this.config.token}`, 'Content-Type': 'application/json' },
|
|
208
|
+
body: JSON.stringify(body),
|
|
209
|
+
}).catch(() => { }); // best-effort, never surface errors
|
|
210
|
+
}
|
|
196
211
|
async _postWithRetry(url, body, attempts, label) {
|
|
197
212
|
const compressed = await _gzip(Buffer.from(JSON.stringify(body)));
|
|
198
213
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -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,145 @@ 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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
const SENTINEL_MAX_AGE_MS = 30000;
|
|
44
|
+
const GLOBAL_SENTINEL_PATH = path.join(os.homedir(), '.omnitype', 'active-model.json');
|
|
45
|
+
function projectSentinelPath(cwd) {
|
|
46
|
+
return path.join(cwd ?? process.cwd(), '.omnitype', 'active-model.json');
|
|
47
|
+
}
|
|
48
|
+
// Matches model identifier strings from all major providers.
|
|
49
|
+
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;
|
|
50
|
+
// Each tool lists its OWN env vars only. Shared vars (OPENAI_MODEL) are NOT duplicated
|
|
51
|
+
// across tools — the first matching entry wins, so ambiguous vars are assigned to
|
|
52
|
+
// the most common owner (openai). Tools with a dedicated var (CODEX_MODEL) are checked
|
|
53
|
+
// earlier so they can claim edits even when OPENAI_MODEL is also set.
|
|
58
54
|
const ENV_VARS = [
|
|
59
|
-
{ vars: ['
|
|
55
|
+
{ vars: ['CLAUDE_CODE_MODEL', 'CLAUDE_MODEL', 'ANTHROPIC_MODEL'], tool: 'claude-code' },
|
|
60
56
|
{ vars: ['AIDER_MODEL'], tool: 'aider' },
|
|
57
|
+
{ vars: ['CODEX_MODEL'], tool: 'codex' }, // before generic OPENAI_MODEL
|
|
61
58
|
{ vars: ['OPENAI_MODEL', 'OPENAI_API_MODEL'], tool: 'openai' },
|
|
62
|
-
{ vars: ['GEMINI_MODEL'
|
|
59
|
+
{ vars: ['GEMINI_MODEL'], tool: 'gemini-cli' },
|
|
63
60
|
{ vars: ['OLLAMA_MODEL'], tool: 'ollama' },
|
|
64
61
|
{ vars: ['COPILOT_MODEL'], tool: 'copilot' },
|
|
65
62
|
{ vars: ['LLM_MODEL'], tool: 'openhands' },
|
|
66
63
|
{ vars: ['TABBY_MODEL'], tool: 'tabby' },
|
|
67
64
|
];
|
|
68
|
-
const KNOWN_FORKS = [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
65
|
+
const KNOWN_FORKS = ['Cursor', 'Windsurf', 'PearAI', 'Void', 'Trae', 'Zed', 'Antigravity'];
|
|
66
|
+
const AUTO_SENTINELS = new Set(['auto', 'cursor-auto', 'windsurf-auto', 'default', 'best']);
|
|
67
|
+
// Host IDE processes: always running — their presence does NOT mean they caused the edit.
|
|
68
|
+
const HOST_IDE_PROCS = new Set(['cursor', 'windsurf', 'antigravity', 'pearai', 'void', 'trae', 'zed']);
|
|
69
|
+
const PROC_MAP = [
|
|
73
70
|
{ match: 'claude', tool: 'claude-code' },
|
|
74
71
|
{ 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
72
|
{ match: 'goose', tool: 'goose' },
|
|
82
|
-
{ match: '
|
|
83
|
-
{ match: '
|
|
73
|
+
{ match: 'codex', tool: 'codex' },
|
|
74
|
+
{ match: 'tabby', tool: 'tabby' },
|
|
84
75
|
];
|
|
85
76
|
class ModelDetector {
|
|
86
|
-
detect(changedFilePath) {
|
|
87
|
-
return (this.
|
|
88
|
-
this.
|
|
89
|
-
this._fromIdeConfigs() ??
|
|
77
|
+
detect(changedFilePath, cwd) {
|
|
78
|
+
return (this._sentinel(projectSentinelPath(cwd), changedFilePath) ??
|
|
79
|
+
this._sentinel(GLOBAL_SENTINEL_PATH, changedFilePath) ??
|
|
90
80
|
this._fromEnv() ??
|
|
91
|
-
this.
|
|
81
|
+
this._fromTranscripts(cwd) ??
|
|
82
|
+
this._fromIdeConfigs() ??
|
|
83
|
+
this._fromPs() ??
|
|
92
84
|
(changedFilePath ? this._fromLsof(changedFilePath) : undefined) ??
|
|
93
85
|
UNKNOWN);
|
|
94
86
|
}
|
|
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 {
|
|
87
|
+
_fromTranscripts(cwd) {
|
|
88
|
+
const r = (0, TranscriptScanner_1.scanTranscripts)(cwd);
|
|
89
|
+
if (!r)
|
|
106
90
|
return undefined;
|
|
107
|
-
}
|
|
91
|
+
return { model: r.model, tool: r.tool, confidence: 'high' };
|
|
108
92
|
}
|
|
109
|
-
|
|
93
|
+
_sentinel(filePath, changedPath) {
|
|
110
94
|
try {
|
|
111
|
-
const
|
|
112
|
-
|
|
95
|
+
const mtime = fs.statSync(filePath).mtimeMs;
|
|
96
|
+
const d = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
97
|
+
if (!d?.model || d.model === 'unknown')
|
|
113
98
|
return undefined;
|
|
114
|
-
|
|
115
|
-
|
|
99
|
+
if (d.file && changedPath) {
|
|
100
|
+
// File-path gated: trust only if the sentinel targets this exact file.
|
|
101
|
+
// Use a generous TTL since path specificity eliminates cross-file contamination.
|
|
102
|
+
if (d.file !== changedPath)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (Date.now() - mtime > 120000)
|
|
105
|
+
return undefined; // 2 min max
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// No path info: fall back to strict TTL to minimise false attribution.
|
|
109
|
+
if (Date.now() - mtime > SENTINEL_MAX_AGE_MS)
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
if (!d?.model || d.model === 'unknown')
|
|
116
113
|
return undefined;
|
|
117
|
-
return { model:
|
|
114
|
+
return { model: d.model, tool: d.tool ?? 'unknown-tool', confidence: 'deterministic' };
|
|
118
115
|
}
|
|
119
116
|
catch {
|
|
120
117
|
return undefined;
|
|
121
118
|
}
|
|
122
119
|
}
|
|
120
|
+
_fromEnv() {
|
|
121
|
+
for (const { vars, tool } of ENV_VARS)
|
|
122
|
+
for (const v of vars) {
|
|
123
|
+
const val = process.env[v];
|
|
124
|
+
if (val && MODEL_PATTERN.test(val))
|
|
125
|
+
return { model: val, tool, confidence: 'high' };
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
123
129
|
_fromIdeConfigs() {
|
|
124
130
|
for (const appName of KNOWN_FORKS) {
|
|
125
131
|
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
|
-
}
|
|
132
|
+
if (scan)
|
|
133
|
+
return { model: scan.model, tool: appName.toLowerCase(), confidence: scan.isAuto ? 'medium' : 'high' };
|
|
133
134
|
}
|
|
135
|
+
// Copilot: no hook API, but model is readable from VS Code / fork settings.json
|
|
136
|
+
const copilot = this._fromCopilotConfig();
|
|
137
|
+
if (copilot)
|
|
138
|
+
return copilot;
|
|
134
139
|
return undefined;
|
|
135
140
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
_fromCopilotConfig() {
|
|
142
|
+
const COPILOT_KEYS = [
|
|
143
|
+
'github.copilot.advanced.model',
|
|
144
|
+
'github.copilot.selectedModel',
|
|
145
|
+
'github.copilot.chat.models.default',
|
|
146
|
+
'github.copilot.chat.agent.model',
|
|
147
|
+
];
|
|
148
|
+
// Check VS Code and every known fork's settings.json
|
|
149
|
+
const vscodePaths = (() => {
|
|
150
|
+
switch (process.platform) {
|
|
151
|
+
case 'darwin': return [path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'settings.json')];
|
|
152
|
+
case 'win32': return [path.join(process.env.APPDATA ?? '', 'Code', 'User', 'settings.json')];
|
|
153
|
+
default: return [path.join(os.homedir(), '.config', 'Code', 'User', 'settings.json')];
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
for (const settingsPath of vscodePaths) {
|
|
157
|
+
try {
|
|
158
|
+
const flat = this._flatten(JSON.parse(fs.readFileSync(settingsPath, 'utf8')));
|
|
159
|
+
for (const key of COPILOT_KEYS) {
|
|
160
|
+
const val = flat[key];
|
|
161
|
+
if (typeof val === 'string' && val && MODEL_PATTERN.test(val))
|
|
162
|
+
return { model: val.toLowerCase(), tool: 'copilot', confidence: 'high' };
|
|
142
163
|
}
|
|
143
164
|
}
|
|
165
|
+
catch { }
|
|
144
166
|
}
|
|
145
167
|
return undefined;
|
|
146
168
|
}
|
|
147
|
-
|
|
169
|
+
_fromPs() {
|
|
148
170
|
if (process.platform === 'win32')
|
|
149
171
|
return undefined;
|
|
150
172
|
try {
|
|
151
173
|
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
|
-
}
|
|
174
|
+
for (const line of lines)
|
|
175
|
+
for (const { match, tool } of PROC_MAP)
|
|
176
|
+
if (line.toLowerCase().includes(match) && !HOST_IDE_PROCS.has(tool))
|
|
177
|
+
return { model: `${tool}-default`, tool, confidence: 'low' };
|
|
160
178
|
}
|
|
161
|
-
catch {
|
|
179
|
+
catch { }
|
|
162
180
|
return undefined;
|
|
163
181
|
}
|
|
164
182
|
_fromLsof(filePath) {
|
|
@@ -170,53 +188,42 @@ class ModelDetector {
|
|
|
170
188
|
if (!line.startsWith('c'))
|
|
171
189
|
continue;
|
|
172
190
|
const cmd = line.slice(1).toLowerCase();
|
|
173
|
-
for (const
|
|
174
|
-
if (cmd.includes(
|
|
175
|
-
return { model: `${
|
|
176
|
-
}
|
|
177
|
-
}
|
|
191
|
+
for (const { match, tool } of PROC_MAP)
|
|
192
|
+
if (cmd.includes(match) && !HOST_IDE_PROCS.has(tool))
|
|
193
|
+
return { model: `${tool}-default`, tool, confidence: 'low' };
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
|
-
catch {
|
|
196
|
+
catch { }
|
|
181
197
|
return undefined;
|
|
182
198
|
}
|
|
183
199
|
_scanIdeSettings(appName) {
|
|
184
|
-
for (const p of this.
|
|
200
|
+
for (const p of this._idePaths(appName)) {
|
|
185
201
|
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']);
|
|
202
|
+
const flat = this._flatten(JSON.parse(fs.readFileSync(p, 'utf8')));
|
|
190
203
|
const candidates = [];
|
|
191
204
|
for (const [key, value] of Object.entries(flat)) {
|
|
192
|
-
if (typeof value !== 'string' || !
|
|
193
|
-
continue;
|
|
194
|
-
if (!key.toLowerCase().includes('model'))
|
|
205
|
+
if (typeof value !== 'string' || !key.toLowerCase().includes('model'))
|
|
195
206
|
continue;
|
|
196
207
|
if (AUTO_SENTINELS.has(value.toLowerCase())) {
|
|
197
208
|
candidates.push({ key, value: `${appName.toLowerCase()}-auto` });
|
|
198
209
|
continue;
|
|
199
210
|
}
|
|
200
|
-
if (MODEL_PATTERN.test(value))
|
|
211
|
+
if (MODEL_PATTERN.test(value))
|
|
201
212
|
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
213
|
}
|
|
214
|
+
if (!candidates.length)
|
|
215
|
+
continue;
|
|
216
|
+
const real = candidates.filter(c => !c.value.endsWith('-auto'));
|
|
217
|
+
const pick = (real.length ? real : candidates).sort((a, b) => b.key.length - a.key.length)[0];
|
|
218
|
+
return { model: pick.value, isAuto: pick.value.endsWith('-auto') };
|
|
211
219
|
}
|
|
212
|
-
catch {
|
|
220
|
+
catch { }
|
|
213
221
|
}
|
|
214
222
|
return undefined;
|
|
215
223
|
}
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
return dirCandidates.map(dir => {
|
|
224
|
+
_idePaths(appName) {
|
|
225
|
+
const id = appName.toLowerCase().replace(/\s+/g, '-');
|
|
226
|
+
return [...new Set([appName, id])].map(dir => {
|
|
220
227
|
switch (process.platform) {
|
|
221
228
|
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support', dir, 'User', 'settings.json');
|
|
222
229
|
case 'win32': return path.join(process.env.APPDATA ?? '', dir, 'User', 'settings.json');
|
|
@@ -224,18 +231,16 @@ class ModelDetector {
|
|
|
224
231
|
}
|
|
225
232
|
});
|
|
226
233
|
}
|
|
227
|
-
|
|
234
|
+
_flatten(obj, prefix = '', depth = 0) {
|
|
228
235
|
if (depth > 5)
|
|
229
236
|
return {};
|
|
230
237
|
const out = {};
|
|
231
238
|
for (const [k, v] of Object.entries(obj)) {
|
|
232
239
|
const key = prefix ? `${prefix}.${k}` : k;
|
|
233
|
-
if (v !== null && typeof v === 'object' && !Array.isArray(v))
|
|
234
|
-
Object.assign(out, this.
|
|
235
|
-
|
|
236
|
-
else {
|
|
240
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v))
|
|
241
|
+
Object.assign(out, this._flatten(v, key, depth + 1));
|
|
242
|
+
else
|
|
237
243
|
out[key] = v;
|
|
238
|
-
}
|
|
239
244
|
}
|
|
240
245
|
return out;
|
|
241
246
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Detects every AI coding tool present on the machine, whether or not OmniType
|
|
4
|
+
* has a hook for it. Used by the doctor command and for market-analysis telemetry.
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.KNOWN_TOOLS = void 0;
|
|
41
|
+
exports.detectAllTools = detectAllTools;
|
|
42
|
+
exports.detectInstalledTools = detectInstalledTools;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const HOME = os.homedir();
|
|
48
|
+
function exists(...parts) {
|
|
49
|
+
try {
|
|
50
|
+
fs.accessSync(path.join(...parts));
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function inPath(bin) {
|
|
58
|
+
try {
|
|
59
|
+
(0, child_process_1.execFileSync)('which', [bin], { timeout: 500, stdio: 'pipe' });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function macApp(name) {
|
|
67
|
+
if (process.platform !== 'darwin')
|
|
68
|
+
return false;
|
|
69
|
+
return exists('/Applications', name) || exists(HOME, 'Applications', name);
|
|
70
|
+
}
|
|
71
|
+
exports.KNOWN_TOOLS = [
|
|
72
|
+
// ── Tools with OmniType hooks ────────────────────────────────────────────
|
|
73
|
+
{
|
|
74
|
+
id: 'claude-code',
|
|
75
|
+
name: 'Claude Code',
|
|
76
|
+
hookSupport: 'hooked',
|
|
77
|
+
detect: () => exists(HOME, '.claude') || inPath('claude'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'cursor',
|
|
81
|
+
name: 'Cursor',
|
|
82
|
+
hookSupport: 'hooked',
|
|
83
|
+
detect: () => exists(HOME, '.cursor') || macApp('Cursor.app'),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'windsurf',
|
|
87
|
+
name: 'Windsurf',
|
|
88
|
+
hookSupport: 'hooked',
|
|
89
|
+
detect: () => exists(HOME, '.codeium') || macApp('Windsurf.app'),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'codex',
|
|
93
|
+
name: 'Codex CLI',
|
|
94
|
+
hookSupport: 'hooked',
|
|
95
|
+
detect: () => exists(HOME, '.codex') || inPath('codex'),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'cline',
|
|
99
|
+
name: 'Cline',
|
|
100
|
+
hookSupport: 'hooked',
|
|
101
|
+
detect: () => exists(HOME, 'Documents', 'Cline', 'Hooks') || exists(HOME, 'Documents', 'Cline'),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'gemini-cli',
|
|
105
|
+
name: 'Gemini CLI',
|
|
106
|
+
hookSupport: 'hooked',
|
|
107
|
+
detect: () => inPath('gemini') || exists(HOME, '.gemini'),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'droid',
|
|
111
|
+
name: 'Droid (Factory)',
|
|
112
|
+
hookSupport: 'hooked',
|
|
113
|
+
detect: () => exists(HOME, '.factory'),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'firebender',
|
|
117
|
+
name: 'Firebender',
|
|
118
|
+
hookSupport: 'hooked',
|
|
119
|
+
detect: () => exists(HOME, '.firebender'),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'amp',
|
|
123
|
+
name: 'Amp',
|
|
124
|
+
hookSupport: 'hooked',
|
|
125
|
+
detect: () => {
|
|
126
|
+
const xdg = process.env.XDG_DATA_HOME ?? path.join(HOME, '.local', 'share');
|
|
127
|
+
return inPath('amp') || exists(xdg, 'amp') ||
|
|
128
|
+
(process.platform === 'win32' && exists(process.env.LOCALAPPDATA ?? '', 'amp'));
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'opencode',
|
|
133
|
+
name: 'OpenCode',
|
|
134
|
+
hookSupport: 'hooked',
|
|
135
|
+
detect: () => exists(HOME, '.config', 'opencode') || inPath('opencode'),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'pi',
|
|
139
|
+
name: 'Pi',
|
|
140
|
+
hookSupport: 'hooked',
|
|
141
|
+
detect: () => exists(HOME, '.pi'),
|
|
142
|
+
},
|
|
143
|
+
// ── Tools detected but no hook yet ──────────────────────────────────────
|
|
144
|
+
{
|
|
145
|
+
id: 'aider',
|
|
146
|
+
name: 'Aider',
|
|
147
|
+
hookSupport: 'no-hook',
|
|
148
|
+
detect: () => inPath('aider') || exists(HOME, '.aider.conf.yml') || exists(HOME, '.aider'),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'continue',
|
|
152
|
+
name: 'Continue',
|
|
153
|
+
hookSupport: 'no-hook',
|
|
154
|
+
detect: () => exists(HOME, '.continue') || inPath('continue'),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'antigravity',
|
|
158
|
+
name: 'Antigravity',
|
|
159
|
+
hookSupport: 'no-hook',
|
|
160
|
+
detect: () => macApp('Antigravity.app') || macApp('Google Antigravity.app'),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'pearai',
|
|
164
|
+
name: 'PearAI',
|
|
165
|
+
hookSupport: 'no-hook',
|
|
166
|
+
detect: () => macApp('PearAI.app') || exists(HOME, '.pearai'),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'void',
|
|
170
|
+
name: 'Void',
|
|
171
|
+
hookSupport: 'no-hook',
|
|
172
|
+
detect: () => macApp('Void.app') || exists(HOME, '.void'),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'zed',
|
|
176
|
+
name: 'Zed',
|
|
177
|
+
hookSupport: 'no-hook',
|
|
178
|
+
detect: () => macApp('Zed.app') ||
|
|
179
|
+
(process.platform === 'linux' && (inPath('zed') || exists(HOME, '.config', 'zed'))),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'roo-cline',
|
|
183
|
+
name: 'Roo Code',
|
|
184
|
+
hookSupport: 'no-hook',
|
|
185
|
+
detect: () => exists(HOME, '.roo') || exists(HOME, '.roo-cline'),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'tabnine',
|
|
189
|
+
name: 'Tabnine',
|
|
190
|
+
hookSupport: 'no-hook',
|
|
191
|
+
detect: () => exists(HOME, '.tabnine') || exists(HOME, '.config', 'tabnine'),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'supermaven',
|
|
195
|
+
name: 'Supermaven',
|
|
196
|
+
hookSupport: 'no-hook',
|
|
197
|
+
detect: () => exists(HOME, '.supermaven'),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'codeium',
|
|
201
|
+
name: 'Codeium',
|
|
202
|
+
hookSupport: 'no-hook',
|
|
203
|
+
// .codeium overlaps with Windsurf — only count if Windsurf isn't the reason
|
|
204
|
+
detect: () => exists(HOME, '.codeium', 'codeium') || exists(HOME, '.codeium', 'config'),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'copilot',
|
|
208
|
+
name: 'GitHub Copilot',
|
|
209
|
+
// Copilot supports PreToolUse/PostToolUse hooks at ~/.copilot/hooks/
|
|
210
|
+
hookSupport: 'hooked',
|
|
211
|
+
detect: () => exists(HOME, '.copilot') ||
|
|
212
|
+
exists(HOME, '.vscode') ||
|
|
213
|
+
exists(HOME, 'Library', 'Application Support', 'Code') ||
|
|
214
|
+
exists(HOME, '.config', 'github-copilot'),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'trae',
|
|
218
|
+
name: 'Trae',
|
|
219
|
+
hookSupport: 'no-hook',
|
|
220
|
+
detect: () => macApp('Trae.app') || exists(HOME, '.trae'),
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'goose',
|
|
224
|
+
name: 'Goose',
|
|
225
|
+
hookSupport: 'no-hook',
|
|
226
|
+
detect: () => inPath('goose') || exists(HOME, '.config', 'goose'),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'openai-codex-legacy',
|
|
230
|
+
name: 'OpenAI Codex (legacy)',
|
|
231
|
+
hookSupport: 'no-hook',
|
|
232
|
+
detect: () => !!process.env.OPENAI_API_KEY && !exists(HOME, '.codex'),
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
/** Returns all known tools with a detected flag. */
|
|
236
|
+
function detectAllTools() {
|
|
237
|
+
return exports.KNOWN_TOOLS.map(tool => {
|
|
238
|
+
let detected = false;
|
|
239
|
+
try {
|
|
240
|
+
detected = tool.detect();
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
return { tool, detected };
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** Returns only the tools that are actually installed. */
|
|
247
|
+
function detectInstalledTools() {
|
|
248
|
+
return detectAllTools().filter(r => r.detected).map(r => r.tool);
|
|
249
|
+
}
|