@omnitype-code/cli 0.1.2 → 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 CHANGED
@@ -4,6 +4,24 @@ All notable changes to the **@omnitype-code/cli** package are documented here.
4
4
 
5
5
  ---
6
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
+
7
25
  ## [0.1.2] — 2026-05-16
8
26
 
9
27
  ### Fixed
@@ -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++) {
@@ -40,9 +40,11 @@ const path = __importStar(require("path"));
40
40
  const child_process_1 = require("child_process");
41
41
  const TranscriptScanner_1 = require("./TranscriptScanner");
42
42
  const UNKNOWN = { model: 'unknown', tool: 'unknown', confidence: 'low' };
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');
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
+ }
46
48
  // Matches model identifier strings from all major providers.
47
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;
48
50
  // Each tool lists its OWN env vars only. Shared vars (OPENAI_MODEL) are NOT duplicated
@@ -72,18 +74,18 @@ const PROC_MAP = [
72
74
  { match: 'tabby', tool: 'tabby' },
73
75
  ];
74
76
  class ModelDetector {
75
- detect(changedFilePath) {
76
- return (this._sentinel(UNIVERSAL_PATH, changedFilePath) ??
77
- this._sentinel(HOOKS_PATH, changedFilePath) ??
77
+ detect(changedFilePath, cwd) {
78
+ return (this._sentinel(projectSentinelPath(cwd), changedFilePath) ??
79
+ this._sentinel(GLOBAL_SENTINEL_PATH, changedFilePath) ??
78
80
  this._fromEnv() ??
79
- this._fromTranscripts() ??
81
+ this._fromTranscripts(cwd) ??
80
82
  this._fromIdeConfigs() ??
81
83
  this._fromPs() ??
82
84
  (changedFilePath ? this._fromLsof(changedFilePath) : undefined) ??
83
85
  UNKNOWN);
84
86
  }
85
- _fromTranscripts() {
86
- const r = (0, TranscriptScanner_1.scanTranscripts)();
87
+ _fromTranscripts(cwd) {
88
+ const r = (0, TranscriptScanner_1.scanTranscripts)(cwd);
87
89
  if (!r)
88
90
  return undefined;
89
91
  return { model: r.model, tool: r.tool, confidence: 'high' };
@@ -130,6 +132,38 @@ class ModelDetector {
130
132
  if (scan)
131
133
  return { model: scan.model, tool: appName.toLowerCase(), confidence: scan.isAuto ? 'medium' : 'high' };
132
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;
139
+ return undefined;
140
+ }
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' };
163
+ }
164
+ }
165
+ catch { }
166
+ }
133
167
  return undefined;
134
168
  }
135
169
  _fromPs() {
@@ -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
+ }