mirror of
https://github.com/instructkr/claw-code.git
synced 2026-11-09 00:00:01 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca48ec7500 | ||
|
|
ab8561dc37 | ||
|
|
747e160b59 | ||
|
|
6ac59ab90b | ||
|
|
67e5783894 | ||
|
|
db58b190bd |
No files matched your search
@@ -5,6 +5,12 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.11] - 2026-11-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- NotebookEdit no longer reuses an existing cell id for a newly inserted cell after a cell has been deleted, so later edits by id target the intended cell (#3371)
|
||||||
|
|
||||||
## [0.1.10] - 2026-11-02
|
## [0.1.10] - 2026-11-02
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -149,6 +149,48 @@ curl -sS "$OPENAI_BASE_URL/models" \
|
|||||||
|
|
||||||
For local configuration, see your gateway's documentation for the correct base URL, API key format, and available model IDs. The same OPENAI_BASE_URL + OPENAI_API_KEY pattern applies to any OpenAI-compatible remote gateway.
|
For local configuration, see your gateway's documentation for the correct base URL, API key format, and available model IDs. The same OPENAI_BASE_URL + OPENAI_API_KEY pattern applies to any OpenAI-compatible remote gateway.
|
||||||
|
|
||||||
|
## LLMTR
|
||||||
|
|
||||||
|
[LLMTR](https://llmtr.com) is an OpenAI-compatible gateway. Use the same client: set `OPENAI_BASE_URL` to `https://llmtr.com/v1` and `OPENAI_API_KEY` to a bearer key (`llmtr-...`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENAI_BASE_URL="https://llmtr.com/v1"
|
||||||
|
export OPENAI_API_KEY="llmtr-..."
|
||||||
|
claw --model "local/llmtr/qwen3-8-27b" prompt "Reply exactly HELLO_WORLD_123"
|
||||||
|
```
|
||||||
|
|
||||||
|
Send the catalog id unchanged. `llmtr/qwen3-8-27b` already starts with the gateway name, so that prefix stays on the wire. The `local/` hatch only selects the OpenAI-compatible transport and is removed before the request. A bare `qwen/` or `qwen-` name is a DashScope route and will not reach LLMTR, so vendor-prefixed ids need the same hatch: `local/<vendor>/<model>`.
|
||||||
|
|
||||||
|
Claw asks for up to 64,000 output tokens on these routes. LLMTR rejects a request whose `max_tokens` is above the model's output limit, so pick a model that allows at least that much. `llmtr/qwen3-8-27b` allows 262,144. Each model's limit is listed as `top_provider.max_completion_tokens` in the catalog below.
|
||||||
|
|
||||||
|
The public model list does not need a key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "https://llmtr.com/v1/models" \
|
||||||
|
| jq -r '.data[] | "\(.id)\t\(.top_provider.max_completion_tokens)"'
|
||||||
|
```
|
||||||
|
|
||||||
|
## UberLLM
|
||||||
|
|
||||||
|
[UberLLM](https://uberllm.dev) is a pay-as-you-go OpenAI-compatible gateway (prepaid, no free tier). Set `OPENAI_BASE_URL` to `https://api.uberllm.dev/v1` and `OPENAI_API_KEY` to a bearer key (`ull_...`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENAI_BASE_URL="https://api.uberllm.dev/v1"
|
||||||
|
export OPENAI_API_KEY="ull_..."
|
||||||
|
claw --model "local/deepseek/deepseek-v4-flash" prompt "Reply exactly HELLO_WORLD_123"
|
||||||
|
```
|
||||||
|
|
||||||
|
UberLLM ids are vendor-prefixed (`deepseek/deepseek-v4-flash`, `moonshotai/kimi-k2.6`, `qwen/qwen3.8-flash`). Put `local/` in front of every one: the hatch selects the OpenAI-compatible transport and is removed, so the catalog id goes out unchanged. Some prefixes would otherwise pick a native route; a bare `qwen/qwen3.8-flash`, for example, goes to DashScope and asks for `DASHSCOPE_API_KEY`.
|
||||||
|
|
||||||
|
Claw asks for up to 64,000 output tokens on these routes. UberLLM does not reject that on a model with a smaller output limit (for example `aion-labs/aion-3-0-mini`, 32,768); the answer is simply bounded by the model's limit, listed as `max_output_tokens` in the catalog below.
|
||||||
|
|
||||||
|
The public model list does not need a key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "https://api.uberllm.dev/v1/models" \
|
||||||
|
| jq -r '.data[] | "\(.id)\t\(.max_output_tokens)"'
|
||||||
|
```
|
||||||
|
|
||||||
## Local skills install from disk
|
## Local skills install from disk
|
||||||
|
|
||||||
Skills are discovered from Claw skill roots such as `.claw/skills/` in a workspace and `~/.claw/skills/` for user-level installs. Legacy `.codex/skills/` roots may also be scanned for compatibility, but new local Claw projects should prefer `.claw/skills/`.
|
Skills are discovered from Claw skill roots such as `.claw/skills/` in a workspace and `~/.claw/skills/` for user-level installs. Legacy `.codex/skills/` roots may also be scanned for compatibility, but new local Claw projects should prefer `.claw/skills/`.
|
||||||
|
|||||||
Generated
+11
-11
@@ -93,7 +93,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
@@ -478,7 +478,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "claw-analog"
|
name = "claw-analog"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"api",
|
"api",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -498,7 +498,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "claw-rag-service"
|
name = "claw-rag-service"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum 0.8.9",
|
"axum 0.8.9",
|
||||||
"blake3",
|
"blake3",
|
||||||
@@ -531,7 +531,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "commands"
|
name = "commands"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"plugins",
|
"plugins",
|
||||||
"runtime",
|
"runtime",
|
||||||
@@ -540,7 +540,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compat-harness"
|
name = "compat-harness"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"commands",
|
"commands",
|
||||||
"runtime",
|
"runtime",
|
||||||
@@ -1568,7 +1568,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mock-anthropic-service"
|
name = "mock-anthropic-service"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"api",
|
"api",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1761,7 +1761,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plugins"
|
name = "plugins"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2136,7 +2136,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "runtime"
|
name = "runtime"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"plugins",
|
"plugins",
|
||||||
@@ -2261,7 +2261,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusty-claude-cli"
|
name = "rusty-claude-cli"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"api",
|
"api",
|
||||||
"commands",
|
"commands",
|
||||||
@@ -2622,7 +2622,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telemetry"
|
name = "telemetry"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2902,7 +2902,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tools"
|
name = "tools"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"api",
|
"api",
|
||||||
"aspect-core",
|
"aspect-core",
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
publish = false
|
publish = false
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ async fn send_message_posts_json_and_parses_response() {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.headers.get("user-agent").map(String::as_str),
|
request.headers.get("user-agent").map(String::as_str),
|
||||||
Some("claude-code/0.1.10")
|
Some("claude-code/0.1.11")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.headers.get("anthropic-beta").map(String::as_str),
|
request.headers.get("anthropic-beta").map(String::as_str),
|
||||||
|
|||||||
@@ -5997,7 +5997,7 @@ fn execute_notebook_edit(input: NotebookEditInput) -> Result<NotebookEditOutput,
|
|||||||
NotebookEditMode::Insert => {
|
NotebookEditMode::Insert => {
|
||||||
let resolved_cell_type = resolved_cell_type
|
let resolved_cell_type = resolved_cell_type
|
||||||
.ok_or_else(|| String::from("insert mode requires a cell type"))?;
|
.ok_or_else(|| String::from("insert mode requires a cell type"))?;
|
||||||
let new_id = make_cell_id(cells.len());
|
let new_id = make_cell_id(cells);
|
||||||
let new_cell = build_notebook_cell(&new_id, resolved_cell_type, &new_source);
|
let new_cell = build_notebook_cell(&new_id, resolved_cell_type, &new_source);
|
||||||
let insert_at = target_index.map_or(cells.len(), |index| index + 1);
|
let insert_at = target_index.map_or(cells.len(), |index| index + 1);
|
||||||
cells.insert(insert_at, new_cell);
|
cells.insert(insert_at, new_cell);
|
||||||
@@ -7022,8 +7022,11 @@ fn format_notebook_edit_mode(mode: NotebookEditMode) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_cell_id(index: usize) -> String {
|
fn make_cell_id(cells: &[Value]) -> String {
|
||||||
format!("cell-{}", index + 1)
|
(cells.len() + 1..)
|
||||||
|
.map(|n| format!("cell-{n}"))
|
||||||
|
.find(|id| !cells.iter().any(|cell| cell["id"] == id.as_str()))
|
||||||
|
.expect("there is always a free id")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_skill_description(contents: &str) -> Option<String> {
|
fn parse_skill_description(contents: &str) -> Option<String> {
|
||||||
@@ -9869,11 +9872,23 @@ mod tests {
|
|||||||
assert!(deleted_output["cell_type"].is_null());
|
assert!(deleted_output["cell_type"].is_null());
|
||||||
assert_eq!(deleted_output["new_source"], "");
|
assert_eq!(deleted_output["new_source"], "");
|
||||||
|
|
||||||
|
let reinserted = execute_tool(
|
||||||
|
"NotebookEdit",
|
||||||
|
&json!({
|
||||||
|
"notebook_path": path.display().to_string(),
|
||||||
|
"new_source": "print(4)\n",
|
||||||
|
"edit_mode": "insert"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("NotebookEdit insert after delete should succeed");
|
||||||
|
let reinserted_output: serde_json::Value = serde_json::from_str(&reinserted).expect("json");
|
||||||
|
assert_eq!(reinserted_output["cell_id"], "cell-4");
|
||||||
|
|
||||||
let final_notebook: serde_json::Value =
|
let final_notebook: serde_json::Value =
|
||||||
serde_json::from_str(&std::fs::read_to_string(&path).expect("read notebook"))
|
serde_json::from_str(&std::fs::read_to_string(&path).expect("read notebook"))
|
||||||
.expect("valid notebook json");
|
.expect("valid notebook json");
|
||||||
let cells = final_notebook["cells"].as_array().expect("cells array");
|
let cells = final_notebook["cells"].as_array().expect("cells array");
|
||||||
assert_eq!(cells.len(), 2);
|
assert_eq!(cells.len(), 3);
|
||||||
assert_eq!(cells[0]["cell_type"], "markdown");
|
assert_eq!(cells[0]["cell_type"], "markdown");
|
||||||
assert!(cells[0].get("outputs").is_none());
|
assert!(cells[0].get("outputs").is_none());
|
||||||
assert_eq!(cells[1]["cell_type"], "code");
|
assert_eq!(cells[1]["cell_type"], "code");
|
||||||
@@ -9881,6 +9896,94 @@ mod tests {
|
|||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn notebook_edit_replace_switches_cell_type_and_rejects_unknown_ids() {
|
||||||
|
let path = temp_path("retype.ipynb");
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r##"{
|
||||||
|
"cells": [
|
||||||
|
{"cell_type": "markdown", "id": "cell-2", "metadata": {}, "source": ["# title\n"]}
|
||||||
|
],
|
||||||
|
"metadata": {},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}"##,
|
||||||
|
)
|
||||||
|
.expect("write notebook");
|
||||||
|
|
||||||
|
let kept_type = execute_tool(
|
||||||
|
"NotebookEdit",
|
||||||
|
&json!({
|
||||||
|
"notebook_path": path.display().to_string(),
|
||||||
|
"cell_id": "cell-2",
|
||||||
|
"new_source": "# renamed\n"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("NotebookEdit default replace should succeed");
|
||||||
|
let kept_output: serde_json::Value = serde_json::from_str(&kept_type).expect("json");
|
||||||
|
assert_eq!(kept_output["edit_mode"], "replace");
|
||||||
|
assert_eq!(kept_output["cell_type"], "markdown");
|
||||||
|
assert_eq!(kept_output["language"], "python");
|
||||||
|
|
||||||
|
let retyped = execute_tool(
|
||||||
|
"NotebookEdit",
|
||||||
|
&json!({
|
||||||
|
"notebook_path": path.display().to_string(),
|
||||||
|
"cell_id": "cell-2",
|
||||||
|
"new_source": "print(1)\n",
|
||||||
|
"cell_type": "code",
|
||||||
|
"edit_mode": "replace"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("NotebookEdit replace to code should succeed");
|
||||||
|
let retyped_output: serde_json::Value = serde_json::from_str(&retyped).expect("json");
|
||||||
|
assert_eq!(retyped_output["cell_type"], "code");
|
||||||
|
|
||||||
|
let inserted = execute_tool(
|
||||||
|
"NotebookEdit",
|
||||||
|
&json!({
|
||||||
|
"notebook_path": path.display().to_string(),
|
||||||
|
"new_source": "print(2)\n",
|
||||||
|
"edit_mode": "insert"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect("NotebookEdit insert should succeed");
|
||||||
|
let inserted_output: serde_json::Value = serde_json::from_str(&inserted).expect("json");
|
||||||
|
assert_eq!(inserted_output["cell_id"], "cell-3");
|
||||||
|
|
||||||
|
let before_missing = std::fs::read_to_string(&path).expect("read notebook");
|
||||||
|
let missing = execute_tool(
|
||||||
|
"NotebookEdit",
|
||||||
|
&json!({
|
||||||
|
"notebook_path": path.display().to_string(),
|
||||||
|
"cell_id": "missing",
|
||||||
|
"new_source": "print(3)\n",
|
||||||
|
"edit_mode": "replace"
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.expect_err("unknown cell id should fail");
|
||||||
|
assert!(missing.contains("Cell id not found: missing"));
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(&path).expect("read notebook"),
|
||||||
|
before_missing
|
||||||
|
);
|
||||||
|
|
||||||
|
let final_notebook: serde_json::Value =
|
||||||
|
serde_json::from_str(&before_missing).expect("valid notebook json");
|
||||||
|
let cells = final_notebook["cells"].as_array().expect("cells array");
|
||||||
|
assert_eq!(cells.len(), 2);
|
||||||
|
assert_eq!(cells[0]["cell_type"], "code");
|
||||||
|
assert_eq!(cells[0]["source"], json!(["print(1)\n"]));
|
||||||
|
assert_eq!(cells[0]["outputs"], json!([]));
|
||||||
|
assert_eq!(
|
||||||
|
cells[0].get("execution_count"),
|
||||||
|
Some(&serde_json::Value::Null)
|
||||||
|
);
|
||||||
|
assert_eq!(cells[1]["id"], "cell-3");
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn notebook_edit_rejects_invalid_inputs() {
|
fn notebook_edit_rejects_invalid_inputs() {
|
||||||
let text_path = temp_path("notebook.txt");
|
let text_path = temp_path("notebook.txt");
|
||||||
|
|||||||
Reference in new issue
Block a user