From b7738b9a74284d92061c7d133f72102545a62ad0 Mon Sep 17 00:00:00 2001 From: Quorafind Date: Sat, 3 Oct 2026 21:37:36 +0800 Subject: [PATCH 1/3] feat(group): Admit eight more external agents to groups Name pi, ZCode, Gemini CLI, Qwen Code, Kimi CLI, Amp, Cursor Agent and Crush as known agent kinds, listed once in AgentKind::KNOWN. Detect the native session of pi, Gemini CLI and Qwen Code from their session variables, and Qwen Code's from its qwen-code/invocation call metadata. REBON_GROUP_AGENT limits detection to the configured host, so a CLI nested under another agent does not inherit the launcher's identity. Gemini CLI reports its hook events as BeforeAgent and AfterTool, so context is delivered on those for it alone. Before such a session has joined a group, its SessionStart hook tells it its agent name and session id so group_join can bind the right member. --- crates/rebon-group/src/agents.rs | 21 ++++ crates/rebon-group/src/deliver.rs | 127 ++++++++++++++++++- crates/rebon-group/src/identity.rs | 169 ++++++++++++++++++++++++-- crates/rebon-group/src/tools_tests.rs | 58 +++++++++ 4 files changed, 364 insertions(+), 11 deletions(-) diff --git a/crates/rebon-group/src/agents.rs b/crates/rebon-group/src/agents.rs index 1c0c7db..d30b888 100644 --- a/crates/rebon-group/src/agents.rs +++ b/crates/rebon-group/src/agents.rs @@ -151,6 +151,27 @@ mod tests { assert_eq!(store.load(&group.id).unwrap().members.len(), 2); } + #[test] + fn every_known_agent_can_be_admitted_excluded_and_reenabled() { + let dir = tempfile::tempdir().unwrap(); + let store = GroupStore::new(dir.path()); + let group = store.create("clients", "/work/app").unwrap(); + for &agent in AgentKind::KNOWN { + store.set_agent_allowed(agent, false).unwrap(); + assert!( + store.join(&group.id, member(agent, "s1")).is_err(), + "{agent}" + ); + assert!(store.load(&group.id).unwrap().members.is_empty()); + store.set_agent_allowed(agent, true).unwrap(); + let joining = member(agent, "s1"); + store.join(&group.id, joining.clone()).unwrap(); + assert_eq!(store.load(&group.id).unwrap().members[0].agent, agent); + store.leave(&group.id, &joining.key()).unwrap(); + } + assert_eq!(store.agent_policy().unwrap(), AgentPolicy::default()); + } + #[test] fn switching_out_leaves_existing_members_where_they_are() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/rebon-group/src/deliver.rs b/crates/rebon-group/src/deliver.rs index ac50966..3bd7ad1 100644 --- a/crates/rebon-group/src/deliver.rs +++ b/crates/rebon-group/src/deliver.rs @@ -157,6 +157,7 @@ fn until_first_wake(mut pending: Pending) -> Pending { /// The hook events that can carry context into the model. const CONTEXT_EVENTS: &[&str] = &["UserPromptSubmit", "SessionStart", "PostToolUse"]; +const GEMINI_CONTEXT_EVENTS: &[&str] = &["BeforeAgent", "SessionStart", "AfterTool"]; /// `SessionStart` sources after which the context no longer holds what /// the group told it. @@ -164,11 +165,16 @@ const FRESH_SOURCES: &[&str] = &["clear", "compact", "resume"]; /// What `rebon group hook --agent ` prints for one hook `input` (the /// JSON the agent passes on stdin), or `None` to print nothing. The shape is -/// the one both Claude Code and Codex read: +/// the one Claude Code, Codex, Qwen Code, ZCode and Gemini CLI read: /// `{"hookSpecificOutput": {"hookEventName": …, "additionalContext": …}}`. pub fn hook_output(store: &GroupStore, agent: &str, input: &Value) -> Option { let event = input.get("hook_event_name").and_then(Value::as_str)?; - if !CONTEXT_EVENTS.contains(&event) { + let events = if agent == crate::AgentKind::GEMINI_CLI { + GEMINI_CONTEXT_EVENTS + } else { + CONTEXT_EVENTS + }; + if !events.contains(&event) { return None; } let session_id = input @@ -180,6 +186,26 @@ pub fn hook_output(store: &GroupStore, agent: &str, input: &Value) -> Option Option { - let codex = meta.get("x-codex-turn-metadata")?; - let id = ["session_id", "thread_id"] - .iter() - .find_map(|key| codex.get(*key).and_then(serde_json::Value::as_str)) - .map(str::trim) - .filter(|id| !id.is_empty())?; + let (agent, id) = if let Some(codex) = meta.get("x-codex-turn-metadata") { + ( + AgentKind::CODEX, + ["session_id", "thread_id"] + .iter() + .find_map(|key| codex.get(*key).and_then(serde_json::Value::as_str))?, + ) + } else { + let qwen = meta.get("qwen-code/invocation")?; + if qwen.get("version").and_then(serde_json::Value::as_u64) != Some(1) { + return None; + } + ( + AgentKind::QWEN_CODE, + qwen.get("sessionId").and_then(serde_json::Value::as_str)?, + ) + }; + let id = id.trim(); + if id.is_empty() { + return None; + } Some(Caller { - agent: AgentKind::CODEX.to_string(), + agent: agent.to_string(), session_id: id.to_string(), }) } @@ -119,10 +175,14 @@ pub fn detect() -> Option { } /// [`detect`] over a given environment and parent, for tests and for hosts -/// that know better than the process environment. +/// that know better than the process environment. `REBON_GROUP_AGENT` limits +/// detection to the configured host so a nested CLI cannot inherit its +/// launching agent's identity. pub fn resolve(env: &HashMap, parent: Option<&Parent>) -> Option { + let expected = env.get("REBON_GROUP_AGENT").map(|agent| agent.trim()); let present: Vec<(&SessionVar, &str)> = SESSION_VARS .iter() + .filter(|known| expected.is_none_or(|agent| known.agent == agent)) .filter_map(|known| { env.get(known.var) .map(|value| value.trim()) @@ -271,6 +331,56 @@ mod tests { } } + #[test] + fn known_agent_names_are_distinct_and_cover_common_clients() { + let names: std::collections::HashSet<_> = AgentKind::KNOWN.iter().copied().collect(); + assert_eq!(names.len(), AgentKind::KNOWN.len()); + for agent in [ + "pi", + "zcode", + "gemini-cli", + "qwen-code", + "kimi-cli", + "amp", + "cursor-agent", + "crush", + ] { + assert!(names.contains(agent), "{agent}"); + } + assert!(names + .iter() + .all(|name| !name.is_empty() && *name == name.to_lowercase())); + } + + #[test] + fn a_configured_external_host_does_not_inherit_the_launchers_identity() { + for &agent in AgentKind::KNOWN { + let inherited = env(&[ + ("REBON_GROUP_AGENT", agent), + ("REBON_SESSION_ID", "parent-rebon"), + ("CLAUDE_CODE_SESSION_ID", "parent-claude"), + ("GROK_SESSION_ID", "parent-grok"), + ]); + let caller = resolve(&inherited, Some(&parent(10, "rebon-cli.exe"))); + match agent { + AgentKind::REBON | AgentKind::CLAUDE_CODE | AgentKind::GROK => { + assert_eq!(caller.unwrap().agent, agent); + } + _ => assert_eq!(caller, None, "{agent}"), + } + } + assert_eq!( + resolve( + &env(&[ + ("REBON_GROUP_AGENT", "unknown"), + ("REBON_SESSION_ID", "parent") + ]), + None + ), + None + ); + } + #[test] fn one_session_variable_names_the_caller() { let caller = resolve(&env(&[("CLAUDE_CODE_SESSION_ID", "aa38901a")]), None).unwrap(); @@ -343,6 +453,47 @@ mod tests { ); } + #[test] + fn native_external_session_variables_are_used_only_for_the_configured_host() { + for (agent, variable) in [ + (AgentKind::PI, "PI_SESSION_ID"), + (AgentKind::GEMINI_CLI, "GEMINI_SESSION_ID"), + (AgentKind::QWEN_CODE, "QWEN_CODE_SESSION_ID"), + ] { + let values = env(&[ + ("REBON_GROUP_AGENT", agent), + ("REBON_SESSION_ID", "parent"), + (variable, " native-session "), + ]); + let caller = resolve(&values, Some(&parent(10, "rebon-cli.exe"))).unwrap(); + assert_eq!(caller.agent, agent); + assert_eq!(caller.session_id, "native-session"); + assert_eq!(resolve(&env(&[(variable, " ")]), None), None); + } + } + + #[test] + fn qwen_stdio_invocation_metadata_names_its_session() { + let caller = from_call_meta(&serde_json::json!({ + "qwen-code/invocation": { "version": 1, "sessionId": " qwen-session ", "promptId": "p" } + })) + .unwrap(); + assert_eq!(caller.agent, AgentKind::QWEN_CODE); + assert_eq!(caller.session_id, "qwen-session"); + for invocation in [ + serde_json::json!({ "version": 1, "sessionId": " " }), + serde_json::json!({ "version": 1, "sessionId": 1 }), + serde_json::json!({ "version": 2, "sessionId": "s" }), + serde_json::json!({ "sessionId": "s" }), + serde_json::json!({ "version": 1 }), + ] { + assert_eq!( + from_call_meta(&serde_json::json!({ "qwen-code/invocation": invocation })), + None + ); + } + } + #[test] fn this_process_has_a_parent() { assert!(parent_process().is_some_and(|parent| parent.pid != 0)); diff --git a/crates/rebon-group/src/tools_tests.rs b/crates/rebon-group/src/tools_tests.rs index a78bdbf..48aef5a 100644 --- a/crates/rebon-group/src/tools_tests.rs +++ b/crates/rebon-group/src/tools_tests.rs @@ -27,6 +27,64 @@ fn run( const ROOT: &str = "/work/app"; +#[test] +fn common_external_clients_bind_their_session_and_exchange_group_messages() { + for &agent in crate::identity::AgentKind::KNOWN { + let dir = tempfile::tempdir().unwrap(); + let store = GroupStore::new(dir.path()); + let mut who = None; + let joined = run( + &store, + &mut who, + ROOT, + GROUP_JOIN, + json!({ "group": "clients", "agent": agent, "session_id": "native-session", "alias": "worker" }), + ) + .unwrap(); + assert_eq!(who, caller(agent, "native-session")); + assert_eq!(joined["you"], "worker"); + let group_id = joined["group"]["id"].as_str().unwrap(); + let request = store + .post_as_user( + group_id, + Draft { + kind: EntryKind::Request, + to: Some("worker".into()), + re: None, + supersedes: None, + text: "verify the integration".into(), + }, + ) + .unwrap(); + let inbox = run(&store, &mut who, ROOT, GROUP_INBOX, json!({ "full": true })).unwrap(); + assert!(inbox["entries"] + .as_array() + .unwrap() + .iter() + .any(|entry| entry["id"] == request.id.as_deref().unwrap())); + run( + &store, + &mut who, + ROOT, + GROUP_SEND, + json!({ "to": "user", "kind": "reply", "re": request.id, "text": "verified" }), + ) + .unwrap(); + run( + &store, + &mut who, + ROOT, + GROUP_REMEMBER, + json!({ "fact": "shared contract" }), + ) + .unwrap(); + let memory = run(&store, &mut who, ROOT, GROUP_RECALL, json!({})).unwrap(); + assert!(memory.to_string().contains("shared contract")); + run(&store, &mut who, ROOT, GROUP_LEAVE, json!({})).unwrap(); + assert!(store.load(group_id).unwrap().members.is_empty()); + } +} + #[test] fn every_schema_is_a_closed_object_and_names_are_unique() { let specs = specs(); From 03ab49d6e6a353af299e624d7877d6f6c60023e7 Mon Sep 17 00:00:00 2001 From: Quorafind Date: Sat, 3 Oct 2026 21:39:36 +0800 Subject: [PATCH 2/3] feat(cli): Read the exec prompt from a file or stdin rebon exec --prompt-file reads the prompt as UTF-8, dropping a BOM and keeping the text otherwise untouched; "-" reads stdin. It conflicts with the positional prompt. The prompt is resolved before startup migrations, so a bad input fails without touching config. With --json, every failure now ends in exactly one error event: input errors, startup errors and turn errors all went out on different paths before, and some printed none while a projected error could be followed by a second one for the same turn. --- crates/rebon-cli/src/exec.rs | 188 +++++++++- crates/rebon-cli/src/main.rs | 101 +++++- crates/rebon-cli/tests/exec_json.rs | 382 ++++++++++++++++++++ crates/rebon-cli/tests/exec_prompt_input.rs | 106 ++++++ 4 files changed, 758 insertions(+), 19 deletions(-) create mode 100644 crates/rebon-cli/tests/exec_json.rs create mode 100644 crates/rebon-cli/tests/exec_prompt_input.rs diff --git a/crates/rebon-cli/src/exec.rs b/crates/rebon-cli/src/exec.rs index 57b6315..5a65c3c 100644 --- a/crates/rebon-cli/src/exec.rs +++ b/crates/rebon-cli/src/exec.rs @@ -40,7 +40,10 @@ use rebon_types::{AgentCapabilityMode, ContentBlock as AcpContentBlock, TextCont use crate::session::commands::effort::{resolve_thinking_from_effort, ThinkingOverrides}; -use std::path::PathBuf; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; /// Parsed inputs for `rebon exec`, assembled from the subcommand args plus the @@ -84,6 +87,52 @@ pub struct ExecArgs { pub max_duration_sec: Option, } +pub fn prepare_prompt( + words: &[String], + prompt_file: Option<&Path>, + json: bool, +) -> anyhow::Result { + resolve_prompt(words, prompt_file, std::io::stdin().lock()).inspect_err(|error| { + if json { + emit_error_once(&AtomicBool::new(false), &error.to_string()); + } + }) +} + +fn resolve_prompt( + words: &[String], + prompt_file: Option<&Path>, + mut stdin: impl Read, +) -> anyhow::Result { + let prompt = match prompt_file { + Some(path) => { + let source = if path == Path::new("-") { + "stdin".to_string() + } else { + format!("prompt file {}", path.display()) + }; + let mut bytes = Vec::new(); + if path == Path::new("-") { + stdin + .read_to_end(&mut bytes) + .with_context(|| format!("rebon exec: failed to read {source}"))?; + } else { + bytes = std::fs::read(path) + .with_context(|| format!("rebon exec: failed to read {source}"))?; + } + let bytes = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]).unwrap_or(&bytes); + std::str::from_utf8(bytes) + .with_context(|| format!("rebon exec: {source} is not valid UTF-8"))? + .to_owned() + } + None => words.join(" "), + }; + if prompt.trim().is_empty() { + anyhow::bail!("rebon exec: empty prompt"); + } + Ok(prompt) +} + fn resolve_exec_thinking( effort: Option, provider_format: ProviderFormat, @@ -118,10 +167,29 @@ fn select_unattended_allow_option(options: &[PermissionQueryOption]) -> Option anyhow::Result<()> { - if args.prompt.trim().is_empty() { - anyhow::bail!("rebon exec: empty prompt"); + let json = args.json; + let error_emitted = Arc::new(AtomicBool::new(false)); + let outcome = run_inner(args, &error_emitted).await; + if json { + if let Err(error) = &outcome { + emit_error_once(&error_emitted, &error.to_string()); + } } + outcome +} +fn emit_error_once(error_emitted: &AtomicBool, message: &str) { + if !error_emitted.swap(true, Ordering::Relaxed) { + let message = if message.trim().is_empty() { + "rebon exec failed" + } else { + message + }; + emit(&serde_json::json!({ "type": "error", "message": message })); + } +} + +async fn run_inner(args: ExecArgs, error_emitted: &Arc) -> anyhow::Result<()> { // Say out loud what this process is, before anything builds a tool list // from it. `exec` has no one to approve a plan, answer a question, or // retry a call the classifier stopped, and the tools that need one of @@ -161,10 +229,20 @@ pub async fn run(args: ExecArgs) -> anyhow::Result<()> { // Observe every raw QueryEvent and project it onto stdout. The callback runs // synchronously inside the executor's single consume loop, so lines are // emitted in event order without interleaving. + let observer_error_emitted = Arc::clone(error_emitted); let observer = QueryEventObserver::new(move |event| { if json { for value in project_json(event) { - emit(&value); + if value["type"] == "error" { + emit_error_once( + &observer_error_emitted, + value["message"] + .as_str() + .expect("projected error has a message"), + ); + } else { + emit(&value); + } } } else { project_text(event); @@ -379,6 +457,15 @@ pub async fn run(args: ExecArgs) -> anyhow::Result<()> { _server_state.close_session(&session_id); rebon_core::system_prompt::remove_scratchpad_for(&cwd, &session_id); + report_outcome(json, &session_id, deadline_reached, result) +} + +fn report_outcome( + json: bool, + session_id: &str, + deadline_reached: bool, + result: Result, +) -> anyhow::Result<()> { match result { Ok(outcome) => { // A caller cannot tell "the harness stopped this" from "the user @@ -426,9 +513,6 @@ pub async fn run(args: ExecArgs) -> anyhow::Result<()> { } Err(err) => { let message = format!("{err:?}"); - if json { - emit(&serde_json::json!({ "type": "error", "message": message })); - } Err(anyhow::anyhow!("rebon exec turn failed: {message}")) } } @@ -600,6 +684,96 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn prompt_input_preserves_positional_words() { + let words = vec![" 检查\n项目".into(), "'quoted' --flag ".into()]; + assert_eq!( + resolve_prompt(&words, None, std::io::empty()).unwrap(), + " 检查\n项目 'quoted' --flag " + ); + } + + #[test] + fn prompt_input_reads_utf8_files_and_bom_without_trimming() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("提示词.txt"); + let content = " 第一行\r\n第二行 café\n\"quoted\"\t\n"; + for prefix in ["", "\u{feff}"] { + std::fs::write(&path, format!("{prefix}{content}")).unwrap(); + assert_eq!( + resolve_prompt(&[], Some(&path), std::io::empty()).unwrap(), + content + ); + } + } + + #[test] + fn prompt_input_reads_stdin_and_bom_without_trimming() { + let content = " 多行提示\n非 ASCII café\r\n"; + for prefix in ["", "\u{feff}"] { + let input = format!("{prefix}{content}"); + assert_eq!( + resolve_prompt(&[], Some(Path::new("-")), input.as_bytes()).unwrap(), + content + ); + } + } + + #[test] + fn prompt_input_rejects_empty_and_whitespace_from_all_sources() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("empty.txt"); + assert!(resolve_prompt(&[], None, std::io::empty()).is_err()); + for input in ["", " \t\r\n\u{2003}"] { + assert!(resolve_prompt(&[input.into()], None, std::io::empty()).is_err()); + for prefix in ["", "\u{feff}"] { + let input = format!("{prefix}{input}"); + std::fs::write(&path, &input).unwrap(); + for error in [ + resolve_prompt(&[], Some(&path), std::io::empty()).unwrap_err(), + resolve_prompt(&[], Some(Path::new("-")), input.as_bytes()).unwrap_err(), + ] { + assert!(error.to_string().contains("empty prompt")); + } + } + } + } + + #[test] + fn prompt_input_rejects_missing_unreadable_and_invalid_utf8_files() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("input.txt"); + for unreadable in [path.as_path(), dir.path()] { + let error = resolve_prompt(&[], Some(unreadable), std::io::empty()).unwrap_err(); + assert!(error.to_string().contains("failed to read prompt file")); + assert!(error + .to_string() + .contains(&unreadable.display().to_string())); + } + for bytes in [vec![0xff], vec![0xef, 0xbb, 0xbf, 0xc3, 0x28]] { + std::fs::write(&path, &bytes).unwrap(); + let error = resolve_prompt(&[], Some(&path), std::io::empty()).unwrap_err(); + assert!(error.to_string().contains("not valid UTF-8")); + let error = resolve_prompt(&[], Some(Path::new("-")), bytes.as_slice()).unwrap_err(); + assert!(error.to_string().contains("stdin is not valid UTF-8")); + } + } + + #[test] + fn prompt_input_reports_stdin_read_errors() { + struct Unreadable; + impl Read for Unreadable { + fn read(&mut self, _: &mut [u8]) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "read failed", + )) + } + } + let error = resolve_prompt(&[], Some(Path::new("-")), Unreadable).unwrap_err(); + assert!(error.to_string().contains("failed to read stdin")); + } + fn permission_option(option_id: &str, kind: PermissionOptionKind) -> PermissionQueryOption { PermissionQueryOption { option_id: option_id.into(), diff --git a/crates/rebon-cli/src/main.rs b/crates/rebon-cli/src/main.rs index 964eae3..cd30c3b 100644 --- a/crates/rebon-cli/src/main.rs +++ b/crates/rebon-cli/src/main.rs @@ -435,9 +435,12 @@ enum Command { /// /// Example: `rebon exec --json --model gpt-5.4 "what is the weather in Beijing?"` Exec { - /// The prompt text to send (all trailing words are joined). - #[arg(value_name = "PROMPT", num_args = 1.., trailing_var_arg = true)] + /// 要发送的提示词文本(所有尾随词以空格连接),与 --prompt-file 互斥。 + #[arg(value_name = "PROMPT", num_args = 1.., trailing_var_arg = true, required_unless_present = "prompt_file")] prompt: Vec, + /// 从 UTF-8 文件读取提示词(支持 BOM);使用 - 从 stdin 读取。 + #[arg(long, value_name = "PATH", conflicts_with = "prompt")] + prompt_file: Option, /// Emit machine-readable JSONL events on stdout (one object per line). /// Without it, a compact human-readable trace is printed instead. #[arg(long = "json", default_value_t = false)] @@ -1409,14 +1412,6 @@ async fn async_main() -> anyhow::Result<()> { async fn route_main() -> anyhow::Result<()> { let startup_started = std::time::Instant::now(); let mut cli = Cli::parse(); - // Runs before any route can touch `config.json`: the seeding rule keys off - // "config exists but has no flag", so a first run must still look like a - // first run here. - rebon_config::migrate_claude_codex_fallback_default(); - // Provider definitions move to `~/.rebon/providers/` here, before any - // route resolves one. Reversible and non-fatal: a failure leaves - // `config.json` authoritative. - rebon_config::migrate_providers_to_store(); let route = classify_cli_startup(&cli)?; let cwd_scope = match cli.cwd.as_ref() { Some(cwd) => { @@ -1429,6 +1424,23 @@ async fn route_main() -> anyhow::Result<()> { } None => None, }; + let exec_prompt = match cli.command.as_ref() { + Some(Command::Exec { + prompt, + prompt_file, + json, + .. + }) => Some(exec::prepare_prompt(prompt, prompt_file.as_deref(), *json)?), + _ => None, + }; + // Runs before any route can touch `config.json`: the seeding rule keys off + // "config exists but has no flag", so a first run must still look like a + // first run here. + rebon_config::migrate_claude_codex_fallback_default(); + // Provider definitions move to `~/.rebon/providers/` here, before any + // route resolves one. Reversible and non-fatal: a failure leaves + // `config.json` authoritative. + rebon_config::migrate_providers_to_store(); match route { StartupRoute::AgentView => { init_tracing(true); @@ -1466,7 +1478,8 @@ async fn route_main() -> anyhow::Result<()> { ); match command { Command::Exec { - prompt, + prompt: _, + prompt_file: _, json, resume, ephemeral, @@ -1477,7 +1490,7 @@ async fn route_main() -> anyhow::Result<()> { max_duration, } => { return exec::run(exec::ExecArgs { - prompt: prompt.join(" "), + prompt: exec_prompt.expect("exec input was resolved before startup"), json, resume, ephemeral, @@ -1879,6 +1892,68 @@ mod tests { assert_eq!(cli.model.as_deref(), Some("gpt-5.6")); } + #[test] + fn exec_prompt_file_parses_paths_and_stdin() { + for path in ["prompt.txt", "提示词 with spaces.txt", "-"] { + let cli = Cli::try_parse_from(["rebon", "exec", "--prompt-file", path]).unwrap(); + assert!(matches!( + cli.command, + Some(Command::Exec { prompt, prompt_file, .. }) + if prompt.is_empty() && prompt_file == Some(PathBuf::from(path)) + )); + } + } + + #[test] + fn exec_prompt_file_requires_one_input_and_rejects_conflicts() { + for args in [vec!["rebon", "exec"], vec!["rebon", "exec", "--json"]] { + assert_eq!( + Cli::try_parse_from(args).unwrap_err().kind(), + clap::error::ErrorKind::MissingRequiredArgument + ); + } + assert_eq!( + Cli::try_parse_from(["rebon", "exec", "--prompt-file"]) + .unwrap_err() + .kind(), + clap::error::ErrorKind::InvalidValue + ); + for path in ["prompt.txt", "-"] { + let error = + Cli::try_parse_from(["rebon", "exec", "--prompt-file", path, "positional prompt"]) + .unwrap_err(); + assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); + } + } + + #[test] + fn exec_prompt_file_keeps_trailing_positional_behavior() { + let cli = Cli::try_parse_from([ + "rebon", + "exec", + "inspect", + "--prompt-file", + "literal.txt", + "--json", + ]) + .unwrap(); + assert!(matches!( + cli.command, + Some(Command::Exec { prompt, prompt_file: None, json: false, .. }) + if prompt == ["inspect", "--prompt-file", "literal.txt", "--json"] + )); + } + + #[test] + fn exec_prompt_file_help_describes_utf8_bom_and_stdin() { + let error = Cli::try_parse_from(["rebon", "exec", "--help"]).unwrap_err(); + assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp); + let help = error.to_string(); + for expected in ["--prompt-file ", "PROMPT", "UTF-8", "BOM", "stdin"] { + assert!(help.contains(expected), "{help}"); + } + } + #[test] fn exec_configuration_flags_parse_after_subcommand() { let cli = Cli::parse_from([ @@ -1897,6 +1972,7 @@ mod tests { cli.command, Some(Command::Exec { prompt: vec!["inspect".to_string(), "the project".to_string()], + prompt_file: None, json: false, resume: None, ephemeral: false, @@ -1942,6 +2018,7 @@ mod tests { cli.command, Some(Command::Exec { prompt: vec!["inspect".to_string()], + prompt_file: None, json: false, resume: None, ephemeral: false, diff --git a/crates/rebon-cli/tests/exec_json.rs b/crates/rebon-cli/tests/exec_json.rs new file mode 100644 index 0000000..dafdaba --- /dev/null +++ b/crates/rebon-cli/tests/exec_json.rs @@ -0,0 +1,382 @@ +use std::path::PathBuf; +use std::process::{Output, Stdio}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use serde_json::{json, Value}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; +use tokio::process::Command; + +struct ExecFixture { + root: tempfile::TempDir, + home: PathBuf, + cwd: PathBuf, + _config_home: rebon_tool::tasks::test_support::TestConfigHome, +} + +impl ExecFixture { + fn new() -> Self { + let config_home = rebon_tool::tasks::test_support::TestConfigHome::new("exec-json"); + let root = tempfile::tempdir().expect("test root"); + let home = root.path().join("home"); + let cwd = root.path().join("cwd"); + std::fs::create_dir(&home).expect("config home"); + std::fs::create_dir(&cwd).expect("cwd"); + std::fs::write(home.join("config.json"), "{}").expect("config"); + std::fs::write(home.join("settings.json"), "{}").expect("settings"); + Self { + root, + home, + cwd, + _config_home: config_home, + } + } + + fn provider(&self, address: std::net::SocketAddr) { + let trust_key = rebon_session::cwd_identity(&self.cwd.to_string_lossy()); + std::fs::write( + self.home.join("config.json"), + json!({ + "activeCustomProvider": "mock", + "customProviders": [{ + "name": "mock", "format": "openai", + "baseUrl": format!("http://{address}/v1"), + "apiKey": "exec-test-not-a-secret", "model": "mock-model", + "models": ["mock-model"] + }], + "projects": { trust_key: { "hasTrustDialogAccepted": true } } + }) + .to_string(), + ) + .expect("mock config"); + } + + async fn run(&self, args: &[&str]) -> Output { + let binary = PathBuf::from(env!("CARGO_BIN_EXE_rebon")); + let mut command = Command::new(std::fs::canonicalize(&binary).expect("exec binary path")); + command.env_clear(); + for name in ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"] { + if let Some(value) = std::env::var_os(name) { + command.env(name, value); + } + } + command + .args(args) + .current_dir(&self.cwd) + .env("REBON_CONFIG_DIR", &self.home) + .env("HOME", self.root.path()) + .env("USERPROFILE", self.root.path()) + .env("NO_PROXY", "127.0.0.1,localhost") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true); + tokio::time::timeout(Duration::from_secs(120), command.output()) + .await + .expect("exec must finish and shut down the plugin plane") + .expect("run rebon exec") + } +} + +fn events(output: &Output) -> Vec { + let stdout = std::str::from_utf8(&output.stdout).expect("UTF-8 stdout"); + stdout + .lines() + .map(|line| { + serde_json::from_str(line) + .unwrap_or_else(|error| panic!("stdout must contain JSONL only: {error}: {stdout}")) + }) + .collect() +} + +fn assert_one_error(output: &Output) -> Vec { + assert!(!output.status.success(), "failure must exit nonzero"); + assert!(!String::from_utf8_lossy(&output.stdout).contains("exec-test-not-a-secret")); + let events = events(output); + let errors: Vec<_> = events + .iter() + .filter(|event| event["type"] == "error") + .collect(); + assert_eq!(errors.len(), 1, "{events:?}"); + let error = errors[0]; + assert_eq!(error.as_object().expect("error object").len(), 2); + assert!(!error["message"] + .as_str() + .expect("error message") + .trim() + .is_empty()); + assert!(!error["message"] + .as_str() + .unwrap() + .contains("stack backtrace")); + events +} +fn event_types(events: &[Value]) -> Vec<&str> { + events + .iter() + .map(|event| event["type"].as_str().expect("event type")) + .collect() +} + +#[derive(Clone, Copy)] +enum Reply { + Success, + Failure, + Pending, +} + +struct Provider { + address: std::net::SocketAddr, + seen: Arc>>, + task: tokio::task::JoinHandle<()>, +} + +impl Provider { + async fn start(reply: Reply) -> Self { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("local provider"); + let address = listener.local_addr().expect("provider address"); + let seen = Arc::new(Mutex::new(Vec::new())); + let requests = Arc::clone(&seen); + let task = tokio::spawn(async move { + loop { + let (mut stream, _) = listener.accept().await.expect("provider connection"); + let mut buffer = Vec::new(); + let mut chunk = [0; 4096]; + let header_end = loop { + let read = stream.read(&mut chunk).await.expect("request headers"); + assert_ne!(read, 0); + buffer.extend_from_slice(&chunk[..read]); + if let Some(at) = buffer.windows(4).position(|window| window == b"\r\n\r\n") { + break at + 4; + } + }; + let head = String::from_utf8_lossy(&buffer[..header_end]).to_ascii_lowercase(); + let length = head + .lines() + .find_map(|line| line.strip_prefix("content-length:")) + .expect("request content-length") + .trim() + .parse::() + .expect("body length"); + while buffer.len() < header_end + length { + let read = stream.read(&mut chunk).await.expect("request body"); + assert_ne!(read, 0); + buffer.extend_from_slice(&chunk[..read]); + } + let body: Value = + serde_json::from_slice(&buffer[header_end..]).expect("request JSON"); + requests + .lock() + .expect("requests poisoned") + .push(body.clone()); + let (status, content_type, response) = match reply { + Reply::Pending => std::future::pending().await, + Reply::Failure => ("400 Bad Request", "application/json", + json!({"error": {"message": "mock turn failed", "type": "invalid_request_error"}}).to_string()), + Reply::Success if body["stream"] == true => { + let chunks = [ + json!({"id": "c1", "object": "chat.completion.chunk", "model": "mock-model", + "choices": [{"index": 0, "delta": {"role": "assistant", "content": "done"}, "finish_reason": null}]}), + json!({"id": "c1", "object": "chat.completion.chunk", "model": "mock-model", + "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}), + json!({"id": "c1", "object": "chat.completion.chunk", "model": "mock-model", "choices": [], + "usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}}), + ]; + let mut sse: String = chunks.iter().map(|chunk| format!("data: {chunk}\n\n")).collect(); + sse.push_str("data: [DONE]\n\n"); + ("200 OK", "text/event-stream", sse) + } + Reply::Success => ("200 OK", "application/json", + json!({"id": "c1", "object": "chat.completion", "model": "mock-model", + "choices": [{"index": 0, "message": {"role": "assistant", "content": "done"}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}}).to_string()), + }; + let response = format!("HTTP/1.1 {status}\r\ncontent-type: {content_type}\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{response}", response.len()); + stream + .write_all(response.as_bytes()) + .await + .expect("provider response"); + stream.shutdown().await.expect("provider shutdown"); + } + }); + Self { + address, + seen, + task, + } + } +} + +impl Drop for Provider { + fn drop(&mut self) { + self.task.abort(); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn plugin_initialization_failure_emits_one_json_error() { + let fixture = ExecFixture::new(); + let plugin_dir = fixture.home.join("plugins"); + std::fs::create_dir(&plugin_dir).expect("plugins"); + std::fs::write(plugin_dir.join("installed.json"), "not JSON").expect("invalid plugin state"); + let output = fixture.run(&["exec", "--json", "hello"]).await; + let events = assert_one_error(&output); + assert_eq!(events.len(), 1); + assert!(events[0]["message"] + .as_str() + .unwrap() + .contains("failed to parse plugin state")); +} + +#[tokio::test(flavor = "multi_thread")] +async fn turn_failure_does_not_repeat_the_observer_error() { + let fixture = ExecFixture::new(); + let provider = Provider::start(Reply::Failure).await; + fixture.provider(provider.address); + let output = fixture.run(&["exec", "--json", "hello"]).await; + let events = assert_one_error(&output); + assert_eq!(event_types(&events), ["session", "error"]); + assert!(events[1]["message"] + .as_str() + .unwrap() + .contains("mock turn failed")); + assert!(!provider.seen.lock().expect("requests poisoned").is_empty()); + let output = fixture.run(&["exec", "hello"]).await; + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn successful_turn_and_resume_keep_the_json_contract() { + let fixture = ExecFixture::new(); + let provider = Provider::start(Reply::Success).await; + fixture.provider(provider.address); + let output = fixture.run(&["exec", "--json", "first prompt"]).await; + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let first = events(&output); + assert_eq!( + event_types(&first), + ["session", "message", "turn.completed", "result"] + ); + assert_eq!(first[1]["text"], "done"); + assert_eq!(first[3]["stopReason"], "end_turn"); + assert_eq!(first[3]["usage"]["input_tokens"], 10); + assert_eq!(first[3]["usage"]["output_tokens"], 2); + let session_id = first[0]["sessionId"].as_str().expect("session id"); + let output = fixture + .run(&["exec", "--json", "--resume", session_id, "second prompt"]) + .await; + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let resumed = events(&output); + assert_eq!(event_types(&resumed), event_types(&first)); + assert_eq!(resumed[0], first[0]); + assert_eq!(resumed[3]["sessionId"], session_id); + let requests = provider.seen.lock().expect("requests poisoned"); + let replay = requests.last().expect("resume request")["messages"].to_string(); + assert!(replay.contains("first prompt")); + assert!(replay.contains("done")); + assert!(replay.contains("second prompt")); + drop(requests); + let output = fixture.run(&["exec", "text prompt"]).await; + assert!(output.status.success()); + assert_eq!(std::str::from_utf8(&output.stdout).unwrap().trim(), "done"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn max_duration_keeps_cancelled_event_and_successful_result() { + let fixture = ExecFixture::new(); + let provider = Provider::start(Reply::Pending).await; + fixture.provider(provider.address); + let output = fixture + .run(&["exec", "--json", "--max-duration", "1", "hello"]) + .await; + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let events = events(&output); + assert_eq!(event_types(&events), ["session", "error", "result"]); + assert_eq!(events[1], json!({"type": "error", "message": "cancelled"})); + assert_eq!( + events[2], + json!({"type": "result", "sessionId": events[0]["sessionId"], + "stopReason": "max_duration", "usage": null}) + ); + assert!(!provider.seen.lock().expect("requests poisoned").is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn slash_command_answer_keeps_message_and_result_without_a_turn() { + let fixture = ExecFixture::new(); + let provider = Provider::start(Reply::Success).await; + fixture.provider(provider.address); + let output = fixture.run(&["exec", "--json", "/runtime"]).await; + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let events = events(&output); + assert_eq!(event_types(&events), ["session", "message", "result"]); + assert!(!events[1]["text"].as_str().expect("answer").is_empty()); + assert_eq!( + events[2], + json!({"type": "result", "sessionId": events[0]["sessionId"], + "stopReason": "command", "usage": null}) + ); + assert!(provider.seen.lock().expect("requests poisoned").is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn blank_prompt_emits_one_json_error_and_text_mode_stays_text() { + let fixture = ExecFixture::new(); + for prompt in ["", " \t\n "] { + let output = fixture.run(&["exec", "--json", prompt]).await; + let events = assert_one_error(&output); + assert_eq!( + events, + [json!({"type": "error", "message": "rebon exec: empty prompt"})] + ); + } + let output = fixture.run(&["exec", " \t "]).await; + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("empty prompt")); +} + +#[tokio::test(flavor = "multi_thread")] +async fn provider_configuration_failure_emits_one_json_error_before_session() { + let fixture = ExecFixture::new(); + let output = fixture + .run(&[ + "--provider", + "missing-test-provider", + "exec", + "--json", + "hello", + ]) + .await; + let events = assert_one_error(&output); + assert_eq!(events.len(), 1); + assert!(events[0]["message"] + .as_str() + .unwrap() + .contains("failed to build headless session")); + let output = fixture + .run(&["--provider", "missing-test-provider", "exec", "hello"]) + .await; + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); +} diff --git a/crates/rebon-cli/tests/exec_prompt_input.rs b/crates/rebon-cli/tests/exec_prompt_input.rs new file mode 100644 index 0000000..cbcd27e --- /dev/null +++ b/crates/rebon-cli/tests/exec_prompt_input.rs @@ -0,0 +1,106 @@ +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; + +use tokio::io::AsyncWriteExt; +use tokio::process::Command; + +#[tokio::test(flavor = "multi_thread")] +async fn exec_input_errors_precede_config_and_runtime_startup() { + let _config_home = rebon_tool::tasks::test_support::TestConfigHome::new("exec-prompt-input"); + let dir = tempfile::tempdir().unwrap(); + let home = dir.path().join("config-home"); + let empty = dir.path().join("empty.txt"); + let invalid = dir.path().join("invalid.txt"); + let missing = dir.path().join("missing.txt"); + std::fs::write(&empty, "\u{feff} \r\n\t").unwrap(); + std::fs::write(&invalid, [0xef, 0xbb, 0xbf, 0xff]).unwrap(); + + let cases = [ + (Vec::new(), Vec::new(), "required"), + (vec![" \t\n".to_string()], Vec::new(), "empty prompt"), + ( + vec!["--prompt-file".into(), empty.display().to_string()], + Vec::new(), + "empty prompt", + ), + ( + vec!["--prompt-file".into(), invalid.display().to_string()], + Vec::new(), + "not valid UTF-8", + ), + ( + vec!["--prompt-file".into(), missing.display().to_string()], + Vec::new(), + "failed to read prompt file", + ), + ( + vec!["--prompt-file".into(), dir.path().display().to_string()], + Vec::new(), + "failed to read prompt file", + ), + ( + vec!["--prompt-file".into(), "-".into()], + Vec::new(), + "empty prompt", + ), + ( + vec!["--prompt-file".into(), "-".into()], + b"\xef\xbb\xbf \r\n".to_vec(), + "empty prompt", + ), + ( + vec!["--prompt-file".into(), "-".into()], + vec![0xff], + "stdin is not valid UTF-8", + ), + ( + vec!["--prompt-file".into(), "-".into(), "conflict".into()], + Vec::new(), + "cannot be used with", + ), + ]; + let binary = PathBuf::from(env!("CARGO_BIN_EXE_rebon")) + .canonicalize() + .unwrap(); + for (args, input, expected) in cases { + for json in [false, true] { + let mut child = Command::new(&binary) + .arg("exec") + .args(json.then_some("--json")) + .args(&args) + .current_dir(dir.path()) + .env("REBON_CONFIG_DIR", &home) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true) + .spawn() + .unwrap(); + let mut stdin = child.stdin.take().unwrap(); + stdin.write_all(&input).await.unwrap(); + drop(stdin); + let output = tokio::time::timeout(Duration::from_secs(30), child.wait_with_output()) + .await + .expect("输入错误不应等待运行时启动") + .unwrap(); + assert!(!output.status.success(), "{args:?}"); + if json && expected != "required" && expected != "cannot be used with" { + let events: Vec = std::str::from_utf8(&output.stdout) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect(); + assert_eq!(events.len(), 1, "{args:?}: {events:?}"); + assert_eq!(events[0]["type"], "error"); + assert_eq!(events[0].as_object().unwrap().len(), 2); + assert!(events[0]["message"].as_str().unwrap().contains(expected)); + } else { + assert!(output.stdout.is_empty(), "{args:?}: {:?}", output.stdout); + } + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.contains(expected), "{args:?}: {stderr}"); + assert!(!home.exists(), "输入错误不应创建配置目录: {args:?}"); + } + } +} From f1051aff07485f1d0c615aaca7a7d77f8e3b0a17 Mon Sep 17 00:00:00 2001 From: Quorafind Date: Sat, 3 Oct 2026 21:43:02 +0800 Subject: [PATCH 3/3] release(cli): Release v1.8.7 Keep the shipped version and release tag in sync. --- Cargo.lock | 154 +++++++++++++++++++++--------------------- Cargo.toml | 2 +- npm/bare/package.json | 4 +- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3dfebe..b4086d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2991,7 +2991,7 @@ dependencies = [ [[package]] name = "rebon-acp" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3018,7 +3018,7 @@ dependencies = [ [[package]] name = "rebon-acp-client" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3039,7 +3039,7 @@ dependencies = [ [[package]] name = "rebon-agent-core" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3058,7 +3058,7 @@ dependencies = [ [[package]] name = "rebon-api" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3078,7 +3078,7 @@ dependencies = [ [[package]] name = "rebon-boa-runner" -version = "1.8.6" +version = "1.8.7" dependencies = [ "boa_engine", "boa_gc", @@ -3109,7 +3109,7 @@ dependencies = [ [[package]] name = "rebon-cli" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "arboard", @@ -3187,7 +3187,7 @@ dependencies = [ [[package]] name = "rebon-code-runner" -version = "1.8.6" +version = "1.8.7" dependencies = [ "futures-util", "libc", @@ -3200,7 +3200,7 @@ dependencies = [ [[package]] name = "rebon-command-seat" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-kernel", "rebon-slash-commands", @@ -3209,7 +3209,7 @@ dependencies = [ [[package]] name = "rebon-config" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "rebon-api", @@ -3227,7 +3227,7 @@ dependencies = [ [[package]] name = "rebon-config-seat" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-kernel", "rebon-types", @@ -3235,7 +3235,7 @@ dependencies = [ [[package]] name = "rebon-core" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3275,22 +3275,22 @@ dependencies = [ [[package]] name = "rebon-customselect" -version = "1.8.6" +version = "1.8.7" [[package]] name = "rebon-design-system" -version = "1.8.6" +version = "1.8.7" [[package]] name = "rebon-dialog" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-customselect", ] [[package]] name = "rebon-group" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "fs2", @@ -3302,7 +3302,7 @@ dependencies = [ [[package]] name = "rebon-harness" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "rebon-hooks" -version = "1.8.6" +version = "1.8.7" dependencies = [ "async-trait", "rebon-tools-core", @@ -3378,7 +3378,7 @@ dependencies = [ [[package]] name = "rebon-i18n-gen" -version = "1.8.6" +version = "1.8.7" dependencies = [ "serde", "serde_json", @@ -3386,7 +3386,7 @@ dependencies = [ [[package]] name = "rebon-instructions" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-kernel", "rebon-session", @@ -3395,7 +3395,7 @@ dependencies = [ [[package]] name = "rebon-kernel" -version = "1.8.6" +version = "1.8.7" dependencies = [ "serde_json", "thiserror 2.0.18", @@ -3404,7 +3404,7 @@ dependencies = [ [[package]] name = "rebon-kernel-seats" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3436,7 +3436,7 @@ dependencies = [ [[package]] name = "rebon-math" -version = "1.8.6" +version = "1.8.7" dependencies = [ "ratex-katex-fonts", "ratex-layout", @@ -3448,7 +3448,7 @@ dependencies = [ [[package]] name = "rebon-mcp-channel" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "clap", @@ -3469,7 +3469,7 @@ dependencies = [ [[package]] name = "rebon-message-tui" -version = "1.8.6" +version = "1.8.7" dependencies = [ "pulldown-cmark", "ratatui", @@ -3483,7 +3483,7 @@ dependencies = [ [[package]] name = "rebon-node-runtime" -version = "1.8.6" +version = "1.8.7" dependencies = [ "serde", "serde_json", @@ -3494,7 +3494,7 @@ dependencies = [ [[package]] name = "rebon-permissions" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-shell-policy", "rebon-tools-core", @@ -3503,11 +3503,11 @@ dependencies = [ [[package]] name = "rebon-picker" -version = "1.8.6" +version = "1.8.7" [[package]] name = "rebon-plugin-agents" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3540,7 +3540,7 @@ dependencies = [ [[package]] name = "rebon-plugin-browser" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "base64 0.22.1", @@ -3561,7 +3561,7 @@ dependencies = [ [[package]] name = "rebon-plugin-computer-use" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3592,7 +3592,7 @@ dependencies = [ [[package]] name = "rebon-plugin-cron" -version = "1.8.6" +version = "1.8.7" dependencies = [ "async-trait", "rebon-core", @@ -3606,7 +3606,7 @@ dependencies = [ [[package]] name = "rebon-plugin-escalation" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3621,7 +3621,7 @@ dependencies = [ [[package]] name = "rebon-plugin-groups" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3638,7 +3638,7 @@ dependencies = [ [[package]] name = "rebon-plugin-host" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3669,7 +3669,7 @@ dependencies = [ [[package]] name = "rebon-plugin-image-gen" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3690,7 +3690,7 @@ dependencies = [ [[package]] name = "rebon-plugin-lsp-mcp" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3710,7 +3710,7 @@ dependencies = [ [[package]] name = "rebon-plugin-mcp" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3732,7 +3732,7 @@ dependencies = [ [[package]] name = "rebon-plugin-memory" -version = "1.8.6" +version = "1.8.7" dependencies = [ "async-trait", "rebon-api", @@ -3755,7 +3755,7 @@ dependencies = [ [[package]] name = "rebon-plugin-model-prompt" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-api", "rebon-core", @@ -3764,7 +3764,7 @@ dependencies = [ [[package]] name = "rebon-plugin-model-routing" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3787,7 +3787,7 @@ dependencies = [ [[package]] name = "rebon-plugin-monitor" -version = "1.8.6" +version = "1.8.7" dependencies = [ "async-trait", "futures-util", @@ -3804,7 +3804,7 @@ dependencies = [ [[package]] name = "rebon-plugin-node-host" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-kernel", "tracing", @@ -3812,7 +3812,7 @@ dependencies = [ [[package]] name = "rebon-plugin-notebook" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3830,7 +3830,7 @@ dependencies = [ [[package]] name = "rebon-plugin-onboarding" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "base64 0.22.1", @@ -3856,7 +3856,7 @@ dependencies = [ [[package]] name = "rebon-plugin-package" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "rebon-hooks", @@ -3869,7 +3869,7 @@ dependencies = [ [[package]] name = "rebon-plugin-plan-mode" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3888,7 +3888,7 @@ dependencies = [ [[package]] name = "rebon-plugin-profile" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3910,7 +3910,7 @@ dependencies = [ [[package]] name = "rebon-plugin-protocol" -version = "1.8.6" +version = "1.8.7" dependencies = [ "serde", "serde_json", @@ -3920,7 +3920,7 @@ dependencies = [ [[package]] name = "rebon-plugin-remote" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "clap", @@ -3937,7 +3937,7 @@ dependencies = [ [[package]] name = "rebon-plugin-sandbox" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "chrono", @@ -3959,7 +3959,7 @@ dependencies = [ [[package]] name = "rebon-plugin-skill" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -3983,7 +3983,7 @@ dependencies = [ [[package]] name = "rebon-plugin-structured-output" -version = "1.8.6" +version = "1.8.7" dependencies = [ "async-trait", "rebon-core", @@ -3996,7 +3996,7 @@ dependencies = [ [[package]] name = "rebon-plugin-supervisor" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-node-runtime", "rebon-plugin-protocol", @@ -4010,7 +4010,7 @@ dependencies = [ [[package]] name = "rebon-plugin-tasks" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -4036,7 +4036,7 @@ dependencies = [ [[package]] name = "rebon-plugin-updater" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "clap", @@ -4055,7 +4055,7 @@ dependencies = [ [[package]] name = "rebon-plugin-web" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -4077,7 +4077,7 @@ dependencies = [ [[package]] name = "rebon-plugin-workflow" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -4101,7 +4101,7 @@ dependencies = [ [[package]] name = "rebon-proto" -version = "1.8.6" +version = "1.8.7" dependencies = [ "libc", "rebon-types", @@ -4115,7 +4115,7 @@ dependencies = [ [[package]] name = "rebon-provider" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -4138,7 +4138,7 @@ dependencies = [ [[package]] name = "rebon-rc-runner" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -4188,7 +4188,7 @@ dependencies = [ [[package]] name = "rebon-render" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-design-system", "rebon-tools-core", @@ -4201,7 +4201,7 @@ dependencies = [ [[package]] name = "rebon-schema-gen" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-proto", "rebon-render", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "rebon-session" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "fs2", @@ -4229,7 +4229,7 @@ dependencies = [ [[package]] name = "rebon-session-host" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "chrono", @@ -4253,7 +4253,7 @@ dependencies = [ [[package]] name = "rebon-session-runtime" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -4301,7 +4301,7 @@ dependencies = [ [[package]] name = "rebon-session-state" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-permissions", "rebon-proto", @@ -4316,14 +4316,14 @@ dependencies = [ [[package]] name = "rebon-shell" -version = "1.8.6" +version = "1.8.7" dependencies = [ "serde_json", ] [[package]] name = "rebon-shell-policy" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-tools-core", "serde_json", @@ -4332,7 +4332,7 @@ dependencies = [ [[package]] name = "rebon-slash-commands" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-types", "tracing", @@ -4340,14 +4340,14 @@ dependencies = [ [[package]] name = "rebon-spinner" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-types", ] [[package]] name = "rebon-tool" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "async-trait", @@ -4387,7 +4387,7 @@ dependencies = [ [[package]] name = "rebon-tools-core" -version = "1.8.6" +version = "1.8.7" dependencies = [ "anyhow", "libc", @@ -4401,7 +4401,7 @@ dependencies = [ [[package]] name = "rebon-tui" -version = "1.8.6" +version = "1.8.7" dependencies = [ "base64 0.22.1", "image 0.25.10", @@ -4426,7 +4426,7 @@ dependencies = [ [[package]] name = "rebon-types" -version = "1.8.6" +version = "1.8.7" dependencies = [ "getrandom 0.2.17", "rebon-tools-core", @@ -4440,7 +4440,7 @@ dependencies = [ [[package]] name = "rebon-ui-seat" -version = "1.8.6" +version = "1.8.7" dependencies = [ "rebon-dialog", "rebon-kernel", @@ -4450,7 +4450,7 @@ dependencies = [ [[package]] name = "rebon-width" -version = "1.8.6" +version = "1.8.7" dependencies = [ "unicode-width", ] @@ -4794,7 +4794,7 @@ dependencies = [ [[package]] name = "sandbox-win" -version = "1.8.6" +version = "1.8.7" dependencies = [ "fs2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7c8523d..b5e5aa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ members = [ exclude = ["services/rc-server"] [workspace.package] -version = "1.8.6" +version = "1.8.7" edition = "2021" license = "Apache-2.0" rust-version = "1.80" diff --git a/npm/bare/package.json b/npm/bare/package.json index 97c2390..de226cc 100644 --- a/npm/bare/package.json +++ b/npm/bare/package.json @@ -1,6 +1,6 @@ { "name": "rebon", - "version": "1.8.6", + "version": "1.8.7", "description": "Rebon agent CLI for coding and more. Installs @rebon/cli.", "license": "Apache-2.0", "repository": { @@ -19,7 +19,7 @@ "rebon": "bin/rebon.js" }, "dependencies": { - "@rebon/cli": "^1.8.6" + "@rebon/cli": "^1.8.7" }, "files": [ "bin",