fix cross-repo path deps; remove el compiler from neuron-lang/
- neuron-runtime, neuron-store, neuron-migrate: fix path deps (products/engram/ → foundation/engram/engrams/, products/el/ → foundation/el/engrams/) - neuron-rs workspace: fix axon-events/axon-protocol paths (../../../platform/ → ../../platform/, paths were off by one level) - neuron-lang/compiler/ removed (el self-hosting compiler now lives in foundation/el/el-compiler/)
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "neuron-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "neuron"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
rustyline = "14"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
colored = "2"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
@@ -0,0 +1,339 @@
|
||||
// src/main.rs
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use rustyline::DefaultEditor;
|
||||
use serde_json::{json, Value};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "neuron", about = "Neuron intelligence CLI")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
|
||||
/// Operator mode — full tool access (uses NEURON_API_KEY)
|
||||
#[arg(long, global = true)]
|
||||
operator: bool,
|
||||
|
||||
/// User mode — standard access (uses NEURON_USER_KEY)
|
||||
#[arg(long, global = true)]
|
||||
user: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// One-shot ask
|
||||
Ask {
|
||||
question: String,
|
||||
},
|
||||
/// Save a memory
|
||||
Remember {
|
||||
content: String,
|
||||
#[arg(short, long)]
|
||||
importance: Option<String>,
|
||||
},
|
||||
/// Search memories and knowledge
|
||||
Search {
|
||||
query: String,
|
||||
},
|
||||
/// Check server status
|
||||
Status,
|
||||
}
|
||||
|
||||
struct NeuronClient {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
tier: Tier,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
enum Tier {
|
||||
Operator,
|
||||
User,
|
||||
}
|
||||
|
||||
impl NeuronClient {
|
||||
fn new(operator: bool) -> Self {
|
||||
let (api_key, tier) = if operator {
|
||||
(
|
||||
std::env::var("NEURON_API_KEY").unwrap_or_else(|_| "neuron-dev".to_string()),
|
||||
Tier::Operator,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
std::env::var("NEURON_USER_KEY")
|
||||
.or_else(|_| std::env::var("NEURON_API_KEY"))
|
||||
.unwrap_or_else(|_| "neuron-dev".to_string()),
|
||||
Tier::User,
|
||||
)
|
||||
};
|
||||
|
||||
let base_url = std::env::var("NEURON_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:7770".to_string());
|
||||
|
||||
Self {
|
||||
base_url,
|
||||
api_key,
|
||||
tier,
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.tier {
|
||||
Tier::Operator => "neuron[op]> ".cyan().bold().to_string(),
|
||||
Tier::User => "neuron> ".cyan().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn axon_call(&self, tool: &str, params: Value) -> Result<Value, String> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let body = json!({
|
||||
"id": id,
|
||||
"method": "tool_call",
|
||||
"params": {
|
||||
"tool": tool,
|
||||
"params": params
|
||||
}
|
||||
});
|
||||
|
||||
let resp = self.client
|
||||
.post(format!("{}/axon/message", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Connection failed: {}. Is neuron running?", e))?;
|
||||
|
||||
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
return Err("Unauthorized. Check NEURON_API_KEY.".to_string());
|
||||
}
|
||||
|
||||
resp.json::<Value>()
|
||||
.await
|
||||
.map_err(|e| format!("Parse error: {}", e))
|
||||
}
|
||||
|
||||
async fn check_status(&self) -> bool {
|
||||
self.client
|
||||
.get(format!("{}/health", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await
|
||||
.map(|r| r.status().is_success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn remember(&self, content: &str, importance: Option<&str>) -> String {
|
||||
let params = json!({
|
||||
"content": content,
|
||||
"importance": importance.unwrap_or("normal"),
|
||||
"tags": ["cli"]
|
||||
});
|
||||
match self.axon_call("remember", params).await {
|
||||
Ok(v) => {
|
||||
let id = v.get("id")
|
||||
.or_else(|| v.get("result").and_then(|r| r.get("id")))
|
||||
.and_then(|i| i.as_str())
|
||||
.unwrap_or("saved");
|
||||
format!("✓ Remembered ({})", id)
|
||||
}
|
||||
Err(e) => format!("✗ {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: &str) -> String {
|
||||
let params = json!({ "query": query, "limit": 10 });
|
||||
match self.axon_call("search_knowledge", params).await {
|
||||
Ok(v) => format_search_results(&v),
|
||||
Err(_) => {
|
||||
// Try memory search
|
||||
match self.axon_call("recall", json!({ "query": query })).await {
|
||||
Ok(v) => format_search_results(&v),
|
||||
Err(e) => format!("✗ {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn chat(&self, message: &str) -> String {
|
||||
let params = json!({
|
||||
"message": message,
|
||||
"context": "cli"
|
||||
});
|
||||
match self.axon_call("chat", params).await {
|
||||
Ok(v) => extract_text_result(&v),
|
||||
Err(e) => format!("✗ {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_text_result(v: &Value) -> String {
|
||||
// Try various result shapes
|
||||
if let Some(r) = v.get("result") {
|
||||
if let Some(s) = r.as_str() { return s.to_string(); }
|
||||
if let Some(c) = r.get("content").and_then(|c| c.as_str()) { return c.to_string(); }
|
||||
return serde_json::to_string_pretty(r).unwrap_or_default();
|
||||
}
|
||||
if let Some(e) = v.get("error").and_then(|e| e.as_str()) {
|
||||
return format!("error: {}", e);
|
||||
}
|
||||
serde_json::to_string_pretty(v).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn format_search_results(v: &Value) -> String {
|
||||
let items = v.get("result")
|
||||
.or(Some(v))
|
||||
.and_then(|r| r.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
if items.is_empty() {
|
||||
return "No results found.".to_string();
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
for (i, item) in items.iter().take(10).enumerate() {
|
||||
let title = item.get("title").and_then(|t| t.as_str())
|
||||
.or_else(|| item.get("content").and_then(|c| c.as_str()).map(|s| &s[..s.len().min(60)]))
|
||||
.unwrap_or("(untitled)");
|
||||
let id = item.get("id").and_then(|i| i.as_str()).unwrap_or("");
|
||||
out.push_str(&format!("{}. {} [{}]\n", i + 1, title, id));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn history_path() -> PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||
PathBuf::from(home).join(".neuron").join("cli_history")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Operator mode if --operator flag or neither flag (default operator for now)
|
||||
let is_operator = cli.operator || !cli.user;
|
||||
let nc = NeuronClient::new(is_operator);
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Status) => {
|
||||
let ok = nc.check_status().await;
|
||||
if ok {
|
||||
println!("{} neuron-api is running at {}", "●".green(), nc.base_url);
|
||||
} else {
|
||||
println!("{} neuron-api is not responding at {}", "●".red(), nc.base_url);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Some(Commands::Ask { question }) => {
|
||||
let result = nc.chat(&question).await;
|
||||
println!("{}", result);
|
||||
}
|
||||
|
||||
Some(Commands::Remember { content, importance }) => {
|
||||
let result = nc.remember(&content, importance.as_deref()).await;
|
||||
if result.starts_with('✓') {
|
||||
println!("{}", result.green());
|
||||
} else {
|
||||
eprintln!("{}", result.red());
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Some(Commands::Search { query }) => {
|
||||
let result = nc.search(&query).await;
|
||||
println!("{}", result);
|
||||
}
|
||||
|
||||
None => {
|
||||
// Interactive REPL
|
||||
if !nc.check_status().await {
|
||||
eprintln!(
|
||||
"{} Neuron is not running at {}.\nStart it with: neuron-api",
|
||||
"✗".red().bold(),
|
||||
nc.base_url
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} {}",
|
||||
"Neuron".cyan().bold(),
|
||||
"intelligence CLI — type :help for commands, :quit to exit".dimmed()
|
||||
);
|
||||
println!();
|
||||
|
||||
let hist = history_path();
|
||||
let mut rl = DefaultEditor::new().expect("readline init failed");
|
||||
if hist.exists() {
|
||||
let _ = rl.load_history(&hist);
|
||||
}
|
||||
|
||||
let prompt = nc.prompt();
|
||||
|
||||
loop {
|
||||
match rl.readline(&prompt) {
|
||||
Ok(line) => {
|
||||
let input = line.trim().to_string();
|
||||
if input.is_empty() { continue; }
|
||||
let _ = rl.add_history_entry(&input);
|
||||
|
||||
match input.as_str() {
|
||||
":quit" | ":exit" | ":q" => {
|
||||
let _ = rl.save_history(&hist);
|
||||
println!("bye");
|
||||
break;
|
||||
}
|
||||
":help" => {
|
||||
println!("Commands:");
|
||||
println!(" :status — check server status");
|
||||
println!(" :quit — exit");
|
||||
println!(" :help — this message");
|
||||
println!(" remember <text> — save a memory");
|
||||
println!(" search <query> — search knowledge");
|
||||
println!(" anything else — chat");
|
||||
}
|
||||
":status" => {
|
||||
let ok = nc.check_status().await;
|
||||
if ok {
|
||||
println!("{}", "● online".green());
|
||||
} else {
|
||||
println!("{}", "● offline".red());
|
||||
}
|
||||
}
|
||||
_ if input.starts_with("remember ") => {
|
||||
let content = &input["remember ".len()..];
|
||||
let result = nc.remember(content, None).await;
|
||||
println!("{}", if result.starts_with('✓') { result.green() } else { result.red() });
|
||||
}
|
||||
_ if input.starts_with("search ") => {
|
||||
let query = &input["search ".len()..];
|
||||
let result = nc.search(query).await;
|
||||
println!("{}", result);
|
||||
}
|
||||
_ => {
|
||||
let result = nc.chat(&input).await;
|
||||
println!("{}", result.white());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(rustyline::error::ReadlineError::Interrupted) => {
|
||||
println!();
|
||||
continue;
|
||||
}
|
||||
Err(rustyline::error::ReadlineError::Eof) => {
|
||||
let _ = rl.save_history(&hist);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("readline error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user