Compare commits
1 Commits
rcc/runtim
...
rcc/doctor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b200198df7 |
@@ -133,7 +133,6 @@ Inside the REPL, useful commands include:
|
|||||||
/diff
|
/diff
|
||||||
/version
|
/version
|
||||||
/export notes.txt
|
/export notes.txt
|
||||||
/sessions
|
|
||||||
/session list
|
/session list
|
||||||
/exit
|
/exit
|
||||||
```
|
```
|
||||||
@@ -144,14 +143,14 @@ Inspect or maintain a saved session file without entering the REPL:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd rust
|
cd rust
|
||||||
cargo run -p rusty-claude-cli -- --resume session-123456 /status /compact /cost
|
cargo run -p rusty-claude-cli -- --resume session.json /status /compact /cost
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also inspect memory/config state for a restored session:
|
You can also inspect memory/config state for a restored session:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd rust
|
cd rust
|
||||||
cargo run -p rusty-claude-cli -- --resume ~/.claude/sessions/session-123456.json /memory /config
|
cargo run -p rusty-claude-cli -- --resume session.json /memory /config
|
||||||
```
|
```
|
||||||
|
|
||||||
## Available commands
|
## Available commands
|
||||||
@@ -159,7 +158,7 @@ cargo run -p rusty-claude-cli -- --resume ~/.claude/sessions/session-123456.json
|
|||||||
### Top-level CLI commands
|
### Top-level CLI commands
|
||||||
|
|
||||||
- `prompt <text...>` — run one prompt non-interactively
|
- `prompt <text...>` — run one prompt non-interactively
|
||||||
- `--resume <session-id-or-path> [/commands...]` — inspect or maintain a saved session stored under `~/.claude/sessions/`
|
- `--resume <session.json> [/commands...]` — inspect or maintain a saved session
|
||||||
- `dump-manifests` — print extracted upstream manifest counts
|
- `dump-manifests` — print extracted upstream manifest counts
|
||||||
- `bootstrap-plan` — print the current bootstrap skeleton
|
- `bootstrap-plan` — print the current bootstrap skeleton
|
||||||
- `system-prompt [--cwd PATH] [--date YYYY-MM-DD]` — render the synthesized system prompt
|
- `system-prompt [--cwd PATH] [--date YYYY-MM-DD]` — render the synthesized system prompt
|
||||||
@@ -177,14 +176,13 @@ cargo run -p rusty-claude-cli -- --resume ~/.claude/sessions/session-123456.json
|
|||||||
- `/permissions [read-only|workspace-write|danger-full-access]` — inspect or switch permissions
|
- `/permissions [read-only|workspace-write|danger-full-access]` — inspect or switch permissions
|
||||||
- `/clear [--confirm]` — clear the current local session
|
- `/clear [--confirm]` — clear the current local session
|
||||||
- `/cost` — show token usage totals
|
- `/cost` — show token usage totals
|
||||||
- `/resume <session-id-or-path>` — load a saved session into the REPL
|
- `/resume <session-path>` — load a saved session into the REPL
|
||||||
- `/config [env|hooks|model]` — inspect discovered Claude config
|
- `/config [env|hooks|model]` — inspect discovered Claude config
|
||||||
- `/memory` — inspect loaded instruction memory files
|
- `/memory` — inspect loaded instruction memory files
|
||||||
- `/init` — create a starter `CLAUDE.md`
|
- `/init` — create a starter `CLAUDE.md`
|
||||||
- `/diff` — show the current git diff for the workspace
|
- `/diff` — show the current git diff for the workspace
|
||||||
- `/version` — print version and build metadata locally
|
- `/version` — print version and build metadata locally
|
||||||
- `/export [file]` — export the current conversation transcript
|
- `/export [file]` — export the current conversation transcript
|
||||||
- `/sessions` — list recent managed local sessions from `~/.claude/sessions/`
|
|
||||||
- `/session [list|switch <session-id>]` — inspect or switch managed local sessions
|
- `/session [list|switch <session-id>]` — inspect or switch managed local sessions
|
||||||
- `/exit` — leave the REPL
|
- `/exit` — leave the REPL
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "resume",
|
name: "resume",
|
||||||
summary: "Load a saved session into the REPL",
|
summary: "Load a saved session into the REPL",
|
||||||
argument_hint: Some("<session-id-or-path>"),
|
argument_hint: Some("<session-path>"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
@@ -129,12 +129,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
argument_hint: Some("[list|switch <session-id>]"),
|
argument_hint: Some("[list|switch <session-id>]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
|
||||||
name: "sessions",
|
|
||||||
summary: "List recent managed local sessions",
|
|
||||||
argument_hint: None,
|
|
||||||
resume_supported: false,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -169,7 +163,6 @@ pub enum SlashCommand {
|
|||||||
action: Option<String>,
|
action: Option<String>,
|
||||||
target: Option<String>,
|
target: Option<String>,
|
||||||
},
|
},
|
||||||
Sessions,
|
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +207,6 @@ impl SlashCommand {
|
|||||||
action: parts.next().map(ToOwned::to_owned),
|
action: parts.next().map(ToOwned::to_owned),
|
||||||
target: parts.next().map(ToOwned::to_owned),
|
target: parts.next().map(ToOwned::to_owned),
|
||||||
},
|
},
|
||||||
"sessions" => Self::Sessions,
|
|
||||||
other => Self::Unknown(other.to_string()),
|
other => Self::Unknown(other.to_string()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -299,7 +291,6 @@ pub fn handle_slash_command(
|
|||||||
| SlashCommand::Version
|
| SlashCommand::Version
|
||||||
| SlashCommand::Export { .. }
|
| SlashCommand::Export { .. }
|
||||||
| SlashCommand::Session { .. }
|
| SlashCommand::Session { .. }
|
||||||
| SlashCommand::Sessions
|
|
||||||
| SlashCommand::Unknown(_) => None,
|
| SlashCommand::Unknown(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,10 +365,6 @@ mod tests {
|
|||||||
target: Some("abc123".to_string())
|
target: Some("abc123".to_string())
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
SlashCommand::parse("/sessions"),
|
|
||||||
Some(SlashCommand::Sessions)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -391,7 +378,7 @@ mod tests {
|
|||||||
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
|
||||||
assert!(help.contains("/clear [--confirm]"));
|
assert!(help.contains("/clear [--confirm]"));
|
||||||
assert!(help.contains("/cost"));
|
assert!(help.contains("/cost"));
|
||||||
assert!(help.contains("/resume <session-id-or-path>"));
|
assert!(help.contains("/resume <session-path>"));
|
||||||
assert!(help.contains("/config [env|hooks|model]"));
|
assert!(help.contains("/config [env|hooks|model]"));
|
||||||
assert!(help.contains("/memory"));
|
assert!(help.contains("/memory"));
|
||||||
assert!(help.contains("/init"));
|
assert!(help.contains("/init"));
|
||||||
@@ -399,8 +386,7 @@ mod tests {
|
|||||||
assert!(help.contains("/version"));
|
assert!(help.contains("/version"));
|
||||||
assert!(help.contains("/export [file]"));
|
assert!(help.contains("/export [file]"));
|
||||||
assert!(help.contains("/session [list|switch <session-id>]"));
|
assert!(help.contains("/session [list|switch <session-id>]"));
|
||||||
assert!(help.contains("/sessions"));
|
assert_eq!(slash_command_specs().len(), 15);
|
||||||
assert_eq!(slash_command_specs().len(), 16);
|
|
||||||
assert_eq!(resume_supported_slash_commands().len(), 11);
|
assert_eq!(resume_supported_slash_commands().len(), 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +404,6 @@ mod tests {
|
|||||||
text: "recent".to_string(),
|
text: "recent".to_string(),
|
||||||
}]),
|
}]),
|
||||||
],
|
],
|
||||||
metadata: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = handle_slash_command(
|
let result = handle_slash_command(
|
||||||
@@ -483,6 +468,5 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
handle_slash_command("/session list", &session, CompactionConfig::default()).is_none()
|
handle_slash_command("/session list", &session, CompactionConfig::default()).is_none()
|
||||||
);
|
);
|
||||||
assert!(handle_slash_command("/sessions", &session, CompactionConfig::default()).is_none());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
|
|||||||
compacted_session: Session {
|
compacted_session: Session {
|
||||||
version: session.version,
|
version: session.version,
|
||||||
messages: compacted_messages,
|
messages: compacted_messages,
|
||||||
metadata: session.metadata.clone(),
|
|
||||||
},
|
},
|
||||||
removed_message_count: removed.len(),
|
removed_message_count: removed.len(),
|
||||||
}
|
}
|
||||||
@@ -394,7 +393,6 @@ mod tests {
|
|||||||
let session = Session {
|
let session = Session {
|
||||||
version: 1,
|
version: 1,
|
||||||
messages: vec![ConversationMessage::user_text("hello")],
|
messages: vec![ConversationMessage::user_text("hello")],
|
||||||
metadata: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = compact_session(&session, CompactionConfig::default());
|
let result = compact_session(&session, CompactionConfig::default());
|
||||||
@@ -422,7 +420,6 @@ mod tests {
|
|||||||
usage: None,
|
usage: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
metadata: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = compact_session(
|
let result = compact_session(
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ pub use remote::{
|
|||||||
RemoteSessionContext, UpstreamProxyBootstrap, UpstreamProxyState, DEFAULT_REMOTE_BASE_URL,
|
RemoteSessionContext, UpstreamProxyBootstrap, UpstreamProxyState, DEFAULT_REMOTE_BASE_URL,
|
||||||
DEFAULT_SESSION_TOKEN_PATH, DEFAULT_SYSTEM_CA_BUNDLE, NO_PROXY_HOSTS, UPSTREAM_PROXY_ENV_KEYS,
|
DEFAULT_SESSION_TOKEN_PATH, DEFAULT_SYSTEM_CA_BUNDLE, NO_PROXY_HOSTS, UPSTREAM_PROXY_ENV_KEYS,
|
||||||
};
|
};
|
||||||
pub use session::{
|
pub use session::{ContentBlock, ConversationMessage, MessageRole, Session, SessionError};
|
||||||
ContentBlock, ConversationMessage, MessageRole, Session, SessionError, SessionMetadata,
|
|
||||||
};
|
|
||||||
pub use usage::{
|
pub use usage::{
|
||||||
format_usd, pricing_for_model, ModelPricing, TokenUsage, UsageCostEstimate, UsageTracker,
|
format_usd, pricing_for_model, ModelPricing, TokenUsage, UsageCostEstimate, UsageTracker,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,19 +39,10 @@ pub struct ConversationMessage {
|
|||||||
pub usage: Option<TokenUsage>,
|
pub usage: Option<TokenUsage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct SessionMetadata {
|
|
||||||
pub started_at: String,
|
|
||||||
pub model: String,
|
|
||||||
pub message_count: u32,
|
|
||||||
pub last_prompt: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
pub messages: Vec<ConversationMessage>,
|
pub messages: Vec<ConversationMessage>,
|
||||||
pub metadata: Option<SessionMetadata>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -91,7 +82,6 @@ impl Session {
|
|||||||
Self {
|
Self {
|
||||||
version: 1,
|
version: 1,
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
metadata: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +111,6 @@ impl Session {
|
|||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if let Some(metadata) = &self.metadata {
|
|
||||||
object.insert("metadata".to_string(), metadata.to_json());
|
|
||||||
}
|
|
||||||
JsonValue::Object(object)
|
JsonValue::Object(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,15 +131,7 @@ impl Session {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(ConversationMessage::from_json)
|
.map(ConversationMessage::from_json)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let metadata = object
|
Ok(Self { version, messages })
|
||||||
.get("metadata")
|
|
||||||
.map(SessionMetadata::from_json)
|
|
||||||
.transpose()?;
|
|
||||||
Ok(Self {
|
|
||||||
version,
|
|
||||||
messages,
|
|
||||||
metadata,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,41 +141,6 @@ impl Default for Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionMetadata {
|
|
||||||
#[must_use]
|
|
||||||
pub fn to_json(&self) -> JsonValue {
|
|
||||||
let mut object = BTreeMap::new();
|
|
||||||
object.insert(
|
|
||||||
"started_at".to_string(),
|
|
||||||
JsonValue::String(self.started_at.clone()),
|
|
||||||
);
|
|
||||||
object.insert("model".to_string(), JsonValue::String(self.model.clone()));
|
|
||||||
object.insert(
|
|
||||||
"message_count".to_string(),
|
|
||||||
JsonValue::Number(i64::from(self.message_count)),
|
|
||||||
);
|
|
||||||
if let Some(last_prompt) = &self.last_prompt {
|
|
||||||
object.insert(
|
|
||||||
"last_prompt".to_string(),
|
|
||||||
JsonValue::String(last_prompt.clone()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
JsonValue::Object(object)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_json(value: &JsonValue) -> Result<Self, SessionError> {
|
|
||||||
let object = value.as_object().ok_or_else(|| {
|
|
||||||
SessionError::Format("session metadata must be an object".to_string())
|
|
||||||
})?;
|
|
||||||
Ok(Self {
|
|
||||||
started_at: required_string(object, "started_at")?,
|
|
||||||
model: required_string(object, "model")?,
|
|
||||||
message_count: required_u32(object, "message_count")?,
|
|
||||||
last_prompt: optional_string(object, "last_prompt"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConversationMessage {
|
impl ConversationMessage {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn user_text(text: impl Into<String>) -> Self {
|
pub fn user_text(text: impl Into<String>) -> Self {
|
||||||
@@ -424,13 +368,6 @@ fn required_string(
|
|||||||
.ok_or_else(|| SessionError::Format(format!("missing {key}")))
|
.ok_or_else(|| SessionError::Format(format!("missing {key}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn optional_string(object: &BTreeMap<String, JsonValue>, key: &str) -> Option<String> {
|
|
||||||
object
|
|
||||||
.get(key)
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn required_u32(object: &BTreeMap<String, JsonValue>, key: &str) -> Result<u32, SessionError> {
|
fn required_u32(object: &BTreeMap<String, JsonValue>, key: &str) -> Result<u32, SessionError> {
|
||||||
let value = object
|
let value = object
|
||||||
.get(key)
|
.get(key)
|
||||||
@@ -441,8 +378,7 @@ fn required_u32(object: &BTreeMap<String, JsonValue>, key: &str) -> Result<u32,
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{ContentBlock, ConversationMessage, MessageRole, Session, SessionMetadata};
|
use super::{ContentBlock, ConversationMessage, MessageRole, Session};
|
||||||
use crate::json::JsonValue;
|
|
||||||
use crate::usage::TokenUsage;
|
use crate::usage::TokenUsage;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
@@ -450,12 +386,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn persists_and_restores_session_json() {
|
fn persists_and_restores_session_json() {
|
||||||
let mut session = Session::new();
|
let mut session = Session::new();
|
||||||
session.metadata = Some(SessionMetadata {
|
|
||||||
started_at: "2026-04-01T00:00:00Z".to_string(),
|
|
||||||
model: "claude-sonnet".to_string(),
|
|
||||||
message_count: 3,
|
|
||||||
last_prompt: Some("hello".to_string()),
|
|
||||||
});
|
|
||||||
session
|
session
|
||||||
.messages
|
.messages
|
||||||
.push(ConversationMessage::user_text("hello"));
|
.push(ConversationMessage::user_text("hello"));
|
||||||
@@ -498,23 +428,5 @@ mod tests {
|
|||||||
restored.messages[1].usage.expect("usage").total_tokens(),
|
restored.messages[1].usage.expect("usage").total_tokens(),
|
||||||
17
|
17
|
||||||
);
|
);
|
||||||
assert_eq!(restored.metadata, session.metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn loads_legacy_session_without_metadata() {
|
|
||||||
let legacy = r#"{
|
|
||||||
"version": 1,
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"blocks": [{"type": "text", "text": "hello"}]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"#;
|
|
||||||
let restored = Session::from_json(&JsonValue::parse(legacy).expect("legacy json"))
|
|
||||||
.expect("legacy session should parse");
|
|
||||||
assert_eq!(restored.messages.len(), 1);
|
|
||||||
assert!(restored.metadata.is_none());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,6 @@ mod tests {
|
|||||||
cache_read_input_tokens: 0,
|
cache_read_input_tokens: 0,
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
metadata: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let tracker = UsageTracker::from_session(&session);
|
let tracker = UsageTracker::from_session(&session);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user