Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,6 @@ credentials.json

# LangGraph API server
.langgraph_api/

# Reports
report*
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ export SKILLSPECTOR_PROVIDER=nv_build
export NVIDIA_INFERENCE_KEY=nvapi-...
skillspector scan ./my-skill/

# Gemini (via OpenAI compatibility layer)
export SKILLSPECTOR_PROVIDER=openai
export OPENAI_API_KEY="YOUR_GEMINI_API_KEY"
export OPENAI_BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai/"
export SKILLSPECTOR_MODEL=gemini-3.5-flash
skillspector scan ./my-skill/

# Local Ollama or any OpenAI-compatible endpoint
export SKILLSPECTOR_PROVIDER=openai
export OPENAI_API_KEY=ollama
Expand Down
4 changes: 4 additions & 0 deletions model_registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ models:
"openai/openai/gpt-5.3-chat":
context_length: 128000
max_output_tokens: 16384

"gemini-3.5-flash":
context_length: 1048576
max_output_tokens: 8192
84 changes: 83 additions & 1 deletion src/skillspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,89 @@ def scan(
"version": __version__,
},
}
result = graph.invoke(state, config=trace_config)
if verbose:
result = graph.invoke(state, config=trace_config)
else:
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
from rich.console import Console
from skillspector.nodes.analyzers import ANALYZER_NODE_IDS
import warnings

# Suppress noisy Pydantic serialization warnings during structured LLM output
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")

total_steps = 4 + len(ANALYZER_NODE_IDS)
result = dict(state)

# Use stderr for progress so stdout remains clean for structured outputs
err_console = Console(stderr=True)

with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeElapsedColumn(),
console=err_console,
transient=True,
) as progress:
task_id = progress.add_task("Resolving input...", total=total_steps)

num_files = 0
analyzers_done = 0
total_analyzers = len(ANALYZER_NODE_IDS)

for update in graph.stream(state, config=trace_config, stream_mode="updates"):
for node_name, node_output in update.items():
progress.advance(task_id)

# Accumulate scalar outputs needed by the CLI (report_body, risk_score, temp_dir, sarif_report)
if "temp_dir_for_cleanup" in node_output:
result["temp_dir_for_cleanup"] = node_output["temp_dir_for_cleanup"]
if "report_body" in node_output:
result["report_body"] = node_output["report_body"]
if "sarif_report" in node_output:
result["sarif_report"] = node_output["sarif_report"]
if "risk_score" in node_output:
result["risk_score"] = node_output["risk_score"]

# Update UI text based on graph progression
if node_name == "resolve_input":
progress.update(task_id, description="Building context...")
elif node_name == "build_context":
components = node_output.get("components", [])
num_files = len(components)
progress.update(task_id, description=f"Analyzing {num_files} files (0/{total_analyzers} rules applied)...")

# Print a proper report of the files and directories being scanned
from rich.tree import Tree
from pathlib import Path

tree = Tree("[bold blue]Discovered Files to Scan[/bold blue]")
nodes = {"": tree}
for path in sorted(components):
parts = Path(path).parts
current = ""
for part in parts:
parent = current
current = f"{current}/{part}" if current else part
if current not in nodes:
is_file = current == path
icon = "📄 " if is_file else "📁 "
style = "green" if is_file else "cyan"
nodes[current] = nodes[parent].add(f"[{style}]{icon}{part}[/{style}]")

err_console.print(tree)
err_console.print()

elif node_name in ANALYZER_NODE_IDS:
analyzers_done += 1
progress.update(task_id, description=f"Analyzing {num_files} files ({analyzers_done}/{total_analyzers} rules applied)...")
# Print which rule just finished above the progress bar
err_console.print(f"[dim]✔ Rule completed: {node_name}[/dim]")
elif node_name == "meta_analyzer":
progress.update(task_id, description="Generating report...")
err_console.print("[dim]✔ Rule completed: meta_analyzer (filtering findings)[/dim]")

_write_result(result, output, format)

Expand Down
4 changes: 2 additions & 2 deletions src/skillspector/nodes/analyzers/mcp_tool_poisoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,8 +799,8 @@ def _check_tp4(state: SkillspectorState) -> list[Finding]:
)
]

except Exception:
logger.warning("%s: TP4 LLM check failed, skipping", ANALYZER_ID, exc_info=True)
except Exception as exc:
logger.warning("%s: TP4 LLM check failed, skipping: %s", ANALYZER_ID, exc)
return []


Expand Down
4 changes: 4 additions & 0 deletions src/skillspector/providers/nv_build/model_registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ models:
"openai/gpt-oss-120b":
context_length: 128000
max_output_tokens: 16384

"gemini-3.5-flash":
context_length: 1048576
max_output_tokens: 8192
4 changes: 4 additions & 0 deletions src/skillspector/providers/openai/model_registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ models:
"gpt-5.4":
context_length: 1000000
max_output_tokens: 128000

"gemini-3.5-flash":
context_length: 1048576
max_output_tokens: 8192