@omnitype-code/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/LICENSE +21 -0
- package/README.md +104 -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 +2 -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
|
+
---
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OmniType
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# OmniType CLI
|
|
2
|
+
|
|
3
|
+
**Know exactly who wrote every line of code — you, your AI, or a paste.**
|
|
4
|
+
|
|
5
|
+
OmniType is an editor-agnostic code provenance engine. It tracks which lines were written by a human, generated by an AI model (Claude, GPT-4, Gemini, etc.), or pasted in — across every tool you use.
|
|
6
|
+
|
|
7
|
+
No vendor lock-in. Works with Claude Code, Cursor, Windsurf, VS Code, Aider, Continue, and any AI coding tool.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @omnitype-code/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Sign in
|
|
23
|
+
omnitype login
|
|
24
|
+
|
|
25
|
+
# Start tracking in your project
|
|
26
|
+
omnitype daemon
|
|
27
|
+
|
|
28
|
+
# See who wrote what
|
|
29
|
+
omnitype blame src/app.ts
|
|
30
|
+
|
|
31
|
+
# Check your AI vs human split
|
|
32
|
+
omnitype stats
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
| Command | Description |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `omnitype login` | Sign in to OmniType Cloud |
|
|
42
|
+
| `omnitype logout` | Sign out |
|
|
43
|
+
| `omnitype status` | Show auth, detected model, and repo info |
|
|
44
|
+
| `omnitype whoami` | Display your profile |
|
|
45
|
+
| `omnitype daemon` | Watch a directory and track provenance in real time |
|
|
46
|
+
| `omnitype blame <file>` | Enhanced `git blame` with AI/model attribution overlay |
|
|
47
|
+
| `omnitype stats` | Personal provenance stats across all projects |
|
|
48
|
+
| `omnitype hooks install` | Install git hooks for commit-level tracking |
|
|
49
|
+
| `omnitype hooks uninstall` | Remove git hooks |
|
|
50
|
+
| `omnitype notes fetch` | Pull attribution notes from remote |
|
|
51
|
+
| `omnitype notes push` | Push attribution notes to remote |
|
|
52
|
+
| `omnitype setup-claude-hook` | Auto-detect model in Claude Code sessions |
|
|
53
|
+
| `omnitype setup-vscode-hook` | Install into VS Code, Cursor, Windsurf, and other forks |
|
|
54
|
+
| `omnitype signal -m <model>` | Manually report the active AI model |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Editor Integrations
|
|
59
|
+
|
|
60
|
+
### Claude Code
|
|
61
|
+
```bash
|
|
62
|
+
omnitype setup-claude-hook
|
|
63
|
+
```
|
|
64
|
+
Installs a hook into `~/.claude/settings.json` that captures the active model on every file edit — automatically.
|
|
65
|
+
|
|
66
|
+
### VS Code / Cursor / Windsurf / and more
|
|
67
|
+
```bash
|
|
68
|
+
omnitype setup-vscode-hook
|
|
69
|
+
```
|
|
70
|
+
Detects every installed VS Code fork and enables the OmniType extension across all of them in one shot.
|
|
71
|
+
|
|
72
|
+
### Any AI tool
|
|
73
|
+
```bash
|
|
74
|
+
omnitype signal --model gpt-4o --tool aider
|
|
75
|
+
```
|
|
76
|
+
Works with any tool that doesn't have a native integration yet.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## How It Works
|
|
81
|
+
|
|
82
|
+
1. **Daemon** watches your project directory for file changes and records which AI model was active at the time of each edit.
|
|
83
|
+
2. **Git hooks** capture provenance at commit time, stored as git notes so they travel with your repo.
|
|
84
|
+
3. **Blame** overlays model attribution on top of standard `git blame` output.
|
|
85
|
+
4. **Stats** aggregates your AI vs. human vs. paste ratios across all tracked projects.
|
|
86
|
+
|
|
87
|
+
Provenance data is stored locally and optionally synced to OmniType Cloud for team dashboards and org-level analytics.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Options
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
omnitype daemon --path ./src --project my-app
|
|
95
|
+
omnitype blame src/index.ts --stats
|
|
96
|
+
omnitype stats --top 20
|
|
97
|
+
omnitype notes push --remote upstream
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -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
|
}
|