mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-02 18:06:58 +00:00
Claude Code gets its message on stdin, so a long brief starts on Windows (#430)
Claude Code's message goes to its stdin instead of argv, so a builder brief over Windows' 32,767-character command-line limit no longer stops the build with os error 206. Fixes #402.
This commit is contained in:
@@ -66,6 +66,11 @@ pub trait AgentAdapter: Send + Sync {
|
||||
fn answers_questions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// The message goes in on stdin, not in argv: a long one passes Windows' command-line limit
|
||||
/// ("The filename or extension is too long", os error 206, #402).
|
||||
fn prompt_on_stdin(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// The process for a local run of `program` (an adapter may clean its environment).
|
||||
fn process(&self, program: &str) -> Process {
|
||||
Process::new(program)
|
||||
@@ -169,7 +174,6 @@ const CHANGING_TOOLS: &[&str] =
|
||||
fn question_args(mut a: Vec<String>, cx: &ArgsCx) -> Vec<String> {
|
||||
a.extend([
|
||||
"-p".into(),
|
||||
cx.prompt.to_string(),
|
||||
"--output-format".into(),
|
||||
"stream-json".into(),
|
||||
"--verbose".into(),
|
||||
@@ -201,6 +205,10 @@ impl AgentAdapter for ClaudeCode {
|
||||
true
|
||||
}
|
||||
|
||||
fn prompt_on_stdin(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn check_tool_server(&self, stop: &CancelToken) -> Result<(), ToolError> {
|
||||
cli_can_start(craft_anything_platform::self_cli_path().as_deref(), stop)
|
||||
}
|
||||
@@ -211,15 +219,7 @@ impl AgentAdapter for ClaudeCode {
|
||||
return question_args(a, cx);
|
||||
}
|
||||
let mcp_json = json!({"mcpServers": {MCP_NAME: {"command": cx.mcp.command, "args": cx.mcp.args}}}).to_string();
|
||||
a.extend([
|
||||
"-p".into(),
|
||||
cx.prompt.to_string(),
|
||||
"--output-format".into(),
|
||||
"stream-json".into(),
|
||||
"--verbose".into(),
|
||||
"--append-system-prompt".into(),
|
||||
cx.preamble.to_string(),
|
||||
]);
|
||||
a.extend(["-p".into(), "--output-format".into(), "stream-json".into(), "--verbose".into(), "--append-system-prompt".into(), cx.preamble.to_string()]);
|
||||
// Opus 5.5 everywhere unless config.toml names another model on purpose; the effort
|
||||
// goes in through the environment (see `run_cli`).
|
||||
a.extend(["--model".into(), cx.options.cli_model.clone().unwrap_or_else(|| crate::anthropic::DEFAULT_MODEL.into())]);
|
||||
|
||||
@@ -551,6 +551,9 @@ fn run_cli(spec: &CliSpec, text: &str, tx: &Sender<AgentEvent>, stop: &Stop, ses
|
||||
if let Some(p) = spec.project.as_ref().filter(|_| remote.is_none()) {
|
||||
proc = proc.cwd(p);
|
||||
}
|
||||
if adapter.prompt_on_stdin() {
|
||||
proc = proc.stdin_bytes(text);
|
||||
}
|
||||
let result = proc.run();
|
||||
let p = parser.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if let Some(id) = &p.session_id {
|
||||
@@ -789,7 +792,8 @@ mod tests {
|
||||
fn claude_args() {
|
||||
let a = build_args(adapter_for("claude-code").as_ref(), &BackendOptions::default(), "-what?", "PRE", &mcp(), Some("sess-1"), Access::Analyse);
|
||||
assert_eq!(a[0], "-p");
|
||||
assert_eq!(a[1], " -what?");
|
||||
// #402: the message goes in on stdin, so a long one can't pass Windows' command-line limit.
|
||||
assert!(adapter_for("claude-code").prompt_on_stdin() && !a.iter().any(|x| x.contains("what?")), "{a:?}");
|
||||
let pos = |f: &str| a.iter().position(|x| x == f).unwrap();
|
||||
assert_eq!(a[pos("--output-format") + 1], "stream-json");
|
||||
assert!(a.contains(&"--verbose".to_string()));
|
||||
@@ -801,7 +805,7 @@ mod tests {
|
||||
assert_eq!(b[b.iter().position(|x| x == "--model").unwrap() + 1], "opus");
|
||||
let cfg: Value = serde_json::from_str(&a[pos("--mcp-config") + 1]).unwrap();
|
||||
assert_eq!(cfg["mcpServers"]["craft-anything"]["args"][0], "mcp");
|
||||
// variadic flags come last so the prompt is never swallowed
|
||||
// variadic flags come last: nothing after them is read as one of their values
|
||||
assert!(pos("--mcp-config") > pos("--resume"));
|
||||
}
|
||||
|
||||
@@ -1080,6 +1084,32 @@ mod tests {
|
||||
assert!(text.contains("Request"), "preamble is passed too");
|
||||
}
|
||||
|
||||
/// #402: a 34,641-character message in argv passed Windows' command-line limit (os error 206);
|
||||
/// Claude Code reads it from stdin instead.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn claude_code_reads_a_long_message_on_stdin() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let program = dir.path().join("claude");
|
||||
let reply = r#"{"type":"assistant","message":{"content":[{"type":"text","text":"read %s bytes"}]}}\n"#;
|
||||
std::fs::write(&program, format!("#!/bin/sh\nn=$(wc -c)\nprintf '{reply}' $n\n")).unwrap();
|
||||
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let config =
|
||||
BackendConfig { name: "Claude".into(), kind: BackendKind::AgentCli, model: "claude-code".into(), base_url: None, ssh_host: None, enabled: true };
|
||||
let options = BackendOptions { command: Some(program.display().to_string()), ..Default::default() };
|
||||
let mut c = CliConversation::new(CliSpec::new(config, options, false, None));
|
||||
c.send(&"a".repeat(40_000));
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(10);
|
||||
let mut all = Vec::new();
|
||||
while std::time::Instant::now() < deadline && !all.iter().any(|e| matches!(e, AgentEvent::TurnDone { .. })) {
|
||||
all.extend(c.poll());
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
let text: String = all.iter().filter_map(|e| if let AgentEvent::TextDelta { text } = e { Some(text.clone()) } else { None }).collect();
|
||||
assert_eq!(text, "read 40000 bytes", "{all:?}");
|
||||
}
|
||||
|
||||
/// #91: a Mac app with no CLI beside it reached Claude Code, which stopped with a stack trace.
|
||||
#[test]
|
||||
fn a_missing_or_broken_helper_is_said_plainly() {
|
||||
|
||||
@@ -555,6 +555,9 @@ Owner: open · Status: in progress
|
||||
manifest) skip the approval policy; `maki acp` takes no tool flags to stop it (0.7.0). Start
|
||||
it in `app/` (paths in the prompt relative to that), or ask Maki for a way to make those
|
||||
writes ask; part of the approval decision above
|
||||
- [ ] Codex, Gemini CLI, OpenCode, Hermes, Maki and the plain adapter still take the message in
|
||||
argv, so a long one can pass Windows' command-line limit as Claude Code's did (#402); hand
|
||||
each its message on stdin or in a file where its CLI reads one
|
||||
|
||||
### R2.3 Website coverage and a better visual metric
|
||||
Owner: open · Status: done
|
||||
|
||||
@@ -143,6 +143,11 @@ external tool a live test that checks it really produced output, not just that i
|
||||
It happened again with Explorer: project folders are kept as `D:/Projects/…\name`, and "Open
|
||||
folder" opened Documents. Paths for the system's programs go through `platform::native_path`.
|
||||
|
||||
**A message on the command line.** Claude Code was handed the whole task as `-p <message>`; a
|
||||
34,641-character brief passed Windows' 32,767-character command-line limit, so the build never
|
||||
started ("The filename or extension is too long", os error 206, #402). Give a program anything
|
||||
that can grow (messages, briefs, lists of files) on stdin or in a file, never in argv.
|
||||
|
||||
**Console windows popping up over the app.** Only the detached launcher hid the console; the
|
||||
supervised runner didn't, so Ghidra's `.bat` and PowerShell each opened a console window during a
|
||||
real Deep-mode run (the CLI never shows it: it already has a console). Every child started from
|
||||
|
||||
Reference in New Issue
Block a user