Compare commits

..
2 Commits
Author SHA1 Message Date
Bellman 122cf3ab0c Merge pull request #3337 from ultraworkers/release/v0.1.4
docs: add CHANGELOG for v0.1.4 release
2026-10-13 09:05:47 +09:00
clawdbot eb1537ca92 docs: add CHANGELOG for v0.1.4 release 2026-10-13 00:03:23 +00:00
10 changed files with 59 additions and 154 deletions
+38
View File
@@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.4] - 2026-10-13
### Added
- GJC-compatible line range selectors for read_file tool (`:50-200`, `:50+150` syntax)
- GJC-compatible `:conflicts` selector for git merge conflict detection
- SDK wire transport with WebSocket support
- Nix flake development environment
- `/status`, `/cost`, and `/version` slash commands
- Persistent provider configuration loading
- Pagination support for large read_file results
- Comprehensive GJC compatibility documentation and analysis
### Fixed
- Windows path containment checks for security boundary enforcement
- Git bash detection on Windows with proper probe caching
- Unresolvable path denial to prevent escape attempts
- Auto-compaction context window measurement
- Tool result preservation on failed turns
- Model stall recovery without tool calls
- Streaming markdown formatting with preserved block separators
- Local model validation bypass generalization beyond Ollama
- Release artifact checksums on macOS and Linux
- Duplicate assistant text rendering
- Workspace slash command wire-up
### Changed
- Enhanced path scope validation for multiple Windows path spellings
- Improved error handling and configuration defaults
- Upgraded GitHub Actions to immutable commit SHAs for CI security
### Known Limitations
- ACP / Zed integration is not yet implemented (status stub available via `claw acp`)
- Python porting workspace is for reference only, not used in production
## [0.1.3] - 2026-10-13
This is the first public binary release of Claw Code.
+1 -1
View File
@@ -41,7 +41,7 @@ TUI rule: formatting fns take `&mut impl Write`, never stdout directly. Never mi
Library crates don't carry the `claw-` prefix. Binary crates do (except legacy `rusty-claude-cli`).
Workspace version is `0.1.3`, `publish = false`, MIT license.
Workspace version is `0.1.4`, `publish = false`, MIT license.
No rust-toolchain file, no MSRV. CI pins `dtolnay/rust-toolchain@stable`.
+12 -12
View File
@@ -93,7 +93,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "api"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"criterion",
"reqwest",
@@ -478,7 +478,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "claw-analog"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"api",
"clap",
@@ -498,7 +498,7 @@ dependencies = [
[[package]]
name = "claw-rag-service"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"axum 0.8.9",
"blake3",
@@ -531,7 +531,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "commands"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"plugins",
"runtime",
@@ -540,7 +540,7 @@ dependencies = [
[[package]]
name = "compat-harness"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"commands",
"runtime",
@@ -1568,7 +1568,7 @@ dependencies = [
[[package]]
name = "mock-anthropic-service"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"api",
"serde_json",
@@ -1761,7 +1761,7 @@ dependencies = [
[[package]]
name = "plugins"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"serde",
"serde_json",
@@ -2136,7 +2136,7 @@ dependencies = [
[[package]]
name = "runtime"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"glob",
"plugins",
@@ -2261,7 +2261,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rusty-claude-cli"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"api",
"commands",
@@ -2333,7 +2333,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdk"
version = "0.1.0"
version = "0.1.4"
dependencies = [
"futures-util",
"serde",
@@ -2619,7 +2619,7 @@ dependencies = [
[[package]]
name = "telemetry"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"serde",
"serde_json",
@@ -2906,7 +2906,7 @@ dependencies = [
[[package]]
name = "tools"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"api",
"aspect-core",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.1.3"
version = "0.1.4"
edition = "2021"
license = "MIT"
publish = false
+3 -58
View File
@@ -15,15 +15,13 @@ pub enum ProviderClient {
/// Provider selected explicitly by persisted/runtime configuration.
///
/// This is intentionally separate from `ProviderKind`: Custom OpenAI and
/// DashScope speak the OpenAI-compatible wire protocol but remain distinct
/// configured providers.
/// This is intentionally separate from `ProviderKind`: DashScope speaks the
/// OpenAI-compatible wire protocol but remains a distinct configured provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderConfigKind {
Anthropic,
Xai,
OpenAi,
CustomOpenAi,
DashScope,
}
@@ -98,12 +96,9 @@ impl ProviderClient {
Ok(Self::Xai(client))
}
ProviderConfigKind::OpenAi
| ProviderConfigKind::CustomOpenAi
| ProviderConfigKind::DashScope => {
ProviderConfigKind::OpenAi | ProviderConfigKind::DashScope => {
let compat = match config.kind {
ProviderConfigKind::OpenAi => OpenAiCompatConfig::openai(),
ProviderConfigKind::CustomOpenAi => OpenAiCompatConfig::custom_openai(),
ProviderConfigKind::DashScope => OpenAiCompatConfig::dashscope(),
_ => unreachable!("non-OpenAI provider reached compatibility path"),
};
@@ -408,56 +403,6 @@ mod tests {
}
}
#[test]
fn persisted_custom_openai_uses_its_own_environment_namespace() {
let _lock = env_lock();
let _custom_key = EnvVarGuard::set("CLAWCUSTOMOPENAI_API_KEY", Some("custom-key"));
let _custom_url = EnvVarGuard::set(
"CLAWCUSTOMOPENAI_BASE_URL",
Some("https://custom.example/v1"),
);
let _openai_key = EnvVarGuard::set("OPENAI_API_KEY", Some("openai-key"));
let _openai_url = EnvVarGuard::set("OPENAI_BASE_URL", Some("https://openai.example/v1"));
let _ollama = EnvVarGuard::set("OLLAMA_HOST", Some("http://localhost:11434"));
let config = ProviderConfig {
kind: ProviderConfigKind::CustomOpenAi,
model: "qwen-plus".to_string(),
api_key: None,
base_url: None,
};
match ProviderClient::from_config(&config).expect("custom OpenAI config should succeed") {
ProviderClient::OpenAi(client) => {
assert_eq!(client.base_url(), "https://custom.example/v1");
}
other => panic!("Expected custom OpenAI provider, got: {other:?}"),
}
}
#[test]
fn persisted_custom_openai_does_not_reuse_openai_credentials() {
let _lock = env_lock();
let _custom_key = EnvVarGuard::set("CLAWCUSTOMOPENAI_API_KEY", None);
let _custom_url = EnvVarGuard::set(
"CLAWCUSTOMOPENAI_BASE_URL",
Some("https://custom.example/v1"),
);
let _openai_key = EnvVarGuard::set("OPENAI_API_KEY", Some("openai-key"));
let _ollama = EnvVarGuard::set("OLLAMA_HOST", Some("http://localhost:11434"));
let config = ProviderConfig {
kind: ProviderConfigKind::CustomOpenAi,
model: "gpt-5".to_string(),
api_key: None,
base_url: None,
};
let error = ProviderClient::from_config(&config)
.expect_err("custom OpenAI must require its own credentials");
assert!(error.to_string().contains("CLAWCUSTOMOPENAI_API_KEY"));
}
#[test]
fn dashscope_model_uses_dashscope_config_not_openai() {
// Regression: qwen-plus was being routed to OpenAiCompatConfig::openai()
@@ -42,7 +42,6 @@ pub struct OpenAiCompatConfig {
const XAI_ENV_VARS: &[&str] = &["XAI_API_KEY"];
const OPENAI_ENV_VARS: &[&str] = &["OPENAI_API_KEY"];
const CUSTOM_OPENAI_ENV_VARS: &[&str] = &["CLAWCUSTOMOPENAI_API_KEY"];
const DASHSCOPE_ENV_VARS: &[&str] = &["DASHSCOPE_API_KEY"];
// Provider-specific request body size limits in bytes
@@ -81,18 +80,6 @@ impl OpenAiCompatConfig {
}
}
/// OpenAI-compatible endpoint with its own environment namespace and no default URL.
#[must_use]
pub const fn custom_openai() -> Self {
Self {
provider_name: "Custom OpenAI",
api_key_env: "CLAWCUSTOMOPENAI_API_KEY",
base_url_env: "CLAWCUSTOMOPENAI_BASE_URL",
default_base_url: "",
max_request_body_bytes: OPENAI_MAX_REQUEST_BODY_BYTES,
}
}
/// Alibaba `DashScope` compatible-mode endpoint (Qwen family models).
/// Uses the OpenAI-compatible REST shape at /compatible-mode/v1.
/// Requested via Discord #clawcode-get-help: native Alibaba API for
@@ -113,7 +100,6 @@ impl OpenAiCompatConfig {
match self.provider_name {
"xAI" => XAI_ENV_VARS,
"OpenAI" => OPENAI_ENV_VARS,
"Custom OpenAI" => CUSTOM_OPENAI_ENV_VARS,
"DashScope" => DASHSCOPE_ENV_VARS,
_ => &[],
}
+1 -1
View File
@@ -82,7 +82,7 @@ async fn send_message_posts_json_and_parses_response() {
);
assert_eq!(
request.headers.get("user-agent").map(String::as_str),
Some("claude-code/0.1.3")
Some("claude-code/0.1.4")
);
assert_eq!(
request.headers.get("anthropic-beta").map(String::as_str),
-56
View File
@@ -12910,7 +12910,6 @@ fn provider_config_from_runtime(
"anthropic" => api::ProviderConfigKind::Anthropic,
"xai" => api::ProviderConfigKind::Xai,
"openai" => api::ProviderConfigKind::OpenAi,
"custom-openai" => api::ProviderConfigKind::CustomOpenAi,
"dashscope" => api::ProviderConfigKind::DashScope,
other => {
return Err(format!(
@@ -12948,7 +12947,6 @@ fn explicit_provider_kind_for_model(
Some("anthropic") => Some(api::ProviderConfigKind::Anthropic),
Some("xai") => Some(api::ProviderConfigKind::Xai),
Some("openai") | Some("local") => Some(api::ProviderConfigKind::OpenAi),
Some("custom-openai") => Some(api::ProviderConfigKind::CustomOpenAi),
Some("dashscope") | Some("qwen") | Some("kimi") => Some(api::ProviderConfigKind::DashScope),
_ => {
let lower = resolved_model.to_ascii_lowercase();
@@ -19666,22 +19664,6 @@ UU conflicted.rs",
super::explicit_provider_kind_for_model("openai/gpt-5", &config.model),
Some(api::ProviderConfigKind::OpenAi)
);
assert_eq!(
super::explicit_provider_kind_for_model("custom-openai/local-model", &config.model),
Some(api::ProviderConfigKind::CustomOpenAi)
);
assert_eq!(
super::explicit_provider_kind_for_model("CUSTOM-OPENAI/local-model", &config.model),
Some(api::ProviderConfigKind::CustomOpenAi)
);
assert_eq!(
super::explicit_provider_kind_for_model(
"custom-openai/local-model",
"custom-openai/local-model"
),
None,
"matching custom model must keep the persisted provider kind"
);
assert_eq!(
super::explicit_provider_kind_for_model("qwen-plus", &config.model),
None,
@@ -19722,44 +19704,6 @@ UU conflicted.rs",
}
}
#[test]
fn persisted_custom_openai_configuration_survives_runtime_boundary() {
let config_home = temp_dir();
let workspace = temp_dir();
fs::create_dir_all(&config_home).expect("config home");
fs::create_dir_all(&workspace).expect("workspace");
fs::write(
config_home.join("settings.json"),
r#"{
"provider": {
"kind": "custom-openai",
"apiKey": "custom-test-key",
"baseUrl": "https://custom.example/v1",
"model": "local-model"
},
"model": "local-model"
}"#,
)
.expect("write provider settings");
let loader = ConfigLoader::new(&workspace, &config_home);
let runtime_config = loader.load().expect("runtime config should load");
let provider_config = super::provider_config_from_runtime(runtime_config.provider())
.expect("custom provider configuration should adapt")
.expect("persisted provider configuration should be present");
assert_eq!(provider_config.kind, api::ProviderConfigKind::CustomOpenAi);
assert_eq!(provider_config.model, "local-model");
assert_eq!(provider_config.api_key.as_deref(), Some("custom-test-key"));
assert_eq!(
provider_config.base_url.as_deref(),
Some("https://custom.example/v1")
);
let _ = fs::remove_dir_all(config_home);
let _ = fs::remove_dir_all(workspace);
}
#[test]
fn persisted_provider_configuration_controls_runtime_provider_dispatch() {
let config_home = temp_dir();
@@ -9,7 +9,7 @@ const PROVIDERS: &[(&str, &str, &str)] = &[
("2", "xAI / Grok", "xai"),
("3", "OpenAI", "openai"),
("4", "DashScope (Qwen/Kimi)", "dashscope"),
("5", "Custom (OpenAI-compat)", "custom-openai"),
("5", "Custom (OpenAI-compat)", "openai"),
];
const PROVIDER_MODELS: &[(&str, &[&str])] = &[
@@ -27,8 +27,6 @@ const DEFAULT_BASE_URLS: &[(&str, &str)] = &[
"dashscope",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
),
// Custom OpenAI has no default; user must supply.
("custom-openai", ""),
];
const API_KEY_ENV_VARS: &[(&str, &str)] = &[
@@ -36,7 +34,6 @@ const API_KEY_ENV_VARS: &[(&str, &str)] = &[
("xai", "XAI_API_KEY"),
("openai", "OPENAI_API_KEY"),
("dashscope", "DASHSCOPE_API_KEY"),
("custom-openai", "CLAWCUSTOMOPENAI_API_KEY"),
];
pub fn run_setup_wizard() -> Result<(), Box<dyn std::error::Error>> {
@@ -180,7 +177,6 @@ fn prompt_base_url(
"xai" => "XAI_BASE_URL",
"openai" => "OPENAI_BASE_URL",
"dashscope" => "DASHSCOPE_BASE_URL",
"custom-openai" => "CLAWCUSTOMOPENAI_BASE_URL",
_ => "BASE_URL",
};
let env_set = std::env::var(env_var).ok().is_some_and(|v| !v.is_empty());
@@ -218,11 +214,7 @@ fn prompt_model(
if !aliases.is_empty() {
println!(" Common: {}", aliases.join(", "));
}
if kind == "custom-openai" {
println!(" Or enter any model name (e.g. custom/gpt-4.1 for custom routing)");
} else {
println!(" Or enter any model name (e.g. openai/gpt-4.1-mini for custom routing)");
}
println!(" Or enter any model name (e.g. openai/gpt-4.1-mini for custom routing)");
let input = read_line(&format!(" Model [{current_model}]: "))?;
if input.trim().is_empty() {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sdk"
version = "0.1.0"
version = "0.1.4"
edition = "2021"
publish = false