mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-03 23:54:49 +08:00
Compare commits
1 Commits
feat/relea
...
feat/uiux-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
864a9124fc |
38
README.md
38
README.md
@@ -33,6 +33,38 @@
|
||||
|
||||
---
|
||||
|
||||
## Built with oh-my-opencode
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-openagent">
|
||||
<img src="https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/.github/assets/omo.png" width="600" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-openagent"><strong>oh-my-opencode</strong></a> — the agent orchestration layer that makes AI coding actually work.
|
||||
<br />
|
||||
<em>Sisyphus doesn't stop until the task is done. Every test passes. Every review clears.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/code-yeongyu/oh-my-openagent"><img src="https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=for-the-badge&logo=github" /></a>
|
||||
<a href="https://www.npmjs.com/package/oh-my-opencode"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fohmyopenagent.com%2Fapi%2Fnpm-downloads&style=for-the-badge" /></a>
|
||||
<a href="https://discord.gg/PUwSMR9XNk"><img src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge" /></a>
|
||||
</p>
|
||||
|
||||
The **entire Rust port** was built by oh-my-opencode's **Sisyphus** agent in `ultrawork` mode.
|
||||
|
||||
> *"If Claude Code does in 7 days what a human does in 3 months, Sisyphus does it in 1 hour."* — B, Quant Researcher
|
||||
|
||||
> *"Oh My OpenCode Is Actually Insane"* — [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
|
||||
|
||||
**Credits:** [@code-yeongyu](https://github.com/code-yeongyu) (oh-my-opencode creator) · **Sisyphus** (autonomous coding agent) · **Jobdori**
|
||||
|
||||
<p align="center"><code>npx oh-my-opencode@latest</code></p>
|
||||
|
||||
---
|
||||
|
||||
## Rust Port
|
||||
|
||||
The Rust workspace under `rust/` is the current systems-language port of the project.
|
||||
@@ -62,7 +94,7 @@ The whole thing was orchestrated end-to-end using [oh-my-codex (OmX)](https://gi
|
||||
|
||||
The result is a clean-room Python rewrite that captures the architectural patterns of Claw Code's agent harness without copying any proprietary source. I'm now actively collaborating with [@bellman_ych](https://x.com/bellman_ych) — the creator of OmX himself — to push this further. The basic Python foundation is already in place and functional, but we're just getting started. **Stay tuned — a much more capable version is on the way.**
|
||||
|
||||
The Rust port was developed with both [oh-my-codex (OmX)](https://github.com/Yeachan-Heo/oh-my-codex) and [oh-my-opencode (OmO)](https://github.com/code-yeongyu/oh-my-openagent): OmX drove scaffolding, orchestration, and architecture direction, while OmO was used for later implementation acceleration and verification support.
|
||||
The Rust port was built separately using [oh-my-opencode (OMO)](https://github.com/code-yeongyu/oh-my-opencode) by [@q_yeon_gyu_kim](https://x.com/q_yeon_gyu_kim) ([@code-yeongyu](https://github.com/code-yeongyu)), which orchestrates [opencode](https://opencode.ai) agents. **The scaffolding and architecture direction were established with [oh-my-codex (OmX)](https://github.com/Yeachan-Heo/oh-my-codex),** and the **Sisyphus** agent then handled implementation work across the API client, runtime engine, CLI, plugin system, MCP integration, and the cleanroom pass in `ultrawork` mode.
|
||||
|
||||
https://github.com/instructkr/claw-code
|
||||
|
||||
@@ -188,8 +220,8 @@ The port now mirrors the archived root-entry file surface, top-level subsystem n
|
||||
|
||||
This repository's porting, cleanroom hardening, and verification workflow was AI-assisted with Yeachan Heo's tooling stack, with **oh-my-codex (OmX)** as the primary scaffolding and orchestration layer.
|
||||
|
||||
- [**oh-my-codex (OmX)**](https://github.com/Yeachan-Heo/oh-my-codex) — scaffolding, orchestration, architecture direction, and core porting workflow
|
||||
- [**oh-my-opencode (OmO)**](https://github.com/code-yeongyu/oh-my-openagent) — implementation acceleration, cleanup, and verification support
|
||||
- [**oh-my-codex (OmX)**](https://github.com/Yeachan-Heo/oh-my-codex) — main branch credit: primary scaffolding, orchestration, and core porting workflow
|
||||
- [**oh-my-opencode (OmO)**](https://github.com/instructkr/oh-my-opencode) — implementation acceleration, cleanup passes, and verification support
|
||||
|
||||
Key workflow patterns used during the port:
|
||||
|
||||
|
||||
229
rust/README.md
229
rust/README.md
@@ -1,122 +1,149 @@
|
||||
# Claw Code
|
||||
# 🦞 Claw Code — Rust Implementation
|
||||
|
||||
Claw Code is a local coding-agent CLI implemented in safe Rust. It is **Claude Code inspired** and developed as a **clean-room implementation**: it aims for a strong local agent experience, but it is **not** a direct port or copy of Claude Code.
|
||||
A high-performance Rust rewrite of the Claw Code CLI agent harness. Built for speed, safety, and native tool execution.
|
||||
|
||||
The Rust workspace is the current main product surface. The `claw` binary provides interactive sessions, one-shot prompts, workspace-aware tools, local agent workflows, and plugin-capable operation from a single workspace.
|
||||
|
||||
## Current status
|
||||
|
||||
- **Version:** `0.1.0`
|
||||
- **Release stage:** initial public release, source-build distribution
|
||||
- **Primary implementation:** Rust workspace in this repository
|
||||
- **Platform focus:** macOS and Linux developer workstations
|
||||
|
||||
## Install, build, and run
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust stable toolchain
|
||||
- Cargo
|
||||
- Provider credentials for the model you want to use
|
||||
|
||||
### Authentication
|
||||
|
||||
Anthropic-compatible models:
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="..."
|
||||
# Optional when using a compatible endpoint
|
||||
export ANTHROPIC_BASE_URL="https://api.anthropic.com"
|
||||
```
|
||||
# Build
|
||||
cd rust/
|
||||
cargo build --release
|
||||
|
||||
Grok models:
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY="..."
|
||||
# Optional when using a compatible endpoint
|
||||
export XAI_BASE_URL="https://api.x.ai"
|
||||
```
|
||||
|
||||
OAuth login is also available:
|
||||
|
||||
```bash
|
||||
cargo run --bin claw -- login
|
||||
```
|
||||
|
||||
### Install locally
|
||||
|
||||
```bash
|
||||
cargo install --path crates/claw-cli --locked
|
||||
```
|
||||
|
||||
### Build from source
|
||||
|
||||
```bash
|
||||
cargo build --release -p claw-cli
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
From the workspace:
|
||||
|
||||
```bash
|
||||
cargo run --bin claw -- --help
|
||||
cargo run --bin claw --
|
||||
cargo run --bin claw -- prompt "summarize this workspace"
|
||||
cargo run --bin claw -- --model sonnet "review the latest changes"
|
||||
```
|
||||
|
||||
From the release build:
|
||||
|
||||
```bash
|
||||
# Run interactive REPL
|
||||
./target/release/claw
|
||||
./target/release/claw prompt "explain crates/runtime"
|
||||
|
||||
# One-shot prompt
|
||||
./target/release/claw prompt "explain this codebase"
|
||||
|
||||
# With specific model
|
||||
./target/release/claw --model sonnet prompt "fix the bug in main.rs"
|
||||
```
|
||||
|
||||
## Supported capabilities
|
||||
## Configuration
|
||||
|
||||
- Interactive REPL and one-shot prompt execution
|
||||
- Saved-session inspection and resume flows
|
||||
- Built-in workspace tools for shell, file read/write/edit, search, web fetch/search, todos, and notebook updates
|
||||
- Slash commands for status, compaction, config inspection, diff, export, session management, and version reporting
|
||||
- Local agent and skill discovery with `claw agents` and `claw skills`
|
||||
- Plugin discovery and management through the CLI and slash-command surfaces
|
||||
- OAuth login/logout plus model/provider selection from the command line
|
||||
- Workspace-aware instruction/config loading (`CLAW.md`, config files, permissions, plugin settings)
|
||||
Set your API credentials:
|
||||
|
||||
## Current limitations
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
# Or use a proxy
|
||||
export ANTHROPIC_BASE_URL="https://your-proxy.com"
|
||||
```
|
||||
|
||||
- Public distribution is **source-build only** today; this workspace is not set up for crates.io publishing
|
||||
- GitHub CI verifies `cargo check`, `cargo test`, and release builds, but automated release packaging is not yet present
|
||||
- Current CI targets Ubuntu and macOS; Windows release readiness is still to be established
|
||||
- Some live-provider integration coverage is opt-in because it requires external credentials and network access
|
||||
- The command surface may continue to evolve during the `0.x` series
|
||||
Or authenticate via OAuth:
|
||||
|
||||
## Implementation
|
||||
```bash
|
||||
claw login
|
||||
```
|
||||
|
||||
The Rust workspace is the active product implementation. It currently includes these crates:
|
||||
## Features
|
||||
|
||||
- `claw-cli` — user-facing binary
|
||||
- `api` — provider clients and streaming
|
||||
- `runtime` — sessions, config, permissions, prompts, and runtime loop
|
||||
- `tools` — built-in tool implementations
|
||||
- `commands` — slash-command registry and handlers
|
||||
- `plugins` — plugin discovery, registry, and lifecycle support
|
||||
- `lsp` — language-server protocol support types and process helpers
|
||||
- `server` and `compat-harness` — supporting services and compatibility tooling
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| API + streaming | ✅ |
|
||||
| OAuth login/logout | ✅ |
|
||||
| Interactive REPL (rustyline) | ✅ |
|
||||
| Tool system (bash, read, write, edit, grep, glob) | ✅ |
|
||||
| Web tools (search, fetch) | ✅ |
|
||||
| Sub-agent orchestration | ✅ |
|
||||
| Todo tracking | ✅ |
|
||||
| Notebook editing | ✅ |
|
||||
| CLAW.md / project memory | ✅ |
|
||||
| Config file hierarchy (.claw.json) | ✅ |
|
||||
| Permission system | ✅ |
|
||||
| MCP server lifecycle | ✅ |
|
||||
| Session persistence + resume | ✅ |
|
||||
| Extended thinking (thinking blocks) | ✅ |
|
||||
| Cost tracking + usage display | ✅ |
|
||||
| Git integration | ✅ |
|
||||
| Markdown terminal rendering (ANSI) | ✅ |
|
||||
| Model aliases (opus/sonnet/haiku) | ✅ |
|
||||
| Slash commands (/status, /compact, /clear, etc.) | ✅ |
|
||||
| Hooks (PreToolUse/PostToolUse) | 🔧 Config only |
|
||||
| Plugin system | 📋 Planned |
|
||||
| Skills registry | 📋 Planned |
|
||||
|
||||
## Roadmap
|
||||
## Model Aliases
|
||||
|
||||
- Publish packaged release artifacts for public installs
|
||||
- Add a repeatable release workflow and longer-lived changelog discipline
|
||||
- Expand platform verification beyond the current CI matrix
|
||||
- Add more task-focused examples and operator documentation
|
||||
- Continue tightening feature coverage and UX polish across the Rust implementation
|
||||
Short names resolve to the latest model versions:
|
||||
|
||||
## Release notes
|
||||
| Alias | Resolves To |
|
||||
|-------|------------|
|
||||
| `opus` | `claude-opus-4-6` |
|
||||
| `sonnet` | `claude-sonnet-4-6` |
|
||||
| `haiku` | `claude-haiku-4-5-20251213` |
|
||||
|
||||
- Draft 0.1.0 release notes: [`docs/releases/0.1.0.md`](docs/releases/0.1.0.md)
|
||||
## CLI Flags
|
||||
|
||||
```
|
||||
claw [OPTIONS] [COMMAND]
|
||||
|
||||
Options:
|
||||
--model MODEL Set the model (alias or full name)
|
||||
--dangerously-skip-permissions Skip all permission checks
|
||||
--permission-mode MODE Set read-only, workspace-write, or danger-full-access
|
||||
--allowedTools TOOLS Restrict enabled tools
|
||||
--output-format FORMAT Output format (text or json)
|
||||
--version, -V Print version info
|
||||
|
||||
Commands:
|
||||
prompt <text> One-shot prompt (non-interactive)
|
||||
login Authenticate via OAuth
|
||||
logout Clear stored credentials
|
||||
init Initialize project config
|
||||
doctor Check environment health
|
||||
self-update Update to latest version
|
||||
```
|
||||
|
||||
## Slash Commands (REPL)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/help` | Show help |
|
||||
| `/status` | Show session status (model, tokens, cost) |
|
||||
| `/cost` | Show cost breakdown |
|
||||
| `/compact` | Compact conversation history |
|
||||
| `/clear` | Clear conversation |
|
||||
| `/model [name]` | Show or switch model |
|
||||
| `/permissions` | Show or switch permission mode |
|
||||
| `/config [section]` | Show config (env, hooks, model) |
|
||||
| `/memory` | Show CLAW.md contents |
|
||||
| `/diff` | Show git diff |
|
||||
| `/export [path]` | Export conversation |
|
||||
| `/session [id]` | Resume a previous session |
|
||||
| `/version` | Show version |
|
||||
|
||||
## Workspace Layout
|
||||
|
||||
```
|
||||
rust/
|
||||
├── Cargo.toml # Workspace root
|
||||
├── Cargo.lock
|
||||
└── crates/
|
||||
├── api/ # API client + SSE streaming
|
||||
├── commands/ # Shared slash-command registry
|
||||
├── compat-harness/ # TS manifest extraction harness
|
||||
├── runtime/ # Session, config, permissions, MCP, prompts
|
||||
├── claw-cli/ # Main CLI binary (`claw`)
|
||||
└── tools/ # Built-in tool implementations
|
||||
```
|
||||
|
||||
### Crate Responsibilities
|
||||
|
||||
- **api** — HTTP client, SSE stream parser, request/response types, auth (API key + OAuth bearer)
|
||||
- **commands** — Slash command definitions and help text generation
|
||||
- **compat-harness** — Extracts tool/prompt manifests from upstream TS source
|
||||
- **runtime** — `ConversationRuntime` agentic loop, `ConfigLoader` hierarchy, `Session` persistence, permission policy, MCP client, system prompt assembly, usage tracking
|
||||
- **claw-cli** — REPL, one-shot prompt, streaming display, tool call rendering, CLI argument parsing
|
||||
- **tools** — Tool specs + execution: Bash, ReadFile, WriteFile, EditFile, GlobSearch, GrepSearch, WebSearch, WebFetch, Agent, TodoWrite, NotebookEdit, Skill, ToolSearch, REPL runtimes
|
||||
|
||||
## Stats
|
||||
|
||||
- **~20K lines** of Rust
|
||||
- **6 crates** in workspace
|
||||
- **Binary name:** `claw`
|
||||
- **Default model:** `claude-opus-4-6`
|
||||
- **Default permissions:** `danger-full-access`
|
||||
|
||||
## License
|
||||
|
||||
See the repository root for licensing details.
|
||||
See repository root.
|
||||
|
||||
@@ -244,6 +244,14 @@ pub struct LineEditor {
|
||||
history: Vec<String>,
|
||||
yank_buffer: YankBuffer,
|
||||
vim_enabled: bool,
|
||||
completion_state: Option<CompletionState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct CompletionState {
|
||||
prefix: String,
|
||||
matches: Vec<String>,
|
||||
next_index: usize,
|
||||
}
|
||||
|
||||
impl LineEditor {
|
||||
@@ -255,6 +263,7 @@ impl LineEditor {
|
||||
history: Vec::new(),
|
||||
yank_buffer: YankBuffer::default(),
|
||||
vim_enabled: false,
|
||||
completion_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +366,10 @@ impl LineEditor {
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, session: &mut EditSession, key: KeyEvent) -> KeyAction {
|
||||
if key.code != KeyCode::Tab {
|
||||
self.completion_state = None;
|
||||
}
|
||||
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
match key.code {
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||
@@ -673,22 +686,62 @@ impl LineEditor {
|
||||
session.cursor = insert_at + self.yank_buffer.text.len();
|
||||
}
|
||||
|
||||
fn complete_slash_command(&self, session: &mut EditSession) {
|
||||
fn complete_slash_command(&mut self, session: &mut EditSession) {
|
||||
if session.mode == EditorMode::Command {
|
||||
self.completion_state = None;
|
||||
return;
|
||||
}
|
||||
if let Some(state) = self
|
||||
.completion_state
|
||||
.as_mut()
|
||||
.filter(|_| session.cursor == session.text.len())
|
||||
.filter(|state| {
|
||||
state
|
||||
.matches
|
||||
.iter()
|
||||
.any(|candidate| candidate == &session.text)
|
||||
})
|
||||
{
|
||||
let candidate = state.matches[state.next_index % state.matches.len()].clone();
|
||||
state.next_index += 1;
|
||||
session.text.replace_range(..session.cursor, &candidate);
|
||||
session.cursor = candidate.len();
|
||||
return;
|
||||
}
|
||||
let Some(prefix) = slash_command_prefix(&session.text, session.cursor) else {
|
||||
self.completion_state = None;
|
||||
return;
|
||||
};
|
||||
let Some(candidate) = self
|
||||
let matches = self
|
||||
.completions
|
||||
.iter()
|
||||
.find(|candidate| candidate.starts_with(prefix) && candidate.as_str() != prefix)
|
||||
else {
|
||||
.filter(|candidate| candidate.starts_with(prefix) && candidate.as_str() != prefix)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if matches.is_empty() {
|
||||
self.completion_state = None;
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate = if let Some(state) = self
|
||||
.completion_state
|
||||
.as_mut()
|
||||
.filter(|state| state.prefix == prefix && state.matches == matches)
|
||||
{
|
||||
let index = state.next_index % state.matches.len();
|
||||
state.next_index += 1;
|
||||
state.matches[index].clone()
|
||||
} else {
|
||||
let candidate = matches[0].clone();
|
||||
self.completion_state = Some(CompletionState {
|
||||
prefix: prefix.to_string(),
|
||||
matches,
|
||||
next_index: 1,
|
||||
});
|
||||
candidate
|
||||
};
|
||||
|
||||
session.text.replace_range(..session.cursor, candidate);
|
||||
session.text.replace_range(..session.cursor, &candidate);
|
||||
session.cursor = candidate.len();
|
||||
}
|
||||
|
||||
@@ -1086,7 +1139,7 @@ mod tests {
|
||||
#[test]
|
||||
fn tab_completes_matching_slash_commands() {
|
||||
// given
|
||||
let editor = LineEditor::new("> ", vec!["/help".to_string(), "/hello".to_string()]);
|
||||
let mut editor = LineEditor::new("> ", vec!["/help".to_string(), "/hello".to_string()]);
|
||||
let mut session = EditSession::new(false);
|
||||
session.text = "/he".to_string();
|
||||
session.cursor = session.text.len();
|
||||
@@ -1099,6 +1152,29 @@ mod tests {
|
||||
assert_eq!(session.cursor, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_cycles_between_matching_slash_commands() {
|
||||
// given
|
||||
let mut editor = LineEditor::new(
|
||||
"> ",
|
||||
vec!["/permissions".to_string(), "/plugin".to_string()],
|
||||
);
|
||||
let mut session = EditSession::new(false);
|
||||
session.text = "/p".to_string();
|
||||
session.cursor = session.text.len();
|
||||
|
||||
// when
|
||||
editor.complete_slash_command(&mut session);
|
||||
let first = session.text.clone();
|
||||
session.cursor = session.text.len();
|
||||
editor.complete_slash_command(&mut session);
|
||||
let second = session.text.clone();
|
||||
|
||||
// then
|
||||
assert_eq!(first, "/permissions");
|
||||
assert_eq!(second, "/plugin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_cancels_when_input_exists() {
|
||||
// given
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::io::{self, IsTerminal, Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
@@ -16,14 +16,15 @@ use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use api::{
|
||||
resolve_startup_auth_source, ClawApiClient, AuthSource, ContentBlockDelta, InputContentBlock,
|
||||
resolve_startup_auth_source, AuthSource, ClawApiClient, ContentBlockDelta, InputContentBlock,
|
||||
InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
|
||||
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
||||
};
|
||||
|
||||
use commands::{
|
||||
handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command,
|
||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs, SlashCommand,
|
||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
||||
suggest_slash_commands, SlashCommand,
|
||||
};
|
||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||
use init::initialize_repo;
|
||||
@@ -59,15 +60,25 @@ type AllowedToolSet = BTreeSet<String>;
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = run() {
|
||||
eprintln!(
|
||||
"error: {error}
|
||||
|
||||
Run `claw --help` for usage."
|
||||
);
|
||||
eprintln!("{}", render_cli_error(&error.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_cli_error(problem: &str) -> String {
|
||||
let mut lines = vec!["Error".to_string()];
|
||||
for (index, line) in problem.lines().enumerate() {
|
||||
let label = if index == 0 {
|
||||
" Problem "
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
lines.push(format!("{label}{line}"));
|
||||
}
|
||||
lines.push(" Help claw --help".to_string());
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
match parse_args(&args)? {
|
||||
@@ -321,17 +332,36 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
||||
Some(SlashCommand::Help) => Ok(CliAction::Help),
|
||||
Some(SlashCommand::Agents { args }) => Ok(CliAction::Agents { args }),
|
||||
Some(SlashCommand::Skills { args }) => Ok(CliAction::Skills { args }),
|
||||
Some(command) => Err(format!(
|
||||
"unsupported direct slash command outside the REPL: {command_name}",
|
||||
command_name = match command {
|
||||
Some(command) => Err(format_direct_slash_command_error(
|
||||
match &command {
|
||||
SlashCommand::Unknown(name) => format!("/{name}"),
|
||||
_ => rest[0].clone(),
|
||||
}
|
||||
.as_str(),
|
||||
matches!(command, SlashCommand::Unknown(_)),
|
||||
)),
|
||||
None => Err(format!("unknown subcommand: {}", rest[0])),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_direct_slash_command_error(command: &str, is_unknown: bool) -> String {
|
||||
let trimmed = command.trim().trim_start_matches('/');
|
||||
let mut lines = vec![
|
||||
"Direct slash command unavailable".to_string(),
|
||||
format!(" Command /{trimmed}"),
|
||||
];
|
||||
if is_unknown {
|
||||
append_slash_command_suggestions(&mut lines, trimmed);
|
||||
} else {
|
||||
lines.push(" Try Start `claw` to use interactive slash commands".to_string());
|
||||
lines.push(
|
||||
" Tip Resume-safe commands also work with `claw --resume SESSION.json ...`"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn resolve_model_alias(model: &str) -> &str {
|
||||
match model {
|
||||
"opus" => "claude-opus-4-6",
|
||||
@@ -670,13 +700,17 @@ struct StatusUsage {
|
||||
fn format_model_report(model: &str, message_count: usize, turns: u32) -> String {
|
||||
format!(
|
||||
"Model
|
||||
Current model {model}
|
||||
Session messages {message_count}
|
||||
Session turns {turns}
|
||||
Current {model}
|
||||
Session {message_count} messages · {turns} turns
|
||||
|
||||
Usage
|
||||
Inspect current model with /model
|
||||
Switch models with /model <name>"
|
||||
Aliases
|
||||
opus claude-opus-4-6
|
||||
sonnet claude-sonnet-4-6
|
||||
haiku claude-haiku-4-5-20251213
|
||||
|
||||
Next
|
||||
/model Show the current model
|
||||
/model <name> Switch models for this REPL session"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -685,7 +719,8 @@ fn format_model_switch_report(previous: &str, next: &str, message_count: usize)
|
||||
"Model updated
|
||||
Previous {previous}
|
||||
Current {next}
|
||||
Preserved msgs {message_count}"
|
||||
Preserved {message_count} messages
|
||||
Tip Existing conversation context stayed attached"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -718,28 +753,34 @@ fn format_permissions_report(mode: &str) -> String {
|
||||
",
|
||||
);
|
||||
|
||||
let effect = match mode {
|
||||
"read-only" => "Only read/search tools can run automatically",
|
||||
"workspace-write" => "Editing tools can modify files in the workspace",
|
||||
"danger-full-access" => "All tools can run without additional sandbox limits",
|
||||
_ => "Unknown permission mode",
|
||||
};
|
||||
|
||||
format!(
|
||||
"Permissions
|
||||
Active mode {mode}
|
||||
Mode status live session default
|
||||
Effect {effect}
|
||||
|
||||
Modes
|
||||
{modes}
|
||||
|
||||
Usage
|
||||
Inspect current mode with /permissions
|
||||
Switch modes with /permissions <mode>"
|
||||
Next
|
||||
/permissions Show the current mode
|
||||
/permissions <mode> Switch modes for subsequent tool calls"
|
||||
)
|
||||
}
|
||||
|
||||
fn format_permissions_switch_report(previous: &str, next: &str) -> String {
|
||||
format!(
|
||||
"Permissions updated
|
||||
Result mode switched
|
||||
Previous mode {previous}
|
||||
Active mode {next}
|
||||
Applies to subsequent tool calls
|
||||
Usage /permissions to inspect current mode"
|
||||
Applies to Subsequent tool calls in this REPL
|
||||
Tip Run /permissions to review all available modes"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -750,7 +791,11 @@ fn format_cost_report(usage: TokenUsage) -> String {
|
||||
Output tokens {}
|
||||
Cache create {}
|
||||
Cache read {}
|
||||
Total tokens {}",
|
||||
Total tokens {}
|
||||
|
||||
Next
|
||||
/status See session + workspace context
|
||||
/compact Trim local history if the session is getting large",
|
||||
usage.input_tokens,
|
||||
usage.output_tokens,
|
||||
usage.cache_creation_input_tokens,
|
||||
@@ -763,8 +808,8 @@ fn format_resume_report(session_path: &str, message_count: usize, turns: u32) ->
|
||||
format!(
|
||||
"Session resumed
|
||||
Session file {session_path}
|
||||
Messages {message_count}
|
||||
Turns {turns}"
|
||||
History {message_count} messages · {turns} turns
|
||||
Next /status · /diff · /export"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -773,7 +818,7 @@ fn format_compact_report(removed: usize, resulting_messages: usize, skipped: boo
|
||||
format!(
|
||||
"Compact
|
||||
Result skipped
|
||||
Reason session below compaction threshold
|
||||
Reason Session is already below the compaction threshold
|
||||
Messages kept {resulting_messages}"
|
||||
)
|
||||
} else {
|
||||
@@ -781,7 +826,8 @@ fn format_compact_report(removed: usize, resulting_messages: usize, skipped: boo
|
||||
"Compact
|
||||
Result compacted
|
||||
Messages removed {removed}
|
||||
Messages kept {resulting_messages}"
|
||||
Messages kept {resulting_messages}
|
||||
Tip Use /status to review the trimmed session"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1052,28 +1098,65 @@ impl LiveCli {
|
||||
}
|
||||
|
||||
fn startup_banner(&self) -> String {
|
||||
let cwd = env::current_dir().map_or_else(
|
||||
|_| "<unknown>".to_string(),
|
||||
let color = io::stdout().is_terminal();
|
||||
let cwd = env::current_dir().ok();
|
||||
let cwd_display = cwd.as_ref().map_or_else(
|
||||
|| "<unknown>".to_string(),
|
||||
|path| path.display().to_string(),
|
||||
);
|
||||
format!(
|
||||
"\x1b[38;5;196m\
|
||||
██████╗██╗ █████╗ ██╗ ██╗\n\
|
||||
██╔════╝██║ ██╔══██╗██║ ██║\n\
|
||||
██║ ██║ ███████║██║ █╗ ██║\n\
|
||||
██║ ██║ ██╔══██║██║███╗██║\n\
|
||||
╚██████╗███████╗██║ ██║╚███╔███╔╝\n\
|
||||
╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\x1b[0m \x1b[38;5;208mCode\x1b[0m 🦞\n\n\
|
||||
\x1b[2mModel\x1b[0m {}\n\
|
||||
\x1b[2mPermissions\x1b[0m {}\n\
|
||||
\x1b[2mDirectory\x1b[0m {}\n\
|
||||
\x1b[2mSession\x1b[0m {}\n\n\
|
||||
Type \x1b[1m/help\x1b[0m for commands · \x1b[2mShift+Enter\x1b[0m for newline",
|
||||
self.model,
|
||||
self.permission_mode.as_str(),
|
||||
cwd,
|
||||
self.session.id,
|
||||
)
|
||||
let workspace_name = cwd
|
||||
.as_ref()
|
||||
.and_then(|path| path.file_name())
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("workspace");
|
||||
let git_branch = status_context(Some(&self.session.path))
|
||||
.ok()
|
||||
.and_then(|context| context.git_branch);
|
||||
let workspace_summary = git_branch.as_deref().map_or_else(
|
||||
|| workspace_name.to_string(),
|
||||
|branch| format!("{workspace_name} · {branch}"),
|
||||
);
|
||||
let has_claw_md = cwd
|
||||
.as_ref()
|
||||
.is_some_and(|path| path.join("CLAW.md").is_file());
|
||||
let mut lines = vec![
|
||||
format!(
|
||||
"{} {}",
|
||||
if color {
|
||||
"\x1b[1;38;5;45m🦞 Claw Code\x1b[0m"
|
||||
} else {
|
||||
"Claw Code"
|
||||
},
|
||||
if color {
|
||||
"\x1b[2m· ready\x1b[0m"
|
||||
} else {
|
||||
"· ready"
|
||||
}
|
||||
),
|
||||
format!(" Workspace {workspace_summary}"),
|
||||
format!(" Directory {cwd_display}"),
|
||||
format!(" Model {}", self.model),
|
||||
format!(" Permissions {}", self.permission_mode.as_str()),
|
||||
format!(" Session {}", self.session.id),
|
||||
format!(
|
||||
" Quick start {}",
|
||||
if has_claw_md {
|
||||
"/help · /status · ask for a task"
|
||||
} else {
|
||||
"/init · /help · /status"
|
||||
}
|
||||
),
|
||||
" Editor Tab completes slash commands · /vim toggles modal editing"
|
||||
.to_string(),
|
||||
" Multiline Shift+Enter or Ctrl+J inserts a newline".to_string(),
|
||||
];
|
||||
if !has_claw_md {
|
||||
lines.push(
|
||||
" First run /init scaffolds CLAW.md, .claw.json, and local session files"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -1246,19 +1329,28 @@ impl LiveCli {
|
||||
false
|
||||
}
|
||||
SlashCommand::Branch { .. } => {
|
||||
eprintln!("git branch commands not yet wired to REPL");
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_mode_unavailable("branch", "git branch commands")
|
||||
);
|
||||
false
|
||||
}
|
||||
SlashCommand::Worktree { .. } => {
|
||||
eprintln!("git worktree commands not yet wired to REPL");
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_mode_unavailable("worktree", "git worktree commands")
|
||||
);
|
||||
false
|
||||
}
|
||||
SlashCommand::CommitPushPr { .. } => {
|
||||
eprintln!("commit-push-pr not yet wired to REPL");
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_mode_unavailable("commit-push-pr", "commit + push + PR automation")
|
||||
);
|
||||
false
|
||||
}
|
||||
SlashCommand::Unknown(name) => {
|
||||
eprintln!("unknown slash command: /{name}");
|
||||
eprintln!("{}", render_unknown_slash_command(&name));
|
||||
false
|
||||
}
|
||||
})
|
||||
@@ -1837,6 +1929,20 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
fn format_relative_timestamp(epoch_secs: u64) -> String {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or(epoch_secs);
|
||||
let elapsed = now.saturating_sub(epoch_secs);
|
||||
match elapsed {
|
||||
0..=59 => format!("{elapsed}s ago"),
|
||||
60..=3_599 => format!("{}m ago", elapsed / 60),
|
||||
3_600..=86_399 => format!("{}h ago", elapsed / 3_600),
|
||||
_ => format!("{}d ago", elapsed / 86_400),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let sessions = list_managed_sessions()?;
|
||||
let mut lines = vec![
|
||||
@@ -1854,26 +1960,28 @@ fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::e
|
||||
"○ saved"
|
||||
};
|
||||
lines.push(format!(
|
||||
" {id:<20} {marker:<10} msgs={msgs:<4} modified={modified} path={path}",
|
||||
" {id:<20} {marker:<10} {msgs:>3} msgs · updated {modified}",
|
||||
id = session.id,
|
||||
msgs = session.message_count,
|
||||
modified = session.modified_epoch_secs,
|
||||
path = session.path.display(),
|
||||
modified = format_relative_timestamp(session.modified_epoch_secs),
|
||||
));
|
||||
lines.push(format!(" {}", session.path.display()));
|
||||
}
|
||||
Ok(lines.join("\n"))
|
||||
}
|
||||
|
||||
fn render_repl_help() -> String {
|
||||
[
|
||||
"REPL".to_string(),
|
||||
" /exit Quit the REPL".to_string(),
|
||||
" /quit Quit the REPL".to_string(),
|
||||
" /vim Toggle Vim keybindings".to_string(),
|
||||
" Up/Down Navigate prompt history".to_string(),
|
||||
" Tab Complete slash commands".to_string(),
|
||||
" Ctrl-C Clear input (or exit on empty prompt)".to_string(),
|
||||
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
|
||||
"Interactive REPL".to_string(),
|
||||
" Quick start Ask a task in plain English or use one of the core commands below."
|
||||
.to_string(),
|
||||
" Core commands /help · /status · /model · /permissions · /compact".to_string(),
|
||||
" Exit /exit or /quit".to_string(),
|
||||
" Vim mode /vim toggles modal editing".to_string(),
|
||||
" History Up/Down recalls previous prompts".to_string(),
|
||||
" Completion Tab cycles slash command matches".to_string(),
|
||||
" Cancel Ctrl-C clears input (or exits on an empty prompt)".to_string(),
|
||||
" Multiline Shift+Enter or Ctrl+J inserts a newline".to_string(),
|
||||
String::new(),
|
||||
render_slash_command_help(),
|
||||
]
|
||||
@@ -1883,6 +1991,41 @@ fn render_repl_help() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn append_slash_command_suggestions(lines: &mut Vec<String>, name: &str) {
|
||||
let suggestions = suggest_slash_commands(name, 3);
|
||||
if suggestions.is_empty() {
|
||||
lines.push(" Try /help shows the full slash command map".to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
lines.push(" Try /help shows the full slash command map".to_string());
|
||||
lines.push("Suggestions".to_string());
|
||||
lines.extend(
|
||||
suggestions
|
||||
.into_iter()
|
||||
.map(|suggestion| format!(" {suggestion}")),
|
||||
);
|
||||
}
|
||||
|
||||
fn render_unknown_slash_command(name: &str) -> String {
|
||||
let mut lines = vec![
|
||||
"Unknown slash command".to_string(),
|
||||
format!(" Command /{name}"),
|
||||
];
|
||||
append_slash_command_suggestions(&mut lines, name);
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn render_mode_unavailable(command: &str, label: &str) -> String {
|
||||
[
|
||||
"Command unavailable in this REPL mode".to_string(),
|
||||
format!(" Command /{command}"),
|
||||
format!(" Feature {label}"),
|
||||
" Tip Use /help to find currently wired REPL commands".to_string(),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn status_context(
|
||||
session_path: Option<&Path>,
|
||||
) -> Result<StatusContext, Box<dyn std::error::Error>> {
|
||||
@@ -1912,33 +2055,41 @@ fn format_status_report(
|
||||
) -> String {
|
||||
[
|
||||
format!(
|
||||
"Status
|
||||
"Session
|
||||
Model {model}
|
||||
Permission mode {permission_mode}
|
||||
Messages {}
|
||||
Turns {}
|
||||
Estimated tokens {}",
|
||||
usage.message_count, usage.turns, usage.estimated_tokens,
|
||||
),
|
||||
format!(
|
||||
"Usage
|
||||
Latest total {}
|
||||
Cumulative input {}
|
||||
Cumulative output {}
|
||||
Cumulative total {}",
|
||||
Permissions {permission_mode}
|
||||
Activity {} messages · {} turns
|
||||
Tokens est {} · latest {} · total {}",
|
||||
usage.message_count,
|
||||
usage.turns,
|
||||
usage.estimated_tokens,
|
||||
usage.latest.total_tokens(),
|
||||
usage.cumulative.input_tokens,
|
||||
usage.cumulative.output_tokens,
|
||||
usage.cumulative.total_tokens(),
|
||||
),
|
||||
format!(
|
||||
"Usage
|
||||
Cumulative input {}
|
||||
Cumulative output {}
|
||||
Cache create {}
|
||||
Cache read {}",
|
||||
usage.cumulative.input_tokens,
|
||||
usage.cumulative.output_tokens,
|
||||
usage.cumulative.cache_creation_input_tokens,
|
||||
usage.cumulative.cache_read_input_tokens,
|
||||
),
|
||||
format!(
|
||||
"Workspace
|
||||
Cwd {}
|
||||
Folder {}
|
||||
Project root {}
|
||||
Git branch {}
|
||||
Session {}
|
||||
Session file {}
|
||||
Config files loaded {}/{}
|
||||
Memory files {}",
|
||||
Memory files {}
|
||||
|
||||
Next
|
||||
/help Browse commands
|
||||
/session list Inspect saved sessions
|
||||
/diff Review current workspace changes",
|
||||
context.cwd.display(),
|
||||
context
|
||||
.project_root
|
||||
@@ -2053,8 +2204,7 @@ fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
|
||||
if project_context.instruction_files.is_empty() {
|
||||
lines.push("Discovered files".to_string());
|
||||
lines.push(
|
||||
" No CLAW instruction files discovered in the current directory ancestry."
|
||||
.to_string(),
|
||||
" No CLAW instruction files discovered in the current directory ancestry.".to_string(),
|
||||
);
|
||||
} else {
|
||||
lines.push("Discovered files".to_string());
|
||||
@@ -2321,7 +2471,7 @@ fn render_version_report() -> String {
|
||||
let git_sha = GIT_SHA.unwrap_or("unknown");
|
||||
let target = BUILD_TARGET.unwrap_or("unknown");
|
||||
format!(
|
||||
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Target {target}\n Build date {DEFAULT_DATE}"
|
||||
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Target {target}\n Build date {DEFAULT_DATE}\n\nSupport\n Help claw --help\n REPL /help"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2806,7 +2956,8 @@ fn build_runtime(
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||
) -> Result<ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>> {
|
||||
) -> Result<ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||
{
|
||||
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
||||
Ok(ConversationRuntime::new_with_features(
|
||||
session,
|
||||
@@ -3730,65 +3881,118 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
||||
}
|
||||
|
||||
fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, "claw v{VERSION}")?;
|
||||
writeln!(out, "Claw Code CLI v{VERSION}")?;
|
||||
writeln!(
|
||||
out,
|
||||
" Interactive coding assistant for the current workspace."
|
||||
)?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "Usage:")?;
|
||||
writeln!(out, "Quick start")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw [--model MODEL] [--allowedTools TOOL[,TOOL...]]"
|
||||
)?;
|
||||
writeln!(out, " Start the interactive REPL")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw [--model MODEL] [--output-format text|json] prompt TEXT"
|
||||
)?;
|
||||
writeln!(out, " Send one prompt and exit")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw [--model MODEL] [--output-format text|json] TEXT"
|
||||
)?;
|
||||
writeln!(out, " Shorthand non-interactive prompt mode")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw --resume SESSION.json [/status] [/compact] [...]"
|
||||
" claw Start the interactive REPL"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" Inspect or maintain a saved session without entering the REPL"
|
||||
" claw \"summarize this repo\" Run one prompt and exit"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw prompt \"explain src/main.rs\" Explicit one-shot prompt"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw --resume SESSION.json /status Inspect a saved session"
|
||||
)?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "Interactive essentials")?;
|
||||
writeln!(
|
||||
out,
|
||||
" /help Browse the full slash command map"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" /status Inspect session + workspace state"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" /model <name> Switch models mid-session"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" /permissions <mode> Adjust tool access"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" Tab Complete slash commands"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" /vim Toggle modal editing"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" Shift+Enter / Ctrl+J Insert a newline"
|
||||
)?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "Commands")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw dump-manifests Read upstream TS sources and print extracted counts"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw bootstrap-plan Print the bootstrap phase skeleton"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw agents List configured agents"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw skills List installed skills"
|
||||
)?;
|
||||
writeln!(out, " claw dump-manifests")?;
|
||||
writeln!(out, " claw bootstrap-plan")?;
|
||||
writeln!(out, " claw agents")?;
|
||||
writeln!(out, " claw skills")?;
|
||||
writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw logout")?;
|
||||
writeln!(out, " claw init")?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "Flags:")?;
|
||||
writeln!(
|
||||
out,
|
||||
" --model MODEL Override the active model"
|
||||
" claw login Start the OAuth login flow"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --output-format FORMAT Non-interactive output format: text or json"
|
||||
" claw logout Clear saved OAuth credentials"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --dangerously-skip-permissions Skip all permission checks"
|
||||
)?;
|
||||
writeln!(out, " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)")?;
|
||||
writeln!(
|
||||
out,
|
||||
" --version, -V Print version and build information locally"
|
||||
" claw init Scaffold CLAW.md + local files"
|
||||
)?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "Interactive slash commands:")?;
|
||||
writeln!(out, "Flags")?;
|
||||
writeln!(
|
||||
out,
|
||||
" --model MODEL Override the active model"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --output-format FORMAT Non-interactive output: text or json"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --dangerously-skip-permissions Skip all permission checks"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" --version, -V Print version and build information"
|
||||
)?;
|
||||
writeln!(out)?;
|
||||
writeln!(out, "Slash command reference")?;
|
||||
writeln!(out, "{}", render_slash_command_help())?;
|
||||
writeln!(out)?;
|
||||
let resume_commands = resume_supported_slash_commands()
|
||||
@@ -3800,7 +4004,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
writeln!(out, "Resume-safe commands: {resume_commands}")?;
|
||||
writeln!(out, "Examples:")?;
|
||||
writeln!(out, "Examples")?;
|
||||
writeln!(out, " claw --model opus \"summarize this repo\"")?;
|
||||
writeln!(
|
||||
out,
|
||||
@@ -4072,7 +4276,8 @@ mod tests {
|
||||
);
|
||||
let error = parse_args(&["/status".to_string()])
|
||||
.expect_err("/status should remain REPL-only when invoked directly");
|
||||
assert!(error.contains("unsupported direct slash command"));
|
||||
assert!(error.contains("Direct slash command unavailable"));
|
||||
assert!(error.contains("/status"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4149,13 +4354,14 @@ mod tests {
|
||||
fn shared_help_uses_resume_annotation_copy() {
|
||||
let help = commands::render_slash_command_help();
|
||||
assert!(help.contains("Slash commands"));
|
||||
assert!(help.contains("Tab completes commands inside the REPL."));
|
||||
assert!(help.contains("works with --resume SESSION.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_help_includes_shared_commands_and_exit() {
|
||||
let help = render_repl_help();
|
||||
assert!(help.contains("REPL"));
|
||||
assert!(help.contains("Interactive REPL"));
|
||||
assert!(help.contains("/help"));
|
||||
assert!(help.contains("/status"));
|
||||
assert!(help.contains("/model [model]"));
|
||||
@@ -4177,6 +4383,7 @@ mod tests {
|
||||
assert!(help.contains("/agents"));
|
||||
assert!(help.contains("/skills"));
|
||||
assert!(help.contains("/exit"));
|
||||
assert!(help.contains("Tab cycles slash command matches"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4199,8 +4406,8 @@ mod tests {
|
||||
let report = format_resume_report("session.json", 14, 6);
|
||||
assert!(report.contains("Session resumed"));
|
||||
assert!(report.contains("Session file session.json"));
|
||||
assert!(report.contains("Messages 14"));
|
||||
assert!(report.contains("Turns 6"));
|
||||
assert!(report.contains("History 14 messages · 6 turns"));
|
||||
assert!(report.contains("/status · /diff · /export"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4209,6 +4416,7 @@ mod tests {
|
||||
assert!(compacted.contains("Compact"));
|
||||
assert!(compacted.contains("Result compacted"));
|
||||
assert!(compacted.contains("Messages removed 8"));
|
||||
assert!(compacted.contains("Use /status"));
|
||||
let skipped = format_compact_report(0, 3, true);
|
||||
assert!(skipped.contains("Result skipped"));
|
||||
}
|
||||
@@ -4227,6 +4435,7 @@ mod tests {
|
||||
assert!(report.contains("Cache create 3"));
|
||||
assert!(report.contains("Cache read 1"));
|
||||
assert!(report.contains("Total tokens 32"));
|
||||
assert!(report.contains("/compact"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4234,6 +4443,7 @@ mod tests {
|
||||
let report = format_permissions_report("workspace-write");
|
||||
assert!(report.contains("Permissions"));
|
||||
assert!(report.contains("Active mode workspace-write"));
|
||||
assert!(report.contains("Effect Editing tools can modify files in the workspace"));
|
||||
assert!(report.contains("Modes"));
|
||||
assert!(report.contains("read-only ○ available Read/search tools only"));
|
||||
assert!(report.contains("workspace-write ● current Edit files inside the workspace"));
|
||||
@@ -4244,10 +4454,9 @@ mod tests {
|
||||
fn permissions_switch_report_is_structured() {
|
||||
let report = format_permissions_switch_report("read-only", "workspace-write");
|
||||
assert!(report.contains("Permissions updated"));
|
||||
assert!(report.contains("Result mode switched"));
|
||||
assert!(report.contains("Previous mode read-only"));
|
||||
assert!(report.contains("Active mode workspace-write"));
|
||||
assert!(report.contains("Applies to subsequent tool calls"));
|
||||
assert!(report.contains("Applies to Subsequent tool calls in this REPL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4265,9 +4474,10 @@ mod tests {
|
||||
fn model_report_uses_sectioned_layout() {
|
||||
let report = format_model_report("sonnet", 12, 4);
|
||||
assert!(report.contains("Model"));
|
||||
assert!(report.contains("Current model sonnet"));
|
||||
assert!(report.contains("Session messages 12"));
|
||||
assert!(report.contains("Switch models with /model <name>"));
|
||||
assert!(report.contains("Current sonnet"));
|
||||
assert!(report.contains("Session 12 messages · 4 turns"));
|
||||
assert!(report.contains("Aliases"));
|
||||
assert!(report.contains("/model <name> Switch models for this REPL session"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4276,7 +4486,7 @@ mod tests {
|
||||
assert!(report.contains("Model updated"));
|
||||
assert!(report.contains("Previous sonnet"));
|
||||
assert!(report.contains("Current opus"));
|
||||
assert!(report.contains("Preserved msgs 9"));
|
||||
assert!(report.contains("Preserved 9 messages"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4311,18 +4521,18 @@ mod tests {
|
||||
git_branch: Some("main".to_string()),
|
||||
},
|
||||
);
|
||||
assert!(status.contains("Status"));
|
||||
assert!(status.contains("Session"));
|
||||
assert!(status.contains("Model sonnet"));
|
||||
assert!(status.contains("Permission mode workspace-write"));
|
||||
assert!(status.contains("Messages 7"));
|
||||
assert!(status.contains("Latest total 10"));
|
||||
assert!(status.contains("Cumulative total 31"));
|
||||
assert!(status.contains("Cwd /tmp/project"));
|
||||
assert!(status.contains("Permissions workspace-write"));
|
||||
assert!(status.contains("Activity 7 messages · 3 turns"));
|
||||
assert!(status.contains("Tokens est 128 · latest 10 · total 31"));
|
||||
assert!(status.contains("Folder /tmp/project"));
|
||||
assert!(status.contains("Project root /tmp"));
|
||||
assert!(status.contains("Git branch main"));
|
||||
assert!(status.contains("Session session.json"));
|
||||
assert!(status.contains("Session file session.json"));
|
||||
assert!(status.contains("Config files loaded 2/3"));
|
||||
assert!(status.contains("Memory files 4"));
|
||||
assert!(status.contains("/session list"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4458,8 +4668,8 @@ mod tests {
|
||||
fn repl_help_mentions_history_completion_and_multiline() {
|
||||
let help = render_repl_help();
|
||||
assert!(help.contains("Up/Down"));
|
||||
assert!(help.contains("Tab"));
|
||||
assert!(help.contains("Shift+Enter/Ctrl+J"));
|
||||
assert!(help.contains("Tab cycles"));
|
||||
assert!(help.contains("Shift+Enter or Ctrl+J"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -39,6 +39,27 @@ impl CommandRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SlashCommandCategory {
|
||||
Core,
|
||||
Workspace,
|
||||
Session,
|
||||
Git,
|
||||
Automation,
|
||||
}
|
||||
|
||||
impl SlashCommandCategory {
|
||||
const fn title(self) -> &'static str {
|
||||
match self {
|
||||
Self::Core => "Core flow",
|
||||
Self::Workspace => "Workspace & memory",
|
||||
Self::Session => "Sessions & output",
|
||||
Self::Git => "Git & GitHub",
|
||||
Self::Automation => "Automation & discovery",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SlashCommandSpec {
|
||||
pub name: &'static str,
|
||||
@@ -46,6 +67,7 @@ pub struct SlashCommandSpec {
|
||||
pub summary: &'static str,
|
||||
pub argument_hint: Option<&'static str>,
|
||||
pub resume_supported: bool,
|
||||
pub category: SlashCommandCategory,
|
||||
}
|
||||
|
||||
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
@@ -55,6 +77,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Show available slash commands",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Core,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "status",
|
||||
@@ -62,6 +85,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Show current session status",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Core,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "compact",
|
||||
@@ -69,6 +93,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Compact local session history",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Core,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "model",
|
||||
@@ -76,6 +101,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Show or switch the active model",
|
||||
argument_hint: Some("[model]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Core,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "permissions",
|
||||
@@ -83,6 +109,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Show or switch the active permission mode",
|
||||
argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Core,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "clear",
|
||||
@@ -90,6 +117,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Start a fresh local session",
|
||||
argument_hint: Some("[--confirm]"),
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Session,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "cost",
|
||||
@@ -97,6 +125,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Show cumulative token usage for this session",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Core,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "resume",
|
||||
@@ -104,6 +133,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Load a saved session into the REPL",
|
||||
argument_hint: Some("<session-path>"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Session,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "config",
|
||||
@@ -111,6 +141,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Inspect Claw config files or merged sections",
|
||||
argument_hint: Some("[env|hooks|model|plugins]"),
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Workspace,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "memory",
|
||||
@@ -118,6 +149,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Inspect loaded Claw instruction memory files",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Workspace,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "init",
|
||||
@@ -125,6 +157,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Create a starter CLAW.md for this repo",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Workspace,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "diff",
|
||||
@@ -132,6 +165,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Show git diff for current workspace changes",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Workspace,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "version",
|
||||
@@ -139,6 +173,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Show CLI version and build information",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Workspace,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "bughunter",
|
||||
@@ -146,6 +181,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Inspect the codebase for likely bugs",
|
||||
argument_hint: Some("[scope]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Automation,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "branch",
|
||||
@@ -153,6 +189,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "List, create, or switch git branches",
|
||||
argument_hint: Some("[list|create <name>|switch <name>]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Git,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "worktree",
|
||||
@@ -160,6 +197,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "List, add, remove, or prune git worktrees",
|
||||
argument_hint: Some("[list|add <path> [branch]|remove <path>|prune]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Git,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "commit",
|
||||
@@ -167,6 +205,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Generate a commit message and create a git commit",
|
||||
argument_hint: None,
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Git,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "commit-push-pr",
|
||||
@@ -174,6 +213,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Commit workspace changes, push the branch, and open a PR",
|
||||
argument_hint: Some("[context]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Git,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "pr",
|
||||
@@ -181,6 +221,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Draft or create a pull request from the conversation",
|
||||
argument_hint: Some("[context]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Git,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "issue",
|
||||
@@ -188,6 +229,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Draft or create a GitHub issue from the conversation",
|
||||
argument_hint: Some("[context]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Git,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "ultraplan",
|
||||
@@ -195,6 +237,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Run a deep planning prompt with multi-step reasoning",
|
||||
argument_hint: Some("[task]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Automation,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "teleport",
|
||||
@@ -202,6 +245,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Jump to a file or symbol by searching the workspace",
|
||||
argument_hint: Some("<symbol-or-path>"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Workspace,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "debug-tool-call",
|
||||
@@ -209,6 +253,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Replay the last tool call with debug details",
|
||||
argument_hint: None,
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Automation,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "export",
|
||||
@@ -216,6 +261,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "Export the current conversation to a file",
|
||||
argument_hint: Some("[file]"),
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Session,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "session",
|
||||
@@ -223,6 +269,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "List or switch managed local sessions",
|
||||
argument_hint: Some("[list|switch <session-id>]"),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Session,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "plugin",
|
||||
@@ -232,6 +279,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
"[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
|
||||
),
|
||||
resume_supported: false,
|
||||
category: SlashCommandCategory::Automation,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "agents",
|
||||
@@ -239,6 +287,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "List configured agents",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Automation,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "skills",
|
||||
@@ -246,6 +295,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
summary: "List available skills",
|
||||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
category: SlashCommandCategory::Automation,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -437,38 +487,131 @@ pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
|
||||
pub fn render_slash_command_help() -> String {
|
||||
let mut lines = vec![
|
||||
"Slash commands".to_string(),
|
||||
" [resume] means the command also works with --resume SESSION.json".to_string(),
|
||||
" Tab completes commands inside the REPL.".to_string(),
|
||||
" [resume] works with --resume SESSION.json.".to_string(),
|
||||
];
|
||||
for spec in slash_command_specs() {
|
||||
let name = match spec.argument_hint {
|
||||
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
||||
None => format!("/{}", spec.name),
|
||||
};
|
||||
let alias_suffix = if spec.aliases.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
" (aliases: {})",
|
||||
spec.aliases
|
||||
.iter()
|
||||
.map(|alias| format!("/{alias}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
let resume = if spec.resume_supported {
|
||||
" [resume]"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
lines.push(format!(
|
||||
" {name:<20} {}{alias_suffix}{resume}",
|
||||
spec.summary
|
||||
));
|
||||
|
||||
for category in [
|
||||
SlashCommandCategory::Core,
|
||||
SlashCommandCategory::Workspace,
|
||||
SlashCommandCategory::Session,
|
||||
SlashCommandCategory::Git,
|
||||
SlashCommandCategory::Automation,
|
||||
] {
|
||||
lines.push(String::new());
|
||||
lines.push(category.title().to_string());
|
||||
lines.extend(
|
||||
slash_command_specs()
|
||||
.iter()
|
||||
.filter(|spec| spec.category == category)
|
||||
.map(render_slash_command_entry),
|
||||
);
|
||||
}
|
||||
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn render_slash_command_entry(spec: &SlashCommandSpec) -> String {
|
||||
let alias_suffix = if spec.aliases.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
" (aliases: {})",
|
||||
spec.aliases
|
||||
.iter()
|
||||
.map(|alias| format!("/{alias}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
let resume = if spec.resume_supported {
|
||||
" [resume]"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
" {name:<46} {}{alias_suffix}{resume}",
|
||||
spec.summary,
|
||||
name = render_slash_command_name(spec),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_slash_command_name(spec: &SlashCommandSpec) -> String {
|
||||
match spec.argument_hint {
|
||||
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
||||
None => format!("/{}", spec.name),
|
||||
}
|
||||
}
|
||||
|
||||
fn levenshtein_distance(left: &str, right: &str) -> usize {
|
||||
if left == right {
|
||||
return 0;
|
||||
}
|
||||
if left.is_empty() {
|
||||
return right.chars().count();
|
||||
}
|
||||
if right.is_empty() {
|
||||
return left.chars().count();
|
||||
}
|
||||
|
||||
let right_chars = right.chars().collect::<Vec<_>>();
|
||||
let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
|
||||
let mut current = vec![0; right_chars.len() + 1];
|
||||
|
||||
for (left_index, left_char) in left.chars().enumerate() {
|
||||
current[0] = left_index + 1;
|
||||
for (right_index, right_char) in right_chars.iter().enumerate() {
|
||||
let cost = usize::from(left_char != *right_char);
|
||||
current[right_index + 1] = (previous[right_index + 1] + 1)
|
||||
.min(current[right_index] + 1)
|
||||
.min(previous[right_index] + cost);
|
||||
}
|
||||
std::mem::swap(&mut previous, &mut current);
|
||||
}
|
||||
|
||||
previous[right_chars.len()]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn suggest_slash_commands(input: &str, limit: usize) -> Vec<String> {
|
||||
let normalized = input.trim().trim_start_matches('/').to_ascii_lowercase();
|
||||
if normalized.is_empty() || limit == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut ranked = slash_command_specs()
|
||||
.iter()
|
||||
.filter_map(|spec| {
|
||||
let score = std::iter::once(spec.name)
|
||||
.chain(spec.aliases.iter().copied())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.filter_map(|alias| {
|
||||
if alias == normalized {
|
||||
Some((0_usize, alias.len()))
|
||||
} else if alias.starts_with(&normalized) {
|
||||
Some((1, alias.len()))
|
||||
} else if alias.contains(&normalized) {
|
||||
Some((2, alias.len()))
|
||||
} else {
|
||||
let distance = levenshtein_distance(&alias, &normalized);
|
||||
(distance <= 2).then_some((3 + distance, alias.len()))
|
||||
}
|
||||
})
|
||||
.min();
|
||||
|
||||
score.map(|(bucket, len)| (bucket, len, render_slash_command_name(spec)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ranked.sort_by(|left, right| left.cmp(right));
|
||||
ranked.dedup_by(|left, right| left.2 == right.2);
|
||||
ranked
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(_, _, display)| display)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SlashCommandResult {
|
||||
pub message: String,
|
||||
@@ -1652,7 +1795,8 @@ mod tests {
|
||||
handle_worktree_slash_command, load_agents_from_roots, load_skills_from_roots,
|
||||
render_agents_report, render_plugins_report, render_skills_report,
|
||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
||||
CommitPushPrRequest, DefinitionSource, SkillOrigin, SkillRoot, SlashCommand,
|
||||
suggest_slash_commands, CommitPushPrRequest, DefinitionSource, SkillOrigin, SkillRoot,
|
||||
SlashCommand,
|
||||
};
|
||||
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
||||
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
||||
@@ -1965,6 +2109,11 @@ mod tests {
|
||||
fn renders_help_from_shared_specs() {
|
||||
let help = render_slash_command_help();
|
||||
assert!(help.contains("works with --resume SESSION.json"));
|
||||
assert!(help.contains("Core flow"));
|
||||
assert!(help.contains("Workspace & memory"));
|
||||
assert!(help.contains("Sessions & output"));
|
||||
assert!(help.contains("Git & GitHub"));
|
||||
assert!(help.contains("Automation & discovery"));
|
||||
assert!(help.contains("/help"));
|
||||
assert!(help.contains("/status"));
|
||||
assert!(help.contains("/compact"));
|
||||
@@ -2000,6 +2149,13 @@ mod tests {
|
||||
assert_eq!(resume_supported_slash_commands().len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_close_slash_commands() {
|
||||
let suggestions = suggest_slash_commands("stats", 3);
|
||||
assert!(!suggestions.is_empty());
|
||||
assert_eq!(suggestions[0], "/status");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compacts_sessions_via_slash_command() {
|
||||
let session = Session {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# Claw Code 0.1.0 release notes (draft)
|
||||
|
||||
## Summary
|
||||
|
||||
Claw Code `0.1.0` is the first public release-prep milestone for the current Rust implementation. Claw Code is Claude Code inspired and built as a clean-room Rust implementation; it is not a direct port or copy. This release centers on a usable local CLI experience: interactive sessions, non-interactive prompts, workspace tools, configuration loading, sessions, plugins, and local agent/skill discovery.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Initial public `0.1.0` release candidate for Claw Code
|
||||
- Safe-Rust implementation as the current primary product surface
|
||||
- `claw` CLI for interactive and one-shot coding-agent workflows
|
||||
- Built-in workspace tools for shell, file operations, search, web fetch/search, todo tracking, and notebook updates
|
||||
- Slash-command surface for status, compaction, config inspection, sessions, diff/export, and version info
|
||||
- Local plugin, agent, and skill discovery/management surfaces
|
||||
- OAuth login/logout plus model/provider selection
|
||||
|
||||
## Install and run
|
||||
|
||||
This release is currently intended for source builds:
|
||||
|
||||
```bash
|
||||
cargo install --path crates/claw-cli --locked
|
||||
# or
|
||||
cargo build --release -p claw-cli
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
claw
|
||||
claw prompt "summarize this repository"
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Source-build distribution only; packaged release artifacts are not yet published
|
||||
- CI currently covers Ubuntu and macOS release builds, checks, and tests
|
||||
- Windows release readiness is not yet established
|
||||
- Some integration coverage is opt-in because live provider credentials and network access are required
|
||||
- Public interfaces may continue to evolve during the `0.x` release line
|
||||
|
||||
## Recommended release framing
|
||||
|
||||
Position `0.1.0` as the first public release of Claw Code in its current Rust implementation for early adopters who are comfortable building from source. The feature surface is broad enough for real usage, while packaging and release automation can continue to improve in later releases.
|
||||
|
||||
## Verification used for this draft
|
||||
|
||||
- Workspace version verified from `Cargo.toml`
|
||||
- `claw` binary/package path verified from `cargo metadata`
|
||||
- CLI command surface verified from `cargo run --quiet --bin claw -- --help`
|
||||
- CI coverage verified from `.github/workflows/ci.yml`
|
||||
Reference in New Issue
Block a user