mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-14 21:50:21 +02:00
feat(3.6.9): OpenCode Zen + Nous Research (Hermes) providers
Add two new model providers, both usable via API key or --subscription (local CLI login, no key): - opencode: OpenCode Zen gateway (OPENCODE_API_KEY, opencode.ai/zen/v1). Subscription mode drives the `opencode` CLI (`opencode run --auto`). Supports the Playwright MCP (--mcp): our .mcp.json is converted to OpenCode's own config schema and injected via OPENCODE_CONFIG. - nous: Nous Research / Hermes models (NOUS_API_KEY, inference-api.nousresearch.com/v1). Subscription mode drives the `hermes` CLI (NousResearch/hermes-agent) on the user's Nous Portal OAuth login (`hermes setup --portal`), via `hermes chat -q`. No CLI-level MCP hook — falls back to Hermes's own built-in toolsets (web/terminal/computer-use). Both wired into cli_binary_for, installed_cli_backends, cli_login_status (prompt passed as argv, not stdin — neither CLI reads stdin for this). Bump version 3.6.8 -> 3.6.9 across Cargo.toml, README, TUTORIAL, setup.sh, install.ps1, and in-binary version strings. README/.env.example updated with the new provider rows and subscription-login table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHFAVCHMvRkTy9Wgw7SayG
This commit is contained in:
co-authored by
Claude Opus 5
parent
1f8ccb6f9e
commit
69c5e3ddb9
@@ -51,6 +51,16 @@ TOGETHER_API_KEY=
|
|||||||
# openrouter: https://openrouter.ai/keys
|
# openrouter: https://openrouter.ai/keys
|
||||||
OPENROUTER_API_KEY=
|
OPENROUTER_API_KEY=
|
||||||
|
|
||||||
|
# opencode: https://opencode.ai/auth (OpenCode Zen gateway)
|
||||||
|
# Or skip the key entirely and use --subscription with the
|
||||||
|
# `opencode` CLI logged into your own Zen/plan account.
|
||||||
|
OPENCODE_API_KEY=
|
||||||
|
|
||||||
|
# nous: Nous Portal (https://portal.nousresearch.com) — Hermes models.
|
||||||
|
# Or skip the key entirely and use --subscription with the
|
||||||
|
# `hermes` CLI (`hermes setup --portal` for OAuth login).
|
||||||
|
NOUS_API_KEY=
|
||||||
|
|
||||||
# ollama: local, no key needed. Override the endpoint if not default:
|
# ollama: local, no key needed. Override the endpoint if not default:
|
||||||
#OLLAMA_BASE_URL=http://localhost:11434/v1
|
#OLLAMA_BASE_URL=http://localhost:11434/v1
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h1 align="center">🧠 NeuroSploit v3.6.8</h1>
|
<h1 align="center">🧠 NeuroSploit v3.6.9</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://trendshift.io/repositories/22624?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-22624" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/22624/daily?language=Python" alt="JoasASantos%2FNeuroSploit | Trendshift" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/22624?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-22624" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/22624/daily?language=Python" alt="JoasASantos%2FNeuroSploit | Trendshift" width="250" height="55"/></a>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/Version-3.6.8-blue?style=flat-square">
|
<img src="https://img.shields.io/badge/Version-3.6.9-blue?style=flat-square">
|
||||||
<img src="https://img.shields.io/badge/Harness-Rust%20%7C%20tokio-e6b673?style=flat-square">
|
<img src="https://img.shields.io/badge/Harness-Rust%20%7C%20tokio-e6b673?style=flat-square">
|
||||||
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square">
|
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square">
|
||||||
<img src="https://img.shields.io/badge/MD%20Agents-435-red?style=flat-square">
|
<img src="https://img.shields.io/badge/MD%20Agents-435-red?style=flat-square">
|
||||||
@@ -433,6 +433,8 @@ export GROQ_API_KEY=... # groq:*
|
|||||||
export TOGETHER_API_KEY=... # together:*
|
export TOGETHER_API_KEY=... # together:*
|
||||||
export MOONSHOT_API_KEY=... # moonshot:* (Kimi K3/K2)
|
export MOONSHOT_API_KEY=... # moonshot:* (Kimi K3/K2)
|
||||||
export OPENROUTER_API_KEY=... # openrouter:*
|
export OPENROUTER_API_KEY=... # openrouter:*
|
||||||
|
export OPENCODE_API_KEY=... # opencode:* (OpenCode Zen gateway)
|
||||||
|
export NOUS_API_KEY=... # nous:* (Nous Portal — Hermes)
|
||||||
# ollama / llamacpp need no key (local)
|
# ollama / llamacpp need no key (local)
|
||||||
|
|
||||||
# then run via API (note: NO --subscription)
|
# then run via API (note: NO --subscription)
|
||||||
@@ -462,6 +464,8 @@ Or put the keys in a `.env` and source it (`cp .env.example .env`; edit; `set -a
|
|||||||
| `together:` | `TOGETHER_API_KEY` | api.together.xyz |
|
| `together:` | `TOGETHER_API_KEY` | api.together.xyz |
|
||||||
| `moonshot:` | `MOONSHOT_API_KEY` | api.moonshot.ai |
|
| `moonshot:` | `MOONSHOT_API_KEY` | api.moonshot.ai |
|
||||||
| `openrouter:` | `OPENROUTER_API_KEY` | openrouter.ai |
|
| `openrouter:` | `OPENROUTER_API_KEY` | openrouter.ai |
|
||||||
|
| `opencode:` | `OPENCODE_API_KEY` | opencode.ai/zen (OpenCode Zen gateway) |
|
||||||
|
| `nous:` | `NOUS_API_KEY` | inference-api.nousresearch.com (Hermes 4) |
|
||||||
| `ollama:` | _(none)_ | localhost:11434 |
|
| `ollama:` | _(none)_ | localhost:11434 |
|
||||||
| `llamacpp:` | _(none)_ | localhost:8080 |
|
| `llamacpp:` | _(none)_ | localhost:8080 |
|
||||||
|
|
||||||
@@ -484,6 +488,12 @@ install and log into one of the CLIs first:
|
|||||||
| `openai:` | `codex` | `codex` login |
|
| `openai:` | `codex` | `codex` login |
|
||||||
| `gemini:` | `gemini` | `gemini` login |
|
| `gemini:` | `gemini` | `gemini` login |
|
||||||
| `xai:` | `grok` | `grok` login |
|
| `xai:` | `grok` | `grok` login |
|
||||||
|
| `opencode:` | `opencode` | `opencode auth login` (or `/connect` in the TUI) — Zen/plan account |
|
||||||
|
| `nous:` | `hermes` | `hermes setup --portal` — Nous Portal OAuth |
|
||||||
|
|
||||||
|
`opencode:` also gets the Playwright MCP (`--mcp`) like anthropic/openai do.
|
||||||
|
`nous:` relies on Hermes's own built-in toolsets (web/terminal/computer-use)
|
||||||
|
instead — it has no CLI-level MCP hook.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./target/release/neurosploit run http://testphp.vulnweb.com/ \
|
./target/release/neurosploit run http://testphp.vulnweb.com/ \
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# NeuroSploit — Tutorial & User Guide (v3.6.8)
|
# NeuroSploit — Tutorial & User Guide (v3.6.9)
|
||||||
|
|
||||||
A complete, hands-on guide to installing, configuring and running NeuroSploit —
|
A complete, hands-on guide to installing, configuring and running NeuroSploit —
|
||||||
the autonomous, multi-model penetration-testing harness.
|
the autonomous, multi-model penetration-testing harness.
|
||||||
@@ -98,7 +98,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith
|
|||||||
### Verify
|
### Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
neurosploit --version # neurosploit 3.6.8
|
neurosploit --version # neurosploit 3.6.9
|
||||||
neurosploit agents # {"vulns":241,...,"ai":30,...,"total":430}
|
neurosploit agents # {"vulns":241,...,"ai":30,...,"total":430}
|
||||||
neurosploit models # all providers & models
|
neurosploit models # all providers & models
|
||||||
```
|
```
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ function Ok ($m) { Write-Host " + $m" -ForegroundColor Green }
|
|||||||
function Warn($m){ Write-Host " ! $m" -ForegroundColor Yellow }
|
function Warn($m){ Write-Host " ! $m" -ForegroundColor Yellow }
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " NeuroSploit installer (Windows) — v3.6.8" -ForegroundColor Cyan
|
Write-Host " NeuroSploit installer (Windows) — v3.6.9" -ForegroundColor Cyan
|
||||||
|
|
||||||
# arch → asset arch (only x64 prebuilt today; arm64 falls back to source)
|
# arch → asset arch (only x64 prebuilt today; arm64 falls back to source)
|
||||||
$rawArch = $env:PROCESSOR_ARCHITECTURE
|
$rawArch = $env:PROCESSOR_ARCHITECTURE
|
||||||
@@ -29,7 +29,7 @@ $ref = $env:NEUROSPLOIT_REF
|
|||||||
if (-not $ref) {
|
if (-not $ref) {
|
||||||
try { $ref = (Invoke-RestMethod "https://api.github.com/repos/$slug/releases/latest").tag_name } catch { }
|
try { $ref = (Invoke-RestMethod "https://api.github.com/repos/$slug/releases/latest").tag_name } catch { }
|
||||||
}
|
}
|
||||||
if (-not $ref) { $ref = "v3.6.8" }
|
if (-not $ref) { $ref = "v3.6.9" }
|
||||||
Say "Release: $ref"
|
Say "Release: $ref"
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||||
|
|||||||
Generated
+2
-2
@@ -871,7 +871,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "neurosploit"
|
name = "neurosploit"
|
||||||
version = "3.6.8"
|
version = "3.6.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -888,7 +888,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "neurosploit-harness"
|
name = "neurosploit-harness"
|
||||||
version = "3.6.8"
|
version = "3.6.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures",
|
"futures",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "3.6.8"
|
version = "3.6.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/JoasASantos/NeuroSploit"
|
repository = "https://github.com/JoasASantos/NeuroSploit"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! NeuroSploit v3.6.8 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
|
//! NeuroSploit v3.6.9 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
|
||||||
|
|
||||||
mod repl;
|
mod repl;
|
||||||
mod tui;
|
mod tui;
|
||||||
@@ -11,9 +11,9 @@ use std::path::{Path, PathBuf};
|
|||||||
#[command(
|
#[command(
|
||||||
name = "neurosploit",
|
name = "neurosploit",
|
||||||
version,
|
version,
|
||||||
about = "NeuroSploit v3.6.8 — multi-model autonomous pentest harness",
|
about = "NeuroSploit v3.6.9 — multi-model autonomous pentest harness",
|
||||||
long_about = "NeuroSploit v3.6.8 — a Rust multi-model harness that drives a pool of LLMs \
|
long_about = "NeuroSploit v3.6.9 — a Rust multi-model harness that drives a pool of LLMs \
|
||||||
(API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \
|
(API key or local subscription: Claude/Codex/Gemini/Grok/OpenCode/Hermes) to autonomously test a target. \
|
||||||
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
|
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
|
||||||
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
|
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
|
||||||
Run with NO arguments for an interactive wizard.\n\n\
|
Run with NO arguments for an interactive wizard.\n\n\
|
||||||
@@ -54,7 +54,7 @@ enum Cmd {
|
|||||||
recon: usize,
|
recon: usize,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
offline: bool,
|
offline: bool,
|
||||||
/// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok login).
|
/// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok/OpenCode/Hermes login).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
subscription: bool,
|
subscription: bool,
|
||||||
/// Enable Playwright MCP (auto-installed if missing; backends that don't
|
/// Enable Playwright MCP (auto-installed if missing; backends that don't
|
||||||
@@ -765,7 +765,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
|
|||||||
println!(" │ ua : {ua}");
|
println!(" │ ua : {ua}");
|
||||||
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
|
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
|
||||||
|
|
||||||
println!(" ┌─ NeuroSploit v3.6.8 · by Joas A Santos & Red Team Leaders");
|
println!(" ┌─ NeuroSploit v3.6.9 · by Joas A Santos & Red Team Leaders");
|
||||||
println!(" │ run id : {run_id}");
|
println!(" │ run id : {run_id}");
|
||||||
println!(" │ target : {}", cfg.target);
|
println!(" │ target : {}", cfg.target);
|
||||||
println!(" │ models : {}", cfg.models.join(", "));
|
println!(" │ models : {}", cfg.models.join(", "));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! NeuroSploit v3.6.8 — interactive session (Claude-Code / Codex / Cursor-CLI style).
|
//! NeuroSploit v3.6.9 — interactive session (Claude-Code / Codex / Cursor-CLI style).
|
||||||
//!
|
//!
|
||||||
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL with
|
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL with
|
||||||
//! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model
|
//! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model
|
||||||
@@ -371,7 +371,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
|||||||
let backends = harness::installed_cli_backends();
|
let backends = harness::installed_cli_backends();
|
||||||
println!("\x1b[1m");
|
println!("\x1b[1m");
|
||||||
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
|
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
|
||||||
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.8");
|
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.9");
|
||||||
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
|
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
|
||||||
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
|
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
|
||||||
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
|
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! NeuroSploit v3.6.8 — TUI "Mission Control" mode.
|
//! NeuroSploit v3.6.9 — TUI "Mission Control" mode.
|
||||||
//!
|
//!
|
||||||
//! Concurrent panels that update live while the engagement runs in the
|
//! Concurrent panels that update live while the engagement runs in the
|
||||||
//! background, with a composer input that stays active during execution:
|
//! background, with a composer input that stays active during execution:
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ pub fn providers() -> Vec<Provider> {
|
|||||||
models: vec!["gpt-4o", "claude-3-7-sonnet", "gemini/gemini-2.5-pro"] },
|
models: vec!["gpt-4o", "claude-3-7-sonnet", "gemini/gemini-2.5-pro"] },
|
||||||
Provider { key: "openrouter", label: "OpenRouter", base_url: "https://openrouter.ai/api/v1", env_key: "OPENROUTER_API_KEY", kind: "api",
|
Provider { key: "openrouter", label: "OpenRouter", base_url: "https://openrouter.ai/api/v1", env_key: "OPENROUTER_API_KEY", kind: "api",
|
||||||
models: vec!["anthropic/claude-opus-4-8", "qwen/qwen-2.5-coder-32b-instruct", "deepseek/deepseek-r1", "meta-llama/llama-3.3-70b-instruct"] },
|
models: vec!["anthropic/claude-opus-4-8", "qwen/qwen-2.5-coder-32b-instruct", "deepseek/deepseek-r1", "meta-llama/llama-3.3-70b-instruct"] },
|
||||||
|
// OpenCode Zen — the curated OpenAI-compatible gateway behind the
|
||||||
|
// `opencode` CLI (https://opencode.ai/zen). Works two ways, like
|
||||||
|
// anthropic/openai/xai/gemini above: as a plain API-key provider here,
|
||||||
|
// or (with --subscription) driven through the locally-installed
|
||||||
|
// `opencode` agentic CLI on the user's own Zen/plan login — no key
|
||||||
|
// needed in that mode. `kind: "cli"` reflects the latter.
|
||||||
|
Provider { key: "opencode", label: "OpenCode Zen", base_url: "https://opencode.ai/zen/v1", env_key: "OPENCODE_API_KEY", kind: "cli",
|
||||||
|
models: vec!["claude-opus-5", "claude-sonnet-5", "gpt-5.6-sol", "gpt-5.5", "gemini-3-pro", "grok-4.5", "deepseek-v4-pro", "qwen3.7-max", "kimi-k3"] },
|
||||||
|
// Nous Research — Hermes models via the Nous Portal. As an API-key
|
||||||
|
// provider here (OpenAI-compatible `inference-api.nousresearch.com`),
|
||||||
|
// or (with --subscription) driven through the `hermes` CLI
|
||||||
|
// (NousResearch/hermes-agent) on the user's OAuth Portal login
|
||||||
|
// (`hermes setup --portal`) — 300+ routed frontier models, no key.
|
||||||
|
Provider { key: "nous", label: "Nous Research (Hermes)", base_url: "https://inference-api.nousresearch.com/v1", env_key: "NOUS_API_KEY", kind: "cli",
|
||||||
|
models: vec!["Hermes-4-405B", "Hermes-4-70B", "DeepHermes-3-Mistral-24B-Preview"] },
|
||||||
// Azure OpenAI (OpenAI-compatible). Set AZURE_OPENAI_ENDPOINT (e.g.
|
// Azure OpenAI (OpenAI-compatible). Set AZURE_OPENAI_ENDPOINT (e.g.
|
||||||
// https://<resource>.openai.azure.com), optionally AZURE_OPENAI_API_VERSION
|
// https://<resource>.openai.azure.com), optionally AZURE_OPENAI_API_VERSION
|
||||||
// (default 2024-10-21), and use `azure:<your-deployment-name>` as the model.
|
// (default 2024-10-21), and use `azure:<your-deployment-name>` as the model.
|
||||||
@@ -226,6 +241,11 @@ impl ChatClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut cmd = Command::new(bin);
|
let mut cmd = Command::new(bin);
|
||||||
|
// Most agentic CLIs here take the prompt on stdin; opencode and hermes
|
||||||
|
// take it as a trailing positional argument instead — track which so we
|
||||||
|
// don't also pipe it into stdin below (that would just hang the child
|
||||||
|
// waiting on a request it already got as an argv value).
|
||||||
|
let mut prompt_via_stdin = true;
|
||||||
match bin {
|
match bin {
|
||||||
// Codex non-interactive exec (uses the ChatGPT/Codex login), prompt on stdin.
|
// Codex non-interactive exec (uses the ChatGPT/Codex login), prompt on stdin.
|
||||||
"codex" => {
|
"codex" => {
|
||||||
@@ -250,13 +270,46 @@ impl ChatClient {
|
|||||||
"grok" => {
|
"grok" => {
|
||||||
cmd.arg("--model").arg(model);
|
cmd.arg("--model").arg(model);
|
||||||
}
|
}
|
||||||
|
// OpenCode CLI (`opencode run`) — non-interactive one-shot, prompt
|
||||||
|
// as a positional arg, not stdin. `--auto` auto-approves anything
|
||||||
|
// not explicitly denied (our equivalent of --dangerously-skip-permissions).
|
||||||
|
// MCP (Playwright) is injected via a generated opencode.json pointed
|
||||||
|
// at through OPENCODE_CONFIG rather than a CLI flag (opencode has none).
|
||||||
|
"opencode" => {
|
||||||
|
prompt_via_stdin = false;
|
||||||
|
cmd.arg("run").arg("--model").arg(model).arg("--auto");
|
||||||
|
if let Some(mcp) = mcp_config {
|
||||||
|
match write_opencode_mcp_config(mcp) {
|
||||||
|
Ok(cfg) => { cmd.env("OPENCODE_CONFIG", cfg); }
|
||||||
|
Err(e) => eprintln!(" [!] opencode MCP config failed: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.arg(&prompt);
|
||||||
|
}
|
||||||
|
// Hermes Agent CLI (NousResearch/hermes-agent) — single-query mode.
|
||||||
|
// `-q` is the prompt-supplying flag (not a stdin read); `--provider
|
||||||
|
// nous` pins the Nous Portal OAuth login; `-Q` quiets banner/spinner
|
||||||
|
// for programmatic use; `--yolo` bypasses dangerous-command prompts.
|
||||||
|
// No CLI-level MCP hook — Hermes falls back to its own built-in
|
||||||
|
// toolsets (web/terminal/computer-use) rather than our Playwright MCP.
|
||||||
|
"hermes" => {
|
||||||
|
prompt_via_stdin = false;
|
||||||
|
cmd.arg("chat").arg("-m").arg(model).arg("--provider").arg("nous")
|
||||||
|
.arg("-Q").arg("--yolo").arg("-q").arg(&prompt);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
|
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
|
||||||
let mut child = cmd.spawn().map_err(|e| anyhow!("spawn {} failed: {}", bin, e))?;
|
let mut child = cmd.spawn().map_err(|e| anyhow!("spawn {} failed: {}", bin, e))?;
|
||||||
if let Some(mut stdin) = child.stdin.take() {
|
if prompt_via_stdin {
|
||||||
stdin.write_all(prompt.as_bytes()).await?;
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
// Drop closes stdin so the CLI processes the prompt and exits.
|
stdin.write_all(prompt.as_bytes()).await?;
|
||||||
|
// Drop closes stdin so the CLI processes the prompt and exits.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Prompt went in as an argv value; close stdin immediately so
|
||||||
|
// nothing lingers waiting on it (opencode/hermes never read it).
|
||||||
|
drop(child.stdin.take());
|
||||||
}
|
}
|
||||||
// Cap a single agentic CLI turn so a stuck tool-loop can't hang the run.
|
// Cap a single agentic CLI turn so a stuck tool-loop can't hang the run.
|
||||||
let out = match tokio::time::timeout(Duration::from_secs(600), child.wait_with_output()).await {
|
let out = match tokio::time::timeout(Duration::from_secs(600), child.wait_with_output()).await {
|
||||||
@@ -577,6 +630,8 @@ pub fn cli_binary_for(provider: &str) -> Option<&'static str> {
|
|||||||
"openai" => Some("codex"),
|
"openai" => Some("codex"),
|
||||||
"xai" => Some("grok"),
|
"xai" => Some("grok"),
|
||||||
"gemini" => Some("gemini"),
|
"gemini" => Some("gemini"),
|
||||||
|
"opencode" => Some("opencode"),
|
||||||
|
"nous" => Some("hermes"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -590,7 +645,7 @@ pub fn binary_in_path(name: &str) -> bool {
|
|||||||
|
|
||||||
/// Which subscription CLI backends are installed locally.
|
/// Which subscription CLI backends are installed locally.
|
||||||
pub fn installed_cli_backends() -> Vec<&'static str> {
|
pub fn installed_cli_backends() -> Vec<&'static str> {
|
||||||
["claude", "codex", "grok", "gemini"].into_iter().filter(|b| binary_in_path(b)).collect()
|
["claude", "codex", "grok", "gemini", "opencode", "hermes"].into_iter().filter(|b| binary_in_path(b)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Login state of a subscription CLI backend.
|
/// Login state of a subscription CLI backend.
|
||||||
@@ -610,15 +665,23 @@ pub async fn cli_login_status(provider: &str) -> LoginStatus {
|
|||||||
let Some(bin) = cli_binary_for(provider) else { return LoginStatus::NotInstalled };
|
let Some(bin) = cli_binary_for(provider) else { return LoginStatus::NotInstalled };
|
||||||
if !binary_in_path(bin) { return LoginStatus::NotInstalled; }
|
if !binary_in_path(bin) { return LoginStatus::NotInstalled; }
|
||||||
let mut cmd = Command::new(bin);
|
let mut cmd = Command::new(bin);
|
||||||
|
// opencode/hermes take the probe prompt as an argv value, not stdin.
|
||||||
|
let prompt_via_stdin = !matches!(bin, "opencode" | "hermes");
|
||||||
match bin {
|
match bin {
|
||||||
"claude" => { cmd.arg("-p").arg("--output-format").arg("text").arg("--dangerously-skip-permissions"); }
|
"claude" => { cmd.arg("-p").arg("--output-format").arg("text").arg("--dangerously-skip-permissions"); }
|
||||||
"codex" => { cmd.arg("exec").arg("--dangerously-bypass-approvals-and-sandbox").arg("-"); }
|
"codex" => { cmd.arg("exec").arg("--dangerously-bypass-approvals-and-sandbox").arg("-"); }
|
||||||
|
"opencode" => { cmd.arg("run").arg("--auto").arg("Reply with exactly: OK"); }
|
||||||
|
"hermes" => { cmd.arg("chat").arg("--provider").arg("nous").arg("-Q").arg("--yolo").arg("-q").arg("Reply with exactly: OK"); }
|
||||||
_ => { cmd.arg("-p"); } // grok / gemini: prompt on stdin
|
_ => { cmd.arg("-p"); } // grok / gemini: prompt on stdin
|
||||||
}
|
}
|
||||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
|
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
|
||||||
let mut child = match cmd.spawn() { Ok(c) => c, Err(_) => return LoginStatus::Unknown };
|
let mut child = match cmd.spawn() { Ok(c) => c, Err(_) => return LoginStatus::Unknown };
|
||||||
if let Some(mut stdin) = child.stdin.take() {
|
if prompt_via_stdin {
|
||||||
let _ = stdin.write_all(b"Reply with exactly: OK").await;
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
let _ = stdin.write_all(b"Reply with exactly: OK").await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
drop(child.stdin.take());
|
||||||
}
|
}
|
||||||
let out = match tokio::time::timeout(Duration::from_secs(45), child.wait_with_output()).await {
|
let out = match tokio::time::timeout(Duration::from_secs(45), child.wait_with_output()).await {
|
||||||
Ok(Ok(o)) => o,
|
Ok(Ok(o)) => o,
|
||||||
@@ -642,10 +705,50 @@ pub async fn cli_login_status(provider: &str) -> LoginStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Does this provider's agentic CLI accept a Playwright MCP config?
|
/// Does this provider's agentic CLI accept a Playwright MCP config?
|
||||||
/// Claude Code and Codex do; Gemini/Grok CLIs don't take an MCP-config flag, so
|
/// Claude Code, Codex, and OpenCode do (OpenCode via a generated
|
||||||
/// they fall back to their own built-in tools.
|
/// `opencode.json` + `OPENCODE_CONFIG`, see `write_opencode_mcp_config`).
|
||||||
|
/// Gemini/Grok/Hermes have no CLI-level MCP hook, so they fall back to their
|
||||||
|
/// own built-in tools (Hermes ships web/terminal/computer-use natively).
|
||||||
pub fn mcp_supported(provider: &str) -> bool {
|
pub fn mcp_supported(provider: &str) -> bool {
|
||||||
matches!(provider, "anthropic" | "openai")
|
matches!(provider, "anthropic" | "openai" | "opencode")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert our `.mcp.json` (`{"mcpServers": {name: {command, args}}}`) into
|
||||||
|
/// OpenCode's own config schema (`{"mcp": {name: {"type":"local","command":
|
||||||
|
/// [command, ...args], "enabled": true}}}`) and write it next to the source
|
||||||
|
/// file. OpenCode has no `--mcp-config` flag; it's pointed at a config file
|
||||||
|
/// via the `OPENCODE_CONFIG` env var instead (set by the `opencode` arm of
|
||||||
|
/// `chat_cli`), so this doesn't touch the user's own `opencode.json`.
|
||||||
|
fn write_opencode_mcp_config(mcp_json_path: &str) -> Result<std::path::PathBuf> {
|
||||||
|
let txt = std::fs::read_to_string(mcp_json_path)
|
||||||
|
.map_err(|e| anyhow!("read {mcp_json_path}: {e}"))?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&txt)
|
||||||
|
.map_err(|e| anyhow!("parse {mcp_json_path}: {e}"))?;
|
||||||
|
let servers = v.get("mcpServers").cloned().unwrap_or(v);
|
||||||
|
let mut mcp = serde_json::Map::new();
|
||||||
|
if let Some(obj) = servers.as_object() {
|
||||||
|
for (name, s) in obj {
|
||||||
|
let command = s.get("command").and_then(|c| c.as_str()).unwrap_or("").to_string();
|
||||||
|
if command.is_empty() { continue; }
|
||||||
|
let mut argv = vec![serde_json::Value::String(command)];
|
||||||
|
if let Some(args) = s.get("args").and_then(|a| a.as_array()) {
|
||||||
|
argv.extend(args.iter().cloned());
|
||||||
|
}
|
||||||
|
mcp.insert(name.clone(), serde_json::json!({
|
||||||
|
"type": "local",
|
||||||
|
"command": argv,
|
||||||
|
"enabled": true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cfg = serde_json::json!({
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": mcp
|
||||||
|
});
|
||||||
|
let path = std::path::Path::new(mcp_json_path).with_file_name("opencode.json");
|
||||||
|
std::fs::write(&path, serde_json::to_string_pretty(&cfg).unwrap_or_default())
|
||||||
|
.map_err(|e| anyhow!("write {}: {e}", path.display()))?;
|
||||||
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Best-effort ensure the Playwright MCP server is available locally. Requires
|
/// Best-effort ensure the Playwright MCP server is available locally. Requires
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ cat <<'BANNER'
|
|||||||
|
|
||||||
███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗
|
███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗
|
||||||
████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit installer
|
████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit installer
|
||||||
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v3.6.8 — Rust harness
|
██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ v3.6.9 — Rust harness
|
||||||
██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos
|
██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos
|
||||||
██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders
|
██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders
|
||||||
╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝
|
╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝
|
||||||
@@ -63,7 +63,7 @@ if [ -z "$REF" ]; then
|
|||||||
REF="$(dl "https://api.github.com/repos/${REPO_SLUG}/releases/latest" /dev/stdout 2>/dev/null \
|
REF="$(dl "https://api.github.com/repos/${REPO_SLUG}/releases/latest" /dev/stdout 2>/dev/null \
|
||||||
| grep -m1 '"tag_name"' | sed -E 's/.*"tag_name" *: *"([^"]+)".*/\1/' || true)"
|
| grep -m1 '"tag_name"' | sed -E 's/.*"tag_name" *: *"([^"]+)".*/\1/' || true)"
|
||||||
fi
|
fi
|
||||||
[ -z "$REF" ] && REF="v3.6.8"
|
[ -z "$REF" ] && REF="v3.6.9"
|
||||||
say "Release: $REF"
|
say "Release: $REF"
|
||||||
|
|
||||||
installed=0
|
installed=0
|
||||||
|
|||||||
Reference in New Issue
Block a user