skannr 0.2.2 → 0.2.4
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/ARCHITECTURE.md +229 -0
- package/README.md +209 -260
- package/dist/guard/provider.d.ts.map +1 -1
- package/dist/guard/provider.js +94 -3
- package/dist/guard/provider.js.map +1 -1
- package/dist/guard/rules-loader.d.ts.map +1 -1
- package/dist/guard/rules-loader.js +58 -7
- package/dist/guard/rules-loader.js.map +1 -1
- package/dist/guard/schema.d.ts +3 -3
- package/dist/guard/schema.js +2 -2
- package/dist/guard/schema.js.map +1 -1
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/guard/provider.ts +109 -3
- package/src/guard/rules-loader.ts +61 -6
- package/src/guard/schema.ts +2 -2
- package/src/guard/types.ts +3 -3
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Skannr: Architecture and Design Decisions
|
|
2
|
+
|
|
3
|
+
Overview
|
|
4
|
+
|
|
5
|
+
Skannr is a CLI tool, MCP server, and library that helps developers and AI assistants understand codebases without reading every file. It scans a project, generates compressed structural skeletons of the most relevant files, and returns them ranked by relevance to a natural language question. The output is 96.5% smaller than raw source while retaining all architectural information.
|
|
6
|
+
|
|
7
|
+
The tool also includes risk analysis (quantifying the downstream impact of a code change) and guard (AI code review against team rules with auto-fix capability).
|
|
8
|
+
|
|
9
|
+
Core Problem
|
|
10
|
+
|
|
11
|
+
AI coding assistants are limited by context windows. A typical repo has thousands of files. You cannot paste them all. The assistant sees only what you show it, and guesses the rest.
|
|
12
|
+
|
|
13
|
+
Existing approaches either dump full files (expensive, noisy) or do keyword search (misses architecturally central files that do not mention the search term directly). Neither approach understands the structural relationships between files.
|
|
14
|
+
|
|
15
|
+
Skannr solves this by combining three ranking signals with structural compression, so the AI gets the right files in the right format at minimal token cost.
|
|
16
|
+
|
|
17
|
+
System Architecture
|
|
18
|
+
|
|
19
|
+
The pipeline is linear and stateless. Each stage takes input from the previous stage and produces output for the next. There is no shared mutable state.
|
|
20
|
+
|
|
21
|
+
Input (question + root path)
|
|
22
|
+
> File Scanner (discovers all source files)
|
|
23
|
+
> Ranker (scores files by relevance using hybrid retrieval)
|
|
24
|
+
> Skeletonizer (strips function bodies, keeps signatures and types)
|
|
25
|
+
> Formatter (outputs human text, markdown, or JSON)
|
|
26
|
+
|
|
27
|
+
Each stage is a separate module. They communicate through typed interfaces, not through side effects.
|
|
28
|
+
|
|
29
|
+
File Layout
|
|
30
|
+
|
|
31
|
+
src/
|
|
32
|
+
cli.ts Command registration (Commander.js, subcommands)
|
|
33
|
+
index.ts Library entry point, orchestrates the pipeline
|
|
34
|
+
scanner.ts File discovery with gitignore awareness
|
|
35
|
+
ranker.ts Basic keyword relevance scoring
|
|
36
|
+
ranker-enhanced.ts Hybrid ranking with dependency graph analysis
|
|
37
|
+
skeletonizer.ts Routes files to the correct language adapter
|
|
38
|
+
formatter.ts Output formatting (human, markdown, JSON)
|
|
39
|
+
blast-radius.ts Risk analysis (diff parsing, graph traversal, scoring)
|
|
40
|
+
guard/ AI code review module (schema, provider, fix applier)
|
|
41
|
+
languages/ Language adapters (TypeScript via ts-morph, others via WASM)
|
|
42
|
+
mcp-server.ts MCP tool registration for AI assistant integration
|
|
43
|
+
types.ts Shared type definitions
|
|
44
|
+
|
|
45
|
+
Design Decisions
|
|
46
|
+
|
|
47
|
+
1. Hybrid Retrieval over Pure Keyword Search
|
|
48
|
+
|
|
49
|
+
Decision: Score files using three signals combined, not just keyword matching.
|
|
50
|
+
|
|
51
|
+
The three signals:
|
|
52
|
+
Lexical: keyword frequency in file path and content, with synonym expansion
|
|
53
|
+
Structural: export density, import relevance, symbol count
|
|
54
|
+
Dependency graph: how many other files import this file (centrality)
|
|
55
|
+
|
|
56
|
+
Why: A file named utils.ts might contain the most architecturally important auth logic but never mention the word "auth" in its filename. Pure keyword search misses it. Dependency centrality catches it because everything imports it.
|
|
57
|
+
|
|
58
|
+
The weights are: lexical 50%, structural 20%, enhanced score 30%, with a dependency boost of 15% blended in. These were tuned empirically against large TypeScript monorepos.
|
|
59
|
+
|
|
60
|
+
2. AST Skeleton Generation over Summarization
|
|
61
|
+
|
|
62
|
+
Decision: Generate skeletons by stripping function bodies from the AST, not by asking an LLM to summarize.
|
|
63
|
+
|
|
64
|
+
Why:
|
|
65
|
+
Deterministic (same input always produces same output)
|
|
66
|
+
Fast (milliseconds, not seconds)
|
|
67
|
+
Free (no API call)
|
|
68
|
+
Lossless for architecture (all types, signatures, imports, class hierarchies preserved)
|
|
69
|
+
Only lossy for implementation details (function bodies replaced with a placeholder)
|
|
70
|
+
|
|
71
|
+
The TypeScript adapter uses ts-morph (a TypeScript compiler API wrapper) which gives perfect AST access. For Python, Go, Rust, and Java, the tool uses web-tree-sitter (WASM compiled grammars) which gives equivalent structural parsing without requiring native C++ compilation.
|
|
72
|
+
|
|
73
|
+
3. WASM over Native Tree-sitter
|
|
74
|
+
|
|
75
|
+
Decision: Ship tree-sitter grammars as WASM files instead of native bindings.
|
|
76
|
+
|
|
77
|
+
Why: Native tree-sitter requires a C++ compiler (Visual Studio on Windows, Xcode on Mac). Most JavaScript developers do not have these installed. The npm install would fail for the majority of users.
|
|
78
|
+
|
|
79
|
+
WASM grammars are platform-independent. They run everywhere Node.js runs. The tradeoff is slightly slower parsing (roughly 2x), but since we only parse files the ranker already selected as relevant (typically 5-10 files), the total time remains under 2 seconds.
|
|
80
|
+
|
|
81
|
+
The WASM files add 2MB to the package. This is acceptable because it eliminates a class of installation failures entirely.
|
|
82
|
+
|
|
83
|
+
4. File-level Dependency Graph (not Symbol-level)
|
|
84
|
+
|
|
85
|
+
Decision: Track dependencies at file granularity (file A imports file B) not at symbol granularity (function X calls function Y).
|
|
86
|
+
|
|
87
|
+
Why: Symbol-level call graphs require type resolution, which in turn requires a full project compilation context. TypeScript's type checker can do this but only for TS/JS. Python, Go, Rust, and Java would each need their own type resolver. The engineering cost is disproportionate to the accuracy gain for the primary use case (ranking files by relevance).
|
|
88
|
+
|
|
89
|
+
File-level import graphs are trivially extractable from any language via regex or AST import node detection. They provide sufficient signal for ranking and risk analysis.
|
|
90
|
+
|
|
91
|
+
This is documented as a stated limitation. Symbol-level resolution is planned for a future version.
|
|
92
|
+
|
|
93
|
+
5. Deterministic Risk Scoring over LLM-based Assessment
|
|
94
|
+
|
|
95
|
+
Decision: Compute risk as a weighted formula with named constants, not by asking an LLM "how risky is this."
|
|
96
|
+
|
|
97
|
+
The formula:
|
|
98
|
+
risk = 2.5 * (affected files / total files)
|
|
99
|
+
+ 2.5 * (average centrality of affected files)
|
|
100
|
+
+ 3.5 * (fraction of affected files with no test coverage)
|
|
101
|
+
+ 1.5 * (max hop depth reached / max hops configured)
|
|
102
|
+
|
|
103
|
+
Why:
|
|
104
|
+
Deterministic: same diff always produces same score
|
|
105
|
+
Fast: runs in under 2 seconds with zero API calls
|
|
106
|
+
CI-gateable: you can block merges on a number, not on an LLM's mood
|
|
107
|
+
Transparent: the formula is visible, the weights are named constants, the inputs are shown
|
|
108
|
+
Reproducible: another developer running the same command gets the same result
|
|
109
|
+
|
|
110
|
+
The untested ratio has the highest weight (3.5 out of 10) because untested downstream code is the strongest signal that a merge is risky. This weighting was a deliberate choice: the number that should most influence whether someone trusts a merge is whether the things it might break have tests.
|
|
111
|
+
|
|
112
|
+
6. Guard: Structured JSON Contract over Text Parsing
|
|
113
|
+
|
|
114
|
+
Decision: Enforce schema-validated JSON responses from the LLM. Never parse free text looking for status keywords.
|
|
115
|
+
|
|
116
|
+
Why: This is a direct response to a known failure mode. Other tools in this space parse LLM responses as raw text, looking for a specific line (like "STATUS: PASSED") at a fixed position. This breaks when providers prepend acknowledgment text ("Sure! Here's my analysis:") before the actual response.
|
|
117
|
+
|
|
118
|
+
Skannr Guard uses:
|
|
119
|
+
Gemini API's responseMimeType: 'application/json' (forces JSON output)
|
|
120
|
+
OpenAI's response_format: { type: 'json_object' } (same)
|
|
121
|
+
Zod schema validation on every response before using it
|
|
122
|
+
A JSON extraction fallback that finds the first { to last } regardless of surrounding text
|
|
123
|
+
One retry with an explicit correction prompt on validation failure
|
|
124
|
+
|
|
125
|
+
The violation object has a fixed schema. The fixable field is set by the rule definition, not by the LLM. This prevents the LLM from deciding on its own what it can auto-fix.
|
|
126
|
+
|
|
127
|
+
7. Guard: CLI Auto-detection over Mandatory API Keys
|
|
128
|
+
|
|
129
|
+
Decision: If the user has Claude Code, Gemini CLI, Kiro, or Ollama installed, use it directly. Do not require a separate API key.
|
|
130
|
+
|
|
131
|
+
Detection priority:
|
|
132
|
+
1. claude CLI (uses existing Claude Code authenticated session)
|
|
133
|
+
2. gemini CLI (uses existing Gemini authenticated session)
|
|
134
|
+
3. kiro-cli (uses existing Kiro session)
|
|
135
|
+
4. ollama (local, no auth needed)
|
|
136
|
+
5. Gemini API (needs GEMINI_API_KEY)
|
|
137
|
+
6. OpenAI API (needs OPENAI_API_KEY)
|
|
138
|
+
|
|
139
|
+
Why: The biggest friction in AI-powered dev tools is API key setup. If someone already has a coding agent installed and authenticated, they should not need to configure anything else. The tool should use what is already there.
|
|
140
|
+
|
|
141
|
+
This mirrors how GGA (gentleman-guardian-angel) works: it shells out to existing CLI tools rather than making its own API calls. The difference is that Skannr Guard also supports direct API calls as a fallback for users who do not have any CLI tool installed.
|
|
142
|
+
|
|
143
|
+
8. Subcommands over Flags
|
|
144
|
+
|
|
145
|
+
Decision: Structure the CLI as subcommands (skannr risk, skannr guard, skannr agent) rather than a single command with 20+ flags.
|
|
146
|
+
|
|
147
|
+
Why: The original CLI had --question, --root, --report, --cache-clear, --cache-stats, --diff, --watch, --mcp, --telemetry-on, --telemetry-off all on one command. Users had to read --help every time to find which flags go together.
|
|
148
|
+
|
|
149
|
+
Subcommands group related functionality. Each subcommand has only the flags relevant to it. The most common operation (asking a question) takes a bare positional argument:
|
|
150
|
+
|
|
151
|
+
skannr "how does auth work?"
|
|
152
|
+
|
|
153
|
+
No flag needed. The tool should be typeable from memory after seeing it once.
|
|
154
|
+
|
|
155
|
+
Backward compatibility: all old flags still work. The new commands are aliases, not replacements.
|
|
156
|
+
|
|
157
|
+
9. Built-in Default Rules over Mandatory Configuration
|
|
158
|
+
|
|
159
|
+
Decision: Guard works without any rules file. Built-in defaults cover universal best practices.
|
|
160
|
+
|
|
161
|
+
Default rules: no-any-type, no-console-log, error-handling, no-hardcoded-secrets, function-complexity, unused-imports.
|
|
162
|
+
|
|
163
|
+
Why: Every extra file a tool requires before it works is friction that prevents adoption. If the user has to create .skannr/rules.json before they can try the tool, most will not try it.
|
|
164
|
+
|
|
165
|
+
The custom rules file is optional. When present, it overrides defaults. Teams that need domain-specific rules can add them. Everyone else gets reasonable defaults out of the box.
|
|
166
|
+
|
|
167
|
+
10. MCP as the Integration Layer
|
|
168
|
+
|
|
169
|
+
Decision: Expose all features as MCP tools (scan_codebase, blast_radius, guard_review) over stdio.
|
|
170
|
+
|
|
171
|
+
Why: MCP (Model Context Protocol) is the emerging standard for giving AI assistants access to external tools. One protocol works with Cursor, Claude Code, Gemini CLI, VS Code Copilot, and any future MCP-compatible tool.
|
|
172
|
+
|
|
173
|
+
The alternative would be building separate plugins for each editor. MCP avoids that entirely. One implementation, every tool.
|
|
174
|
+
|
|
175
|
+
The MCP server and CLI share the same internal functions. There is no code duplication between the two surfaces.
|
|
176
|
+
|
|
177
|
+
Technology Choices
|
|
178
|
+
|
|
179
|
+
TypeScript: The project itself is written in TypeScript. This aligns with the primary audience (JavaScript/TypeScript developers) and allows dogfooding the TypeScript adapter on the project itself.
|
|
180
|
+
|
|
181
|
+
Commander.js: CLI framework. Mature, well-typed, supports subcommands and positional arguments. Already a standard choice in the Node.js ecosystem.
|
|
182
|
+
|
|
183
|
+
ts-morph: TypeScript/JavaScript AST access. Wraps the TypeScript compiler API with a cleaner interface. Gives perfect type information, symbol extraction, and skeleton generation for TS/JS files.
|
|
184
|
+
|
|
185
|
+
web-tree-sitter: Multi-language parsing via WASM. No native compilation. Supports 40+ languages via grammar files. We bundle Python, Go, Rust, Java (2MB total).
|
|
186
|
+
|
|
187
|
+
parse-diff: Unified diff parsing. Small focused library, well-maintained, has types. Used by blast-radius to extract changed files and line ranges from git diffs.
|
|
188
|
+
|
|
189
|
+
zod: Schema validation for Guard's rules file and LLM response contract. Runtime type checking that integrates with TypeScript's type system.
|
|
190
|
+
|
|
191
|
+
@modelcontextprotocol/sdk: Official MCP SDK. Handles the stdio transport protocol so we only implement tool logic.
|
|
192
|
+
|
|
193
|
+
@google/generative-ai: Gemini SDK for Guard's direct API calls and the interactive agent mode.
|
|
194
|
+
|
|
195
|
+
What is Not Included (and Why)
|
|
196
|
+
|
|
197
|
+
No vector embeddings: Skannr does not use embeddings or a vector store. The hybrid ranking (lexical + structural + graph centrality) is sufficient for finding relevant files. Embeddings would add a model dependency, increase latency, and require a build step to generate the index.
|
|
198
|
+
|
|
199
|
+
No persistent index: Each run scans fresh. The cache is keyed by file hash (MD5) and invalidates automatically when files change. A persistent index would be faster for repeated queries on unchanged codebases, but adds complexity around invalidation and stale results.
|
|
200
|
+
|
|
201
|
+
No UI: Skannr is a CLI tool and MCP server. There is no web dashboard or VS Code extension. The landing page is marketing only. The tool's value is in the terminal and in the AI assistant context window.
|
|
202
|
+
|
|
203
|
+
No custom language grammars: Adding a new language means adding one config entry (which AST node types represent functions, classes, imports) and one WASM grammar file. We do not write custom parsers per language.
|
|
204
|
+
|
|
205
|
+
Testing Approach
|
|
206
|
+
|
|
207
|
+
Jest with ts-jest. Tests are in a flat tests/ directory, named *.test.ts.
|
|
208
|
+
|
|
209
|
+
Test patterns:
|
|
210
|
+
Temp directories with real files (not mocks of the filesystem)
|
|
211
|
+
beforeEach creates fixtures, afterEach cleans up
|
|
212
|
+
WASM dependencies mocked in tests to avoid loading .wasm files in CI
|
|
213
|
+
Integration tests run git commands in real temp repos
|
|
214
|
+
|
|
215
|
+
Coverage areas:
|
|
216
|
+
Schema validation (valid input accepted, invalid input rejected)
|
|
217
|
+
Graph traversal (correct hop distances, cycle handling, max-hop limits)
|
|
218
|
+
Risk scoring (known input/output pairs)
|
|
219
|
+
Fix scoping (never touches non-fixable violations)
|
|
220
|
+
Hook installation (append-safe, idempotent, clean uninstall)
|
|
221
|
+
|
|
222
|
+
Performance Characteristics
|
|
223
|
+
|
|
224
|
+
Typical query: 1-2 seconds end to end on a 2000-file TypeScript repo
|
|
225
|
+
Risk analysis: under 2 seconds (no API call)
|
|
226
|
+
Guard review: 5-15 seconds (LLM round-trip)
|
|
227
|
+
Package size: 450KB compressed, 2.8MB unpacked
|
|
228
|
+
Memory: under 100MB for typical repos
|
|
229
|
+
No background processes: runs, outputs, exits
|
package/README.md
CHANGED
|
@@ -1,260 +1,209 @@
|
|
|
1
|
-
# Skannr
|
|
2
|
-
|
|
3
|
-
A CLI tool and MCP server that helps AI
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
skannr
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
skannr
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
skannr
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Changed symbols:
|
|
212
|
-
function validateToken (src/auth/session.ts)
|
|
213
|
-
|
|
214
|
-
Hop 1 (2 files):
|
|
215
|
-
src/middleware/auth-guard.ts centrality=0.85 [NO TEST]
|
|
216
|
-
src/api/login.ts centrality=0.60
|
|
217
|
-
|
|
218
|
-
Hop 2 (2 files):
|
|
219
|
-
src/routes/index.ts centrality=0.45 [NO TEST]
|
|
220
|
-
src/api/admin.ts centrality=0.30 [NO TEST]
|
|
221
|
-
|
|
222
|
-
Formula inputs:
|
|
223
|
-
affected_count (normalized): 0.080
|
|
224
|
-
avg_centrality: 0.550
|
|
225
|
-
untested_ratio: 0.750
|
|
226
|
-
hop_spread (normalized): 1.000
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### MCP
|
|
230
|
-
|
|
231
|
-
The `blast_radius` tool is exposed via the same MCP server (`skannr --mcp`).
|
|
232
|
-
|
|
233
|
-
Input schema:
|
|
234
|
-
|
|
235
|
-
```json
|
|
236
|
-
{
|
|
237
|
-
"root": "/path/to/repo",
|
|
238
|
-
"diff": "<optional unified diff content>",
|
|
239
|
-
"hops": 2
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
If `diff` is omitted, the tool runs `git diff HEAD` in the given root.
|
|
244
|
-
The output is the same JSON structure as `skannr blast-radius --json`.
|
|
245
|
-
|
|
246
|
-
### Risk Score Formula
|
|
247
|
-
|
|
248
|
-
```
|
|
249
|
-
risk = 2.5 × normalizedAffectedCount
|
|
250
|
-
+ 2.5 × avgCentrality
|
|
251
|
-
+ 3.5 × untestedRatio
|
|
252
|
-
+ 1.5 × normalizedMaxHopSpread
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
| Input | Range | Meaning |
|
|
256
|
-
|-------|-------|---------|
|
|
257
|
-
| normalizedAffectedCount | 0–1 | Affected files / total project files |
|
|
258
|
-
| avgCentrality | 0–1 | Mean in-degree centrality of affected files |
|
|
259
|
-
| untestedRatio | 0–1 | Fraction of affected files with no test file |
|
|
260
|
-
| normalizedMaxHopSpread | 0–1 | Deepest hop reached / max configured hops |
|
|
1
|
+
# Skannr
|
|
2
|
+
|
|
3
|
+
A CLI tool and MCP server that helps developers and AI assistants understand any codebase. Scans files, generates compressed structural skeletons, and ranks results using hybrid retrieval. Reduces token usage by 96.5% vs full file reads.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g skannr
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx skannr "how does auth work?"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Ask about any codebase
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
skannr "how does authentication work?"
|
|
23
|
+
skannr "database queries" -n 5
|
|
24
|
+
skannr "API endpoints" --json
|
|
25
|
+
skannr "class structure" --lang python
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
No config needed. Auto-detects language and module structure.
|
|
29
|
+
|
|
30
|
+
### Risk analysis
|
|
31
|
+
|
|
32
|
+
Check the downstream impact of your changes before pushing:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
skannr risk
|
|
36
|
+
skannr risk --json
|
|
37
|
+
skannr risk -n 3
|
|
38
|
+
skannr risk --diff feature.patch
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Walks the dependency graph, finds affected files, flags untested code, gives you a 0-10 score. No LLM, no API key, runs in under 2 seconds.
|
|
42
|
+
|
|
43
|
+
### Guard (AI code review)
|
|
44
|
+
|
|
45
|
+
Reviews staged changes against coding rules at the symbol level:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git add .
|
|
49
|
+
skannr guard
|
|
50
|
+
skannr guard --fix
|
|
51
|
+
skannr guard --fix --dry-run
|
|
52
|
+
skannr guard --pr-mode
|
|
53
|
+
skannr guard --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Auto-detects your coding agent (Claude, Gemini, Kiro, Ollama). No API key needed if you already have one installed. Works out of the box with built-in rules.
|
|
57
|
+
|
|
58
|
+
Install as a pre-commit hook:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
skannr guard install
|
|
62
|
+
skannr guard uninstall
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Custom rules (optional):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
// .skannr/rules.json
|
|
69
|
+
{
|
|
70
|
+
"rules": [
|
|
71
|
+
{
|
|
72
|
+
"id": "no-mutations",
|
|
73
|
+
"description": "Do not mutate function arguments",
|
|
74
|
+
"severity": "high",
|
|
75
|
+
"fixable": false,
|
|
76
|
+
"category": "functional"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Other commands
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
skannr report # repo health summary (JSON)
|
|
86
|
+
skannr agent # interactive exploration mode
|
|
87
|
+
skannr cache stats # cache hit rate
|
|
88
|
+
skannr cache clear # wipe cache
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Supported Languages
|
|
92
|
+
|
|
93
|
+
| Language | Support | Method |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| TypeScript / TSX | Full AST | ts-morph |
|
|
96
|
+
| JavaScript / JSX | Full AST | ts-morph |
|
|
97
|
+
| Python | Full AST | tree-sitter WASM |
|
|
98
|
+
| Go | Full AST | tree-sitter WASM |
|
|
99
|
+
| Rust | Full AST | tree-sitter WASM |
|
|
100
|
+
| Java | Full AST | tree-sitter WASM |
|
|
101
|
+
| Others | Basic | First 50 lines fallback |
|
|
102
|
+
|
|
103
|
+
No native build tools required. WASM grammars run everywhere npm runs.
|
|
104
|
+
|
|
105
|
+
## MCP Server
|
|
106
|
+
|
|
107
|
+
Exposes three tools: `scan_codebase`, `blast_radius`, `guard_review`.
|
|
108
|
+
|
|
109
|
+
One-time setup for your AI tool:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"mcpServers": {
|
|
114
|
+
"skannr": {
|
|
115
|
+
"command": "npx",
|
|
116
|
+
"args": ["-y", "skannr", "--mcp"]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Works with Cursor, Claude Code, Gemini CLI, Kiro, VS Code Copilot, and any MCP-compatible tool.
|
|
123
|
+
|
|
124
|
+
After setup, just ask your assistant:
|
|
125
|
+
- "How does auth work in this repo?"
|
|
126
|
+
- "Review my staged changes"
|
|
127
|
+
- "What's the risk of my current diff?"
|
|
128
|
+
|
|
129
|
+
## Risk Score Formula
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
risk = 2.5 x (affected files / total files)
|
|
133
|
+
+ 2.5 x (avg centrality of affected files)
|
|
134
|
+
+ 3.5 x (% of affected files with no tests)
|
|
135
|
+
+ 1.5 x (max hop reached / max hops configured)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Deterministic. Same diff always produces the same score. CI-gateable:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
RISK=$(skannr risk --json | jq '.riskScore')
|
|
142
|
+
if [ $(echo "$RISK > 7" | bc -l) -eq 1 ]; then exit 1; fi
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Guard Providers
|
|
146
|
+
|
|
147
|
+
Guard auto-detects in this order (no config needed):
|
|
148
|
+
|
|
149
|
+
1. `claude` CLI (uses your existing session)
|
|
150
|
+
2. `gemini` CLI (uses your existing session)
|
|
151
|
+
3. `kiro-cli` (uses your existing session)
|
|
152
|
+
4. `ollama` (local, no auth)
|
|
153
|
+
5. Gemini API (set `GEMINI_API_KEY`)
|
|
154
|
+
6. OpenAI API (set `OPENAI_API_KEY`)
|
|
155
|
+
|
|
156
|
+
Override with `SKANNR_GUARD_PROVIDER` env var or `.skannr/guard.json`.
|
|
157
|
+
|
|
158
|
+
## Config File (optional)
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
// code-analyzer.config.json (project root)
|
|
162
|
+
{
|
|
163
|
+
"modules": {
|
|
164
|
+
"auth": ["src/auth", "lib/auth"],
|
|
165
|
+
"api": ["src/api", "src/routes"]
|
|
166
|
+
},
|
|
167
|
+
"exclude": ["**/generated/**", "**/migrations/**"],
|
|
168
|
+
"extensions": [".ts", ".js"],
|
|
169
|
+
"defaultLimit": 10
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Performance
|
|
174
|
+
|
|
175
|
+
| Metric | Result |
|
|
176
|
+
|---|---|
|
|
177
|
+
| Token reduction vs full scan | 96.5% |
|
|
178
|
+
| Typical query time | 1-2 seconds |
|
|
179
|
+
| Risk analysis | under 2 seconds (no API call) |
|
|
180
|
+
| Guard review | 5-15 seconds (LLM round-trip) |
|
|
181
|
+
| Package size | 450KB compressed |
|
|
182
|
+
|
|
183
|
+
## Interactive Agent
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
skannr agent
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Commands inside the agent:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
/files List retrieved files
|
|
193
|
+
/symbols <query> Search for symbols
|
|
194
|
+
/symbol <id> Get full implementation
|
|
195
|
+
/deps <file> Show imports for a file
|
|
196
|
+
/refresh Re-analyze with new context
|
|
197
|
+
/stats Show mapping statistics
|
|
198
|
+
/exit Quit
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Requires `GEMINI_API_KEY` for the interactive agent mode.
|
|
202
|
+
|
|
203
|
+
## Architecture
|
|
204
|
+
|
|
205
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for design decisions, technology choices, and the reasoning behind each one.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/guard/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/guard/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA2OtF;;;GAGG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,SAAS,EAAE,EAClB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,cAAc,CAAC,CAgEzB"}
|
package/dist/guard/provider.js
CHANGED
|
@@ -109,6 +109,82 @@ function extractJson(text) {
|
|
|
109
109
|
}
|
|
110
110
|
return text.slice(firstBrace, lastBrace + 1);
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Call a CLI-based provider (claude, gemini, kiro).
|
|
114
|
+
* These use the user's existing authenticated session — no API key needed.
|
|
115
|
+
*/
|
|
116
|
+
async function callCli(command, fullPrompt) {
|
|
117
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
118
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
119
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
120
|
+
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
|
121
|
+
// Write prompt to a temp file (avoids ARG_MAX limits on large diffs)
|
|
122
|
+
const tmpFile = path.join(os.tmpdir(), `skannr-guard-${Date.now()}.txt`);
|
|
123
|
+
fs.writeFileSync(tmpFile, fullPrompt, 'utf-8');
|
|
124
|
+
try {
|
|
125
|
+
let result;
|
|
126
|
+
switch (command) {
|
|
127
|
+
case 'claude-cli':
|
|
128
|
+
// Claude CLI accepts prompt via stdin pipe with --print flag
|
|
129
|
+
result = execSync(`cat "${tmpFile}" | claude --print`, {
|
|
130
|
+
encoding: 'utf-8',
|
|
131
|
+
timeout: 120_000,
|
|
132
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
case 'gemini-cli':
|
|
136
|
+
// Gemini CLI accepts prompt via -p flag
|
|
137
|
+
result = execSync(`gemini -p "$(cat "${tmpFile}")"`, {
|
|
138
|
+
encoding: 'utf-8',
|
|
139
|
+
timeout: 120_000,
|
|
140
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
141
|
+
});
|
|
142
|
+
break;
|
|
143
|
+
case 'kiro-cli':
|
|
144
|
+
// Kiro CLI accepts prompt via stdin in non-interactive mode
|
|
145
|
+
result = execSync(`cat "${tmpFile}" | kiro-cli chat --no-interactive "Review and respond with JSON only."`, {
|
|
146
|
+
encoding: 'utf-8',
|
|
147
|
+
timeout: 120_000,
|
|
148
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unknown CLI provider: ${command}`);
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
try {
|
|
158
|
+
fs.unlinkSync(tmpFile);
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Call Ollama local inference.
|
|
165
|
+
*/
|
|
166
|
+
async function callOllama(model, systemPrompt, userPrompt) {
|
|
167
|
+
const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
168
|
+
const url = `${baseUrl}/api/chat`;
|
|
169
|
+
const response = await fetch(url, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
model: model || 'llama3',
|
|
174
|
+
messages: [
|
|
175
|
+
{ role: 'system', content: systemPrompt },
|
|
176
|
+
{ role: 'user', content: userPrompt },
|
|
177
|
+
],
|
|
178
|
+
stream: false,
|
|
179
|
+
format: 'json',
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
|
|
184
|
+
}
|
|
185
|
+
const data = await response.json();
|
|
186
|
+
return data.message?.content ?? '';
|
|
187
|
+
}
|
|
112
188
|
/**
|
|
113
189
|
* Call Gemini API with structured output enforcement.
|
|
114
190
|
*/
|
|
@@ -175,9 +251,24 @@ async function runLlmReview(config, rules, units) {
|
|
|
175
251
|
: `${userPrompt}\n\n[RETRY: Your last response didn't match the required schema. Error: ${lastError}. Return ONLY valid JSON matching the schema.]`;
|
|
176
252
|
let rawResponse;
|
|
177
253
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
254
|
+
switch (config.provider) {
|
|
255
|
+
case 'openai':
|
|
256
|
+
rawResponse = await callOpenAI(config, systemPrompt, prompt);
|
|
257
|
+
break;
|
|
258
|
+
case 'gemini':
|
|
259
|
+
rawResponse = await callGemini(config, systemPrompt, prompt);
|
|
260
|
+
break;
|
|
261
|
+
case 'ollama':
|
|
262
|
+
rawResponse = await callOllama(config.model, systemPrompt, prompt);
|
|
263
|
+
break;
|
|
264
|
+
case 'claude-cli':
|
|
265
|
+
case 'gemini-cli':
|
|
266
|
+
case 'kiro-cli':
|
|
267
|
+
rawResponse = await callCli(config.provider, systemPrompt + '\n\n' + prompt);
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
271
|
+
}
|
|
181
272
|
}
|
|
182
273
|
catch (err) {
|
|
183
274
|
throw new Error(`Provider call failed (${config.provider}): ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/guard/provider.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/guard/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules-loader.d.ts","sourceRoot":"","sources":["../../src/guard/rules-loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAwDtD,+FAA+F;AAC/F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,CAwBnD;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"rules-loader.d.ts","sourceRoot":"","sources":["../../src/guard/rules-loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAwDtD,+FAA+F;AAC/F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,CAwBnD;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CA8BzD"}
|
|
@@ -42,7 +42,6 @@ exports.loadGuardConfig = loadGuardConfig;
|
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const schema_1 = require("./schema");
|
|
45
|
-
const schema_2 = require("./schema");
|
|
46
45
|
/** Default locations to search for the rules file. */
|
|
47
46
|
const RULES_PATHS = [
|
|
48
47
|
'.skannr/rules.json',
|
|
@@ -130,18 +129,70 @@ function loadGuardConfig(root) {
|
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
// Env vars override file config
|
|
133
|
-
const
|
|
134
|
-
const model = (process.env.SKANNR_GUARD_MODEL || rawConfig.model || '
|
|
132
|
+
const explicitProvider = process.env.SKANNR_GUARD_PROVIDER || rawConfig.provider;
|
|
133
|
+
const model = (process.env.SKANNR_GUARD_MODEL || rawConfig.model || '');
|
|
135
134
|
const apiKey = process.env.SKANNR_GUARD_API_KEY
|
|
136
135
|
|| process.env.GEMINI_API_KEY
|
|
137
136
|
|| process.env.OPENAI_API_KEY
|
|
138
137
|
|| rawConfig.apiKey;
|
|
139
138
|
const baseUrl = process.env.OPENAI_BASE_URL || rawConfig.baseUrl;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// Auto-detect provider if not explicitly set
|
|
140
|
+
const provider = explicitProvider || autoDetectProvider(apiKey);
|
|
141
|
+
return {
|
|
142
|
+
provider: provider,
|
|
143
|
+
model: model || getDefaultModel(provider),
|
|
144
|
+
apiKey,
|
|
145
|
+
baseUrl,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Auto-detect the best available provider.
|
|
150
|
+
* Priority: existing CLI tools (free, already authenticated) > API keys.
|
|
151
|
+
*/
|
|
152
|
+
function autoDetectProvider(apiKey) {
|
|
153
|
+
const { execSync } = require('child_process');
|
|
154
|
+
// Check for CLI tools first (no API key needed)
|
|
155
|
+
const cliChecks = [
|
|
156
|
+
['claude', 'claude-cli'],
|
|
157
|
+
['gemini', 'gemini-cli'],
|
|
158
|
+
['kiro-cli', 'kiro-cli'],
|
|
159
|
+
];
|
|
160
|
+
for (const [cmd, provider] of cliChecks) {
|
|
161
|
+
try {
|
|
162
|
+
execSync(`${cmd} --version`, { stdio: 'ignore', timeout: 3000 });
|
|
163
|
+
return provider;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Not available, try next
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Check for Ollama (local, no key needed)
|
|
170
|
+
try {
|
|
171
|
+
execSync('ollama --version', { stdio: 'ignore', timeout: 3000 });
|
|
172
|
+
return 'ollama';
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Not available
|
|
176
|
+
}
|
|
177
|
+
// Fall back to API-based providers if key is available
|
|
178
|
+
if (apiKey) {
|
|
179
|
+
if (process.env.GEMINI_API_KEY)
|
|
180
|
+
return 'gemini';
|
|
181
|
+
if (process.env.OPENAI_API_KEY)
|
|
182
|
+
return 'openai';
|
|
183
|
+
return 'gemini'; // default API provider
|
|
184
|
+
}
|
|
185
|
+
// Nothing found — default to gemini-cli and let it fail with a clear error
|
|
186
|
+
return 'gemini-cli';
|
|
187
|
+
}
|
|
188
|
+
/** Get the default model for a provider. */
|
|
189
|
+
function getDefaultModel(provider) {
|
|
190
|
+
switch (provider) {
|
|
191
|
+
case 'gemini': return 'gemini-2.0-flash-exp';
|
|
192
|
+
case 'openai': return 'gpt-4o';
|
|
193
|
+
case 'ollama': return 'llama3';
|
|
194
|
+
default: return ''; // CLI providers don't need a model specified
|
|
143
195
|
}
|
|
144
|
-
return result.data;
|
|
145
196
|
}
|
|
146
197
|
/** Find the first existing rules file in the project. */
|
|
147
198
|
function findRulesFile(root) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules-loader.js","sourceRoot":"","sources":["../../src/guard/rules-loader.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DH,8BAwBC;AAGD,
|
|
1
|
+
{"version":3,"file":"rules-loader.js","sourceRoot":"","sources":["../../src/guard/rules-loader.ts"],"names":[],"mappings":"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"}
|
package/dist/guard/schema.d.ts
CHANGED
|
@@ -164,19 +164,19 @@ export declare const ReviewResponseSchema: z.ZodObject<{
|
|
|
164
164
|
summary: string;
|
|
165
165
|
}>;
|
|
166
166
|
export declare const GuardConfigSchema: z.ZodObject<{
|
|
167
|
-
provider: z.ZodDefault<z.ZodEnum<["gemini", "openai"]>>;
|
|
167
|
+
provider: z.ZodDefault<z.ZodEnum<["gemini", "openai", "claude-cli", "gemini-cli", "kiro-cli", "ollama"]>>;
|
|
168
168
|
model: z.ZodDefault<z.ZodString>;
|
|
169
169
|
apiKey: z.ZodOptional<z.ZodString>;
|
|
170
170
|
baseUrl: z.ZodOptional<z.ZodString>;
|
|
171
171
|
}, "strip", z.ZodTypeAny, {
|
|
172
172
|
model: string;
|
|
173
|
-
provider: "gemini" | "openai";
|
|
173
|
+
provider: "gemini" | "openai" | "claude-cli" | "gemini-cli" | "kiro-cli" | "ollama";
|
|
174
174
|
apiKey?: string | undefined;
|
|
175
175
|
baseUrl?: string | undefined;
|
|
176
176
|
}, {
|
|
177
177
|
apiKey?: string | undefined;
|
|
178
178
|
model?: string | undefined;
|
|
179
|
-
provider?: "gemini" | "openai" | undefined;
|
|
179
|
+
provider?: "gemini" | "openai" | "claude-cli" | "gemini-cli" | "kiro-cli" | "ollama" | undefined;
|
|
180
180
|
baseUrl?: string | undefined;
|
|
181
181
|
}>;
|
|
182
182
|
//# sourceMappingURL=schema.d.ts.map
|
package/dist/guard/schema.js
CHANGED
|
@@ -45,8 +45,8 @@ exports.ReviewResponseSchema = zod_1.z.object({
|
|
|
45
45
|
// Guard config schema (optional .skannr/guard.json)
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
47
47
|
exports.GuardConfigSchema = zod_1.z.object({
|
|
48
|
-
provider: zod_1.z.enum(['gemini', 'openai']).default('
|
|
49
|
-
model: zod_1.z.string().default('
|
|
48
|
+
provider: zod_1.z.enum(['gemini', 'openai', 'claude-cli', 'gemini-cli', 'kiro-cli', 'ollama']).default('auto'),
|
|
49
|
+
model: zod_1.z.string().default(''),
|
|
50
50
|
apiKey: zod_1.z.string().optional(),
|
|
51
51
|
baseUrl: zod_1.z.string().optional(),
|
|
52
52
|
});
|
package/dist/guard/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/guard/schema.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,6BAAwB;AAExB,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAEjE,QAAA,cAAc,GAAG,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAE/D,QAAA,UAAU,GAAG,OAAC,CAAC,MAAM,CAAC;IACjC,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,QAAQ,EAAE,sBAAc;IACxB,OAAO,EAAE,OAAC,CAAC,OAAO,EAAE;IACpB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC5B,CAAC,CAAC;AAEU,QAAA,eAAe,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,KAAK,EAAE,OAAC,CAAC,KAAK,CAAC,kBAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAEjE,QAAA,eAAe,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE;IAClB,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC1C,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACxC,QAAQ,EAAE,sBAAc;IACxB,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,OAAO,EAAE,OAAC,CAAC,OAAO,EAAE;IACpB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAEU,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,UAAU,EAAE,OAAC,CAAC,KAAK,CAAC,uBAAe,CAAC;IACpC,MAAM,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3B,CAAC,CAAC;AAEH,8EAA8E;AAC9E,oDAAoD;AACpD,8EAA8E;AAEjE,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACxC,QAAQ,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/guard/schema.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,6BAAwB;AAExB,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAEjE,QAAA,cAAc,GAAG,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAE/D,QAAA,UAAU,GAAG,OAAC,CAAC,MAAM,CAAC;IACjC,EAAE,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,QAAQ,EAAE,sBAAc;IACxB,OAAO,EAAE,OAAC,CAAC,OAAO,EAAE;IACpB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC5B,CAAC,CAAC;AAEU,QAAA,eAAe,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,KAAK,EAAE,OAAC,CAAC,KAAK,CAAC,kBAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAEjE,QAAA,eAAe,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE;IAClB,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC1C,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACxC,QAAQ,EAAE,sBAAc;IACxB,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,OAAO,EAAE,OAAC,CAAC,OAAO,EAAE;IACpB,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAEU,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,UAAU,EAAE,OAAC,CAAC,KAAK,CAAC,uBAAe,CAAC;IACpC,MAAM,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3B,CAAC,CAAC;AAEH,8EAA8E;AAC9E,oDAAoD;AACpD,8EAA8E;AAEjE,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACxC,QAAQ,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAa,CAAC;IAC/G,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7B,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAC"}
|
package/dist/guard/types.d.ts
CHANGED
|
@@ -50,11 +50,11 @@ export interface GuardResult {
|
|
|
50
50
|
}
|
|
51
51
|
/** Guard configuration (provider, model, etc.). */
|
|
52
52
|
export interface GuardConfig {
|
|
53
|
-
/** LLM provider: 'gemini' | 'openai' */
|
|
54
|
-
provider: 'gemini' | 'openai';
|
|
53
|
+
/** LLM provider: 'gemini' | 'openai' | 'claude-cli' | 'gemini-cli' | 'kiro-cli' | 'ollama' */
|
|
54
|
+
provider: 'gemini' | 'openai' | 'claude-cli' | 'gemini-cli' | 'kiro-cli' | 'ollama';
|
|
55
55
|
/** Model name (e.g. 'gemini-2.0-flash-exp', 'gpt-4o'). */
|
|
56
56
|
model: string;
|
|
57
|
-
/** API key (resolved from env if not set). */
|
|
57
|
+
/** API key (resolved from env if not set). Only needed for 'gemini' and 'openai' providers. */
|
|
58
58
|
apiKey?: string;
|
|
59
59
|
/** OpenAI-compatible base URL (for openai provider). */
|
|
60
60
|
baseUrl?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/guard/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,gDAAgD;AAChD,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE9D,kCAAkC;AAClC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/guard/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,gDAAgD;AAChD,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE9D,kCAAkC;AAClC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,8FAA8F;IAC9F,QAAQ,EAAE,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,CAAC;IACpF,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
package/package.json
CHANGED
package/src/guard/provider.ts
CHANGED
|
@@ -79,6 +79,97 @@ function extractJson(text: string): string {
|
|
|
79
79
|
return text.slice(firstBrace, lastBrace + 1);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Call a CLI-based provider (claude, gemini, kiro).
|
|
84
|
+
* These use the user's existing authenticated session — no API key needed.
|
|
85
|
+
*/
|
|
86
|
+
async function callCli(
|
|
87
|
+
command: string,
|
|
88
|
+
fullPrompt: string,
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
const { execSync } = await import('child_process');
|
|
91
|
+
const fs = await import('fs');
|
|
92
|
+
const path = await import('path');
|
|
93
|
+
const os = await import('os');
|
|
94
|
+
|
|
95
|
+
// Write prompt to a temp file (avoids ARG_MAX limits on large diffs)
|
|
96
|
+
const tmpFile = path.join(os.tmpdir(), `skannr-guard-${Date.now()}.txt`);
|
|
97
|
+
fs.writeFileSync(tmpFile, fullPrompt, 'utf-8');
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
let result: string;
|
|
101
|
+
|
|
102
|
+
switch (command) {
|
|
103
|
+
case 'claude-cli':
|
|
104
|
+
// Claude CLI accepts prompt via stdin pipe with --print flag
|
|
105
|
+
result = execSync(`cat "${tmpFile}" | claude --print`, {
|
|
106
|
+
encoding: 'utf-8',
|
|
107
|
+
timeout: 120_000,
|
|
108
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'gemini-cli':
|
|
113
|
+
// Gemini CLI accepts prompt via -p flag
|
|
114
|
+
result = execSync(`gemini -p "$(cat "${tmpFile}")"`, {
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
timeout: 120_000,
|
|
117
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'kiro-cli':
|
|
122
|
+
// Kiro CLI accepts prompt via stdin in non-interactive mode
|
|
123
|
+
result = execSync(`cat "${tmpFile}" | kiro-cli chat --no-interactive "Review and respond with JSON only."`, {
|
|
124
|
+
encoding: 'utf-8',
|
|
125
|
+
timeout: 120_000,
|
|
126
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
127
|
+
});
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`Unknown CLI provider: ${command}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
} finally {
|
|
136
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Call Ollama local inference.
|
|
142
|
+
*/
|
|
143
|
+
async function callOllama(
|
|
144
|
+
model: string,
|
|
145
|
+
systemPrompt: string,
|
|
146
|
+
userPrompt: string,
|
|
147
|
+
): Promise<string> {
|
|
148
|
+
const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
149
|
+
const url = `${baseUrl}/api/chat`;
|
|
150
|
+
|
|
151
|
+
const response = await fetch(url, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
model: model || 'llama3',
|
|
156
|
+
messages: [
|
|
157
|
+
{ role: 'system', content: systemPrompt },
|
|
158
|
+
{ role: 'user', content: userPrompt },
|
|
159
|
+
],
|
|
160
|
+
stream: false,
|
|
161
|
+
format: 'json',
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const data = await response.json() as any;
|
|
170
|
+
return data.message?.content ?? '';
|
|
171
|
+
}
|
|
172
|
+
|
|
82
173
|
/**
|
|
83
174
|
* Call Gemini API with structured output enforcement.
|
|
84
175
|
*/
|
|
@@ -170,9 +261,24 @@ export async function runLlmReview(
|
|
|
170
261
|
|
|
171
262
|
let rawResponse: string;
|
|
172
263
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
264
|
+
switch (config.provider) {
|
|
265
|
+
case 'openai':
|
|
266
|
+
rawResponse = await callOpenAI(config, systemPrompt, prompt);
|
|
267
|
+
break;
|
|
268
|
+
case 'gemini':
|
|
269
|
+
rawResponse = await callGemini(config, systemPrompt, prompt);
|
|
270
|
+
break;
|
|
271
|
+
case 'ollama':
|
|
272
|
+
rawResponse = await callOllama(config.model, systemPrompt, prompt);
|
|
273
|
+
break;
|
|
274
|
+
case 'claude-cli':
|
|
275
|
+
case 'gemini-cli':
|
|
276
|
+
case 'kiro-cli':
|
|
277
|
+
rawResponse = await callCli(config.provider, systemPrompt + '\n\n' + prompt);
|
|
278
|
+
break;
|
|
279
|
+
default:
|
|
280
|
+
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
281
|
+
}
|
|
176
282
|
} catch (err) {
|
|
177
283
|
throw new Error(
|
|
178
284
|
`Provider call failed (${config.provider}): ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -103,20 +103,75 @@ export function loadGuardConfig(root: string): GuardConfig {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// Env vars override file config
|
|
106
|
-
const
|
|
107
|
-
const model = (process.env.SKANNR_GUARD_MODEL || rawConfig.model || '
|
|
106
|
+
const explicitProvider = process.env.SKANNR_GUARD_PROVIDER || rawConfig.provider as string | undefined;
|
|
107
|
+
const model = (process.env.SKANNR_GUARD_MODEL || rawConfig.model || '') as string;
|
|
108
108
|
const apiKey = process.env.SKANNR_GUARD_API_KEY
|
|
109
109
|
|| process.env.GEMINI_API_KEY
|
|
110
110
|
|| process.env.OPENAI_API_KEY
|
|
111
111
|
|| (rawConfig.apiKey as string | undefined);
|
|
112
112
|
const baseUrl = process.env.OPENAI_BASE_URL || (rawConfig.baseUrl as string | undefined);
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
// Auto-detect provider if not explicitly set
|
|
115
|
+
const provider = explicitProvider || autoDetectProvider(apiKey);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
provider: provider as GuardConfig['provider'],
|
|
119
|
+
model: model || getDefaultModel(provider),
|
|
120
|
+
apiKey,
|
|
121
|
+
baseUrl,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Auto-detect the best available provider.
|
|
127
|
+
* Priority: existing CLI tools (free, already authenticated) > API keys.
|
|
128
|
+
*/
|
|
129
|
+
function autoDetectProvider(apiKey?: string): string {
|
|
130
|
+
const { execSync } = require('child_process');
|
|
131
|
+
|
|
132
|
+
// Check for CLI tools first (no API key needed)
|
|
133
|
+
const cliChecks: Array<[string, string]> = [
|
|
134
|
+
['claude', 'claude-cli'],
|
|
135
|
+
['gemini', 'gemini-cli'],
|
|
136
|
+
['kiro-cli', 'kiro-cli'],
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const [cmd, provider] of cliChecks) {
|
|
140
|
+
try {
|
|
141
|
+
execSync(`${cmd} --version`, { stdio: 'ignore', timeout: 3000 });
|
|
142
|
+
return provider;
|
|
143
|
+
} catch {
|
|
144
|
+
// Not available, try next
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check for Ollama (local, no key needed)
|
|
149
|
+
try {
|
|
150
|
+
execSync('ollama --version', { stdio: 'ignore', timeout: 3000 });
|
|
151
|
+
return 'ollama';
|
|
152
|
+
} catch {
|
|
153
|
+
// Not available
|
|
117
154
|
}
|
|
118
155
|
|
|
119
|
-
|
|
156
|
+
// Fall back to API-based providers if key is available
|
|
157
|
+
if (apiKey) {
|
|
158
|
+
if (process.env.GEMINI_API_KEY) return 'gemini';
|
|
159
|
+
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
160
|
+
return 'gemini'; // default API provider
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Nothing found — default to gemini-cli and let it fail with a clear error
|
|
164
|
+
return 'gemini-cli';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Get the default model for a provider. */
|
|
168
|
+
function getDefaultModel(provider: string): string {
|
|
169
|
+
switch (provider) {
|
|
170
|
+
case 'gemini': return 'gemini-2.0-flash-exp';
|
|
171
|
+
case 'openai': return 'gpt-4o';
|
|
172
|
+
case 'ollama': return 'llama3';
|
|
173
|
+
default: return ''; // CLI providers don't need a model specified
|
|
174
|
+
}
|
|
120
175
|
}
|
|
121
176
|
|
|
122
177
|
/** Find the first existing rules file in the project. */
|
package/src/guard/schema.ts
CHANGED
|
@@ -52,8 +52,8 @@ export const ReviewResponseSchema = z.object({
|
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
|
|
54
54
|
export const GuardConfigSchema = z.object({
|
|
55
|
-
provider: z.enum(['gemini', 'openai']).default('
|
|
56
|
-
model: z.string().default('
|
|
55
|
+
provider: z.enum(['gemini', 'openai', 'claude-cli', 'gemini-cli', 'kiro-cli', 'ollama']).default('auto' as any),
|
|
56
|
+
model: z.string().default(''),
|
|
57
57
|
apiKey: z.string().optional(),
|
|
58
58
|
baseUrl: z.string().optional(),
|
|
59
59
|
});
|
package/src/guard/types.ts
CHANGED
|
@@ -57,11 +57,11 @@ export interface GuardResult {
|
|
|
57
57
|
|
|
58
58
|
/** Guard configuration (provider, model, etc.). */
|
|
59
59
|
export interface GuardConfig {
|
|
60
|
-
/** LLM provider: 'gemini' | 'openai' */
|
|
61
|
-
provider: 'gemini' | 'openai';
|
|
60
|
+
/** LLM provider: 'gemini' | 'openai' | 'claude-cli' | 'gemini-cli' | 'kiro-cli' | 'ollama' */
|
|
61
|
+
provider: 'gemini' | 'openai' | 'claude-cli' | 'gemini-cli' | 'kiro-cli' | 'ollama';
|
|
62
62
|
/** Model name (e.g. 'gemini-2.0-flash-exp', 'gpt-4o'). */
|
|
63
63
|
model: string;
|
|
64
|
-
/** API key (resolved from env if not set). */
|
|
64
|
+
/** API key (resolved from env if not set). Only needed for 'gemini' and 'openai' providers. */
|
|
65
65
|
apiKey?: string;
|
|
66
66
|
/** OpenAI-compatible base URL (for openai provider). */
|
|
67
67
|
baseUrl?: string;
|