尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Nine small command-line tools for AI agents, written in cancho: seek, list, peek, jsonq, tally, hash, write, replace, move. JSON output, errors that name a rule, and a compiler-proven authority per binary.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Ten small tools that AI agents and scripts can rely on, and one opt-in. seek, list, peek, jsonq, tally, hash, write, replace, move and archive do the everyday work of grep, ls/find, sed -n, jq, sort | uniq -c, sha256sum, a careful cp/mv and tar/gzip, and run starts a program from a directory you chose, with explicit semantics, bounded resources, tagged refusals that often carry a repair, and a checkable authority row per binary. Every answer is JSON against a published schema, and every tool describes itself (introspect, skill); an MCP server serves them to an agent runtime from that same description. Written in cancho. The project page has one page per tool.
Status: early, ten tools and one opt-in built and tested. Not claimed: that agents do better with these tools than with the incumbents. That needs an agent-in-the-loop evaluation (cancho#228) that has not been run. What is claimed is what the conformance gates check. What is missing is listed under "What it cannot do".
| Tool | Instead of | Does |
|---|---|---|
seek | grep -F -n -b | literal search over files, a stream of matches |
list | ls, find, tree | the entries beneath a directory, sorted, depth-bounded, links never followed |
peek | sed -n, head, tail, wc -l | a range of lines or bytes, with numbers and a cursor |
jsonq | jq -c | one value from one strict JSON document, by RFC 6901 pointer |
tally | sort | uniq -c | sort -rn | distinct lines or fields, counted and ranked |
hash | sha256sum, sha512sum | file digests of any size, --verify |
write | cp, >, tee | replace a file atomically, only if it holds what the caller says |
replace | sed -i | replace exact text an expected number of times, atomically |
move | mv | rename a file or directory in place, never over an existing name |
archive | tar, gzip, unzip, zstd | list, extract and create tar archives, gzip- or zstd-compressed or not, and zips; read bzip2 and xz; checked whole before any file is written (docs/archive.md) |
run | exec, timeout | opt-in: start one program from the deployer's directory with exact arguments, a fixed environment, a deadline and an output cap (docs/next-tools.md section 3) |
Two agents read notes.txt and each adds a line. With plain writes the second write wins and agent A's line is gone, and nobody is told:
$ printf 'shopping list\n- milk (agent A)\n' > notes.txt # agent A
$ printf 'shopping list\n- bread (agent B)\n' > notes.txt # agent B
$ cat notes.txt
shopping list
- bread (agent B)
With guarded writes (write --if-sha256) the stale write is refused, and the refusal carries what the agent needs to retry (selected fields of each JSON line):
$ hash --root . notes.txt # A and B both read it: H1 is its hash
{"type":"hash","path":"notes.txt","algo":"sha256","hex":"936bb8ff57a0…","bytes":14}
$ ... | write --root . --if-sha256 H1 --stdin notes.txt # agent A adds its line
{"path":"notes.txt","changed":true,"bytes":31,"after_sha256":"7bde7db5f30a…"}
$ ... | write --root . --if-sha256 H1 --stdin notes.txt # agent B, stale
{"rule":"precondition.hash-mismatch","hint":"re-read the file, then decide","repair":{"kind":"none","reason":"the file changed since it was read; re-read, then decide"},"detail":{"path":"notes.txt","expected_sha256":"936bb8ff57a0…","actual_sha256":"7bde7db5f30a…","size":31}}
# exit status 5
$ ... | write --root . --if-sha256 H2 --stdin notes.txt # agent B re-reads (H2 came with the refusal), re-applies, retries
{"path":"notes.txt","changed":true,"bytes":49,"after_sha256":"60e7843a980c…"}
$ cat notes.txt
shopping list
- milk (agent A)
- bread (agent B)
The full transcript, with every JSON line as printed, is in docs/reference.md.
grep, sed and jq were made for a person at a terminal, who reads the prose and retries. An agent has to parse text, often cannot see stderr, and decides its next step from what came back. Each bullet is a property the gates check, with where to see it.
schemas/). A path, a line number and a byte offset are fields; bytes that are not UTF-8 come back as {"b64": …}, never replaced.end record, so a stream without one was cut short. A capped answer says truncated: true and gives a next cursor to resume exactly there.{code, rule, message, hint, repair, detail}; match on rule. When the fix is mechanical, repair is an argv to run as it stands, and a repair never widens what the tool may touch. seek introspect lists the rules and exit codes.write needs --create or --if-sha256 of what it read, and replace needs the text to occur --expect times: a stale view is precondition.hash-mismatch, not a lost edit. Writes are atomic, locked and idempotent, and --dry-run shows the diff.--root. .., absolute and outside paths are refused before anything is opened, and no link below the root is followed (path.symlink).--max-line-bytes: about 1.7 MB whether the file is 1 MiB or 256 MiB (reference).introspect prints the flags, rules, exit codes, limits, schema and guarantees as JSON; skill prints a guide for an agent. Both come from the tables the parser runs on, so they cannot drift.tools.toml.You need git, Rust and clang. On a 16-core Mac with Rust installed and its dependencies already downloaded, the compiler built in 17 seconds and the first nine tools in 7; the clones and a first-time dependency download are extra, so allow a few minutes the first time.
git clone https://github.com/alpibrusl/cancho
git clone https://github.com/alpibrusl/cancho-tools && cd cancho-tools
REV=$(sed -n 's/^cancho *= *"\([0-9a-f]*\)".*/\1/p' cancho.toml) # the compiler these sources need
(cd ../cancho && git fetch -q origin && git checkout "$REV" && cargo build --release -p cancho)
export PATH=$PWD/../cancho/target/release:$PWD/build:$PATH
cancho build # every tool into build/
seek --root . --format text 'fn main' tools/seek/seek.cho # try one
seek introspect # flags, rules, limits, schema, authority (JSON)
One real command per tool, on a small project (src/main.rs, package.json, access.log). Every tool takes --root .: paths are relative to it and nothing outside can be reached. The output is what the built tools print, with long hashes shortened.
seek: search files for literal text.
$ seek --root . add src/main.rs
{"type":"match","path":"src/main.rs","line":2,"offset":12,"text":" let total = add(2, 3);"}
{"type":"match","path":"src/main.rs","line":6,"offset":69,"text":"fn add(a: i32, b: i32) -> i32 {"}
{"type":"file","path":"src/main.rs","matches":2,"bytes":113,"binary":false,"complete":true}
{"type":"end","ok":true,"command":"seek","schema":"seek.v1","complete":true,"files":1,"matches":2,"errors":0,"truncated":false,"next":null}
list: the entries beneath a directory.
$ list --root . --depth 2 --format text
access.log
package.json
src/
src/main.rs
peek: a range of lines, with numbers.
$ peek --root . --lines 2:3 --format text src/main.rs
2 let total = add(2, 3);
3 println!("{}", total);
jsonq: one value from a JSON document.
$ jsonq --root . -p /deps/serde package.json
{"ok":true,"command":"jsonq","schema":"jsonq.v1","data":{"pointer":"/deps/serde","kind":"string","value":"1.0"},"meta":{"version":"0.1.0"}}
tally: count and rank a field.
$ tally --root . --field 3 --delim ' ' --format text access.log
3 200
1 404
1 500
hash: a file's SHA-256.
$ hash --root . package.json
{"type":"hash","path":"package.json","algo":"sha256","hex":"2c313334b372…","bytes":78}
{"type":"end","ok":true,"command":"hash","schema":"hash.v1","complete":true,"files":1,"errors":0}
write: create a file, or replace it only if it is what you think.
$ printf 'hello\n' | write --root . --create --stdin NOTES.md
{"ok":true,"command":"write","schema":"write.v1","data":{"path":"NOTES.md","changed":true,"created":true,"bytes":6,"before_sha256":null,"after_sha256":"5891b5b522d5…","diff":[{"old_start":1,"new_start":1,"removed_count":0,"added_count":1,"removed":[],"added":["hello\n"]}],"diff_truncated":false},"meta":{"version":"0.1.0"}}
replace: change exact text, here as a dry run.
$ replace --root . --old 'a + b' --new 'a.wrapping_add(b)' --dry-run src/main.rs
{"ok":true,"command":"replace","schema":"replace.v1","data":{"path":"src/main.rs","changed":false,"replacements":1,"bytes":125,"before_sha256":"98ace4483c48…","after_sha256":"63261420d975…","diff":[{"old_start":7,"new_start":7,"removed_count":1,"added_count":1,"removed":[" a + b\n"],"added":[" a.wrapping_add(b)\n"]}],"diff_truncated":false},"dry_run":true,"planned_actions":[{"op":"replace","path":"src/main.rs","replacements":1,"before_sha256":"98ace4483c48…","after_sha256":"63261420d975…","bytes":125,"diff":[{"old_start":7,"new_start":7,"removed_count":1,"added_count":1,"removed":[" a + b\n"],"added":[" a.wrapping_add(b)\n"]}],"diff_truncated":false}],"meta":{"version":"0.1.0"}}
# exit status 9
move: rename in place, never over an existing name.
$ move --root . --dry-run NOTES.md NOTES-2026.md
{"ok":true,"command":"move","schema":"move.v1","data":{"path":"NOTES.md","to":"NOTES-2026.md","changed":false,"kind":"file","sha256":null},"dry_run":true,"planned_actions":[{"op":"move","path":"NOTES.md","to":"NOTES-2026.md","kind":"file","sha256":null}],"meta":{"version":"0.1.0"}}
# exit status 9
archive: a tarball, byte for byte the same every time, here as a dry run.
$ archive --root . --op create --dry-run project.tgz src package.json
{"type":"dir","path":"src"}
{"type":"file","path":"src/main.rs","bytes":113,"sha256":"98ace4483c48…"}
{"type":"file","path":"package.json","bytes":78,"sha256":"2c313334b372…"}
{"type":"end","ok":true,"command":"archive","schema":"archive.v1","complete":true,"op":"create","format":"tgz","entries":3,"bytes":191,"errors":0,"dry_run":true,"archive_sha256":"372e8efb591b…"}
# exit status 9
Flags, more examples, and each tool's rules are in docs/reference.md.
server/mcp.cho serves the ten default tools over MCP (run only if the server is built with --with run) (stdio, protocol 2025-06-18). tools/list is generated from each tool's introspect, and a tools/call runs the binary and answers with its output byte for byte. It may start the binaries in the one directory it was built with, and nothing else; --root is the server's, never the model's.
python3 scripts/mcp.py build --bin "$PWD/build" --out build/mcp
build/mcp --root /path/to/workspace
Design and measurements: docs/mcp.md.
What "designed for agents" means here, what a test checks on every binary, and what is not checked: docs/agent-first.md.
move --remove --if-sha256 HEX renames it to a tombstone in the same directory, and move NAME.removed-… NAME undoes it.curl or ssh: a network tier would be a separate project with its own authority ceiling, docs/archive.md section 9). seek is literal text; jsonq takes pointers, not jq filters.move takes a new name, not a new directory.seek takes named files, not a directory: list then seek is the pattern for now.run, which is opt-in, holds exec for its programs directory and a clock for its deadline, and says it is irreversible-consequential. No other tool's authority holds them (tools.toml).archive holds the archive in memory, up to --max-bytes, so its memory is that bound and not a constant. extract makes the directories an entry needs beneath --into, never --into itself (docs/archive.md section 13). It does not write bzip2 or xz.The list of open work is in issues.
Supporting evidence: these tools are not built to win on speed, and mostly do not lose. A 64 MiB text file, through a pipe, minimum of 7 interleaved rounds, Linux x86-64 on a shared machine; speed is reported, not gated.
| Command | cancho-tools | Incumbent |
|---|---|---|
seek gamma (about a million matches) | 0.49 s | grep -F -n -b 0.51 s, rg 0.39 s |
seek with no match | 0.04 s | grep -F -n -b 0.04 s |
hash (SHA-256) | 0.52 s | sha256sum 0.17 s |
peek --count-lines --lines 1:100 | 0.08 s | sed -n 1,100p; wc -l 0.13 s |
tally --field 1 | 0.16 s | cut | sort | uniq -c | sort | head 0.68 s |
jsonq -p /0, 16 MiB array | 0.14 s | jq -c .[0] 0.73 s |
list, one directory of 100,000 files | 0.144 s | find -printf 0.146 s, ls -1 0.154 s |
sha256sum's lead is OpenSSL's hand-written assembly, which cancho has no way to express. More rows (seek -i is 0.58 s against 0.52 s), startup and --root cost: docs/reference.md. Measure on your machine: python3 scripts/toolbench.py --tool seek --size 64MiB.
| project page | one page per tool, generated from the tool itself: flags, rules, exit codes, authority, JSON schemas and a ready-made SKILL.md |
| docs/reference.md | every tool with its examples, the error protocol, confinement, authority, memory, performance, the package, development |
| docs/mcp.md | the MCP server: design and measurements |
| docs/history.md | how this was built, what it found in the compiler, the performance rounds |
cancho's docs/agent-toolbox.md | the design (epic cancho#214) |
| cancho-table | a tenth tool, built outside this repository on its contract package |
Every change goes through what CI runs: cancho fmt --check contract tools tests generated server, cancho build, cancho test, scripts/schemas.py --check, scripts/manifest.py --check, scripts/demo_race.py --check, scripts/package.py --check, scripts/mcp.py --check, scripts/cli_mutants.py --check (the mutants of the table scans), the conformance tests. A change that needs a new effect fails until tools.toml allows it. After changing a tool, run python3 scripts/manifest.py and cancho build; after changing what a tool prints, python3 scripts/demo_race.py --update-readme rewrites the generated sections of this file. Layout, the gates and how to build more tools on the contract package: docs/reference.md.