@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 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
+ ---
@@ -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 HOOKS_SENTINEL_PATH = path.join(os.homedir(), '.claude', 'provenance-hook.json');
54
- const UNIVERSAL_SENTINEL_PATH = path.join(os.homedir(), '.omnitype', 'active-model.json');
55
- const SENTINEL_MAX_AGE_MS = 10000;
56
- const MODEL_PATTERN = /\b(claude-[\w.-]+|gpt-[\w.-]+|o[1234](?:-[\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;
57
- // Maps known CLI/IDE env vars to their tool
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: ['CLAUDE_MODEL', 'CLAUDE_CODE_MODEL', 'ANTHROPIC_MODEL'], tool: 'claude-code' },
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', 'GEMINI_API_KEY'], tool: 'gemini-cli' },
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
- 'Cursor', 'Windsurf', 'PearAI', 'Void', 'Trae', 'Zed', 'Antigravity'
70
- ];
71
- // lsof command-name tool mapping
72
- const LSOF_CMD_MAP = [
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: 'node', tool: 'unknown-cli' },
83
- { match: 'python', tool: 'unknown-cli' },
73
+ { match: 'codex', tool: 'codex' },
74
+ { match: 'tabby', tool: 'tabby' },
84
75
  ];
85
76
  class ModelDetector {
86
- detect(changedFilePath) {
87
- return (this._fromUniversalSentinel() ??
88
- this._fromHooksSentinel() ??
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._fromPsPatterns() ??
81
+ this._fromTranscripts(cwd) ??
82
+ this._fromIdeConfigs() ??
83
+ this._fromPs() ??
92
84
  (changedFilePath ? this._fromLsof(changedFilePath) : undefined) ??
93
85
  UNKNOWN);
94
86
  }
95
- _fromUniversalSentinel() {
96
- try {
97
- const stat = fs.statSync(UNIVERSAL_SENTINEL_PATH);
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
- _fromHooksSentinel() {
93
+ _sentinel(filePath, changedPath) {
110
94
  try {
111
- const stat = fs.statSync(HOOKS_SENTINEL_PATH);
112
- if (Date.now() - stat.mtimeMs > SENTINEL_MAX_AGE_MS)
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
- const data = JSON.parse(fs.readFileSync(HOOKS_SENTINEL_PATH, 'utf8'));
115
- if (!data?.model || data.model === 'unknown')
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: data.model, tool: data.tool ?? 'claude-code', confidence: 'deterministic' };
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
- _fromEnv() {
137
- for (const { vars, tool } of ENV_VARS) {
138
- for (const v of vars) {
139
- const val = process.env[v];
140
- if (val && MODEL_PATTERN.test(val)) {
141
- return { model: val, tool, confidence: 'high' };
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
- _fromPsPatterns() {
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 entry of LSOF_CMD_MAP) {
154
- if (line.toLowerCase().includes(entry.match)) {
155
- // Fallback model name
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 { /* ps unavailable */ }
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 entry of LSOF_CMD_MAP) {
174
- if (cmd.includes(entry.match)) {
175
- return { model: `${entry.tool}-default`, tool: entry.tool, confidence: 'low' };
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 { /* lsof unavailable */ }
196
+ catch { }
181
197
  return undefined;
182
198
  }
183
199
  _scanIdeSettings(appName) {
184
- for (const p of this._getIdeSettingsPaths(appName)) {
200
+ for (const p of this._idePaths(appName)) {
185
201
  try {
186
- const raw = fs.readFileSync(p, 'utf8');
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' || !value)
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 { /* absent */ }
220
+ catch { }
213
221
  }
214
222
  return undefined;
215
223
  }
216
- _getIdeSettingsPaths(appName) {
217
- const toolId = appName.toLowerCase().replace(/\s+/g, '-');
218
- const dirCandidates = [...new Set([appName, toolId])];
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
- _flattenObject(obj, prefix = '', depth = 0) {
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._flattenObject(v, key, depth + 1));
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
+ }