Skip to content

Comments

feat: add memory system, CLI version check, and skills batch enable#33

Open
zhuhu00 wants to merge 2 commits intoSaladDay:mainfrom
zhuhu00:main
Open

feat: add memory system, CLI version check, and skills batch enable#33
zhuhu00 wants to merge 2 commits intoSaladDay:mainfrom
zhuhu00:main

Conversation

@zhuhu00
Copy link

@zhuhu00 zhuhu00 commented Feb 19, 2026

Summary

This PR adds three new features to cc-switch:

1. Local Memory System (cc-switch memory)

A SQLite + FTS5-based local knowledge memory system for capturing and retrieving session context.

Subcommands:

  • memory add <title> - Add observations with type (decision/error/pattern/preference/general), tags, and project association
  • memory list - List observations with filters (type, project, limit)
  • memory show <id> - View observation details
  • memory search <query> - Full-text search via FTS5
  • memory delete <id> - Delete observations
  • memory stats - Show statistics (total count, tokens, type distribution)
  • memory context - Progressive disclosure context retrieval with token budget (priority: FTS match > project match > recent)
  • memory sessions - List session history

Claude Code Hooks Integration:

  • memory hooks register/unregister/status - Manage Claude Code hooks
  • SessionStart hook: Creates new session and outputs relevant context
  • PostToolUse hook: Automatically captures observations from tool usage:
    • Write/Edit → recorded as pattern
    • Bash errors → recorded as error
    • GitHub MCP tools (create_pull_request, merge_pull_request, create_issue, create_branch, push_files, create_or_update_file) → recorded as decision/pattern/general

Storage: ~/.cc-switch/memory.db with auto-synced FTS5 index via triggers.

2. CLI Version Check (cc-switch check)

Check installed versions and available updates for AI CLI tools.

  • cc-switch check updates - Compare local vs latest versions for Claude Code, Codex, Gemini, OpenCode, Qwen Code
  • cc-switch check updates -a claude - Check specific tool only
  • cc-switch check updates --json - JSON output for scripting
  • cc-switch check upgrade --yes - Upgrade all tools to latest

3. Skills Batch Enable

Added batch enable/disable functionality to the skills command for managing multiple skills at once.

Files Changed

File Description
src/cli/commands/check.rs New: CLI version check command implementation
src/cli/commands/memory.rs New: Memory CLI command handlers
src/services/memory.rs New: Memory service (SQLite, FTS5, hooks, context)
src/cli/commands/skills.rs Modified: Added batch enable/disable
src/cli/commands/mod.rs Modified: Register new command modules
src/cli/mod.rs Modified: Add Memory and Check to CLI enum
src/main.rs Modified: Route new commands
src/lib.rs Modified: Export MemoryService
src/services/mod.rs Modified: Register memory module

Test Results

All features manually tested and verified:

  • cc-switch memory add "test" -c "content" -t general --tags "test" - creates observation
  • cc-switch memory list - lists all observations with table
  • cc-switch memory show <id> - displays full observation details
  • cc-switch memory search "query" - FTS5 search returns matched results
  • cc-switch memory delete <id> - deletes and confirmed via search
  • cc-switch memory stats - shows total count, tokens, type distribution
  • cc-switch memory sessions - lists session history
  • cc-switch memory context --max-tokens 1000 - progressive disclosure works
  • cc-switch memory hooks status - correctly reports registration state
  • cc-switch check updates - shows version table for Claude Code, Codex, Gemini, OpenCode, Qwen Code
  • cc-switch check upgrade --help - upgrade command available
  • cargo build --release - compiles without errors

🤖 Generated with Claude Code

zhuhu00 and others added 2 commits February 10, 2026 11:53
…e hooks

- Add SQLite-based memory storage at ~/.cc-switch/memory.db
- Support observation types: decision, error, pattern, preference, general
- FTS5 full-text search for semantic queries
- Progressive disclosure context with token budget
- Claude Code hooks integration (SessionStart, PostToolUse)
- Track GitHub operations: PR create/merge, issue create, branch create, push
- Session tracking for conversation history

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds three significant new features to cc-switch: a local memory system for context capture, CLI version checking for AI tools, and batch operations for skill management.

Changes:

  • Adds SQLite + FTS5-based memory system for capturing and retrieving session context with Claude Code hooks integration
  • Implements CLI version checking and upgrading for AI tools (Claude Code, Codex, Gemini, OpenCode, Qwen Code)
  • Adds batch enable/disable functionality for skills management

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 21 comments.

Show a summary per file
File Description
src-tauri/src/services/memory.rs New memory service implementing SQLite database, FTS5 search, hooks integration, and progressive context disclosure
src-tauri/src/cli/commands/memory.rs CLI command handlers for memory operations (add, list, search, delete, stats, context, hooks, sessions)
src-tauri/src/cli/commands/check.rs New CLI version check command for comparing and upgrading installed AI CLI tools
src-tauri/src/cli/commands/skills.rs Added EnableAll and DisableAll commands for batch skill management
src-tauri/src/services/mod.rs Registers memory module and exports MemoryService
src-tauri/src/cli/commands/mod.rs Registers check and memory command modules
src-tauri/src/cli/mod.rs Adds Memory and Check to CLI enum
src-tauri/src/main.rs Routes new commands to their handlers
src-tauri/src/lib.rs Exports MemoryService for external use

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.collect(),
tokens: row.get(6)?,
relevance_score: row.get(7)?,
created_at: Utc.timestamp_opt(row.get(8)?, 0).unwrap(),
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timestamp_opt call uses .unwrap() which will panic if the timestamp is invalid. This could crash the application if corrupted data exists in the database. Consider using single() or proper error handling instead to return an AppError.

Copilot uses AI. Check for mistakes.
Comment on lines +608 to +611
started_at: Utc.timestamp_opt(row.get(3)?, 0).unwrap(),
ended_at: row
.get::<_, Option<i64>>(4)?
.map(|ts| Utc.timestamp_opt(ts, 0).unwrap()),
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple timestamp_opt().unwrap() calls will panic if timestamps are invalid. Consider using single() or proper error handling to return an AppError instead of panicking.

Copilot uses AI. Check for mistakes.
Comment on lines +411 to +413
// Escape query for FTS5
let escaped_query = query.replace('"', "\"\"");
let fts_query = format!("\"{}\"", escaped_query);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FTS5 query escaping only handles double quotes, but FTS5 has other special characters that may cause issues. Consider using parameterized queries or a more robust escaping mechanism. Additionally, wrapping all queries in double quotes may not work correctly for phrase searches or complex queries with operators like AND, OR, NOT.

Copilot uses AI. Check for mistakes.
Comment on lines +228 to +231
pb.set_style(
indicatif::ProgressStyle::default_bar()
.template("{spinner:.cyan} [{pos}/{len}] Checking {msg}...")
.unwrap(),
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progress bar template uses .unwrap() which will panic if the template string is invalid. While the template looks correct, consider handling this error more gracefully or document that this is intentional for developer feedback during development.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +52
impl VersionStatus {
fn display(&self) -> &'static str {
match self {
VersionStatus::Latest => "最新",
VersionStatus::Upgradable => "可升级",
VersionStatus::NotInstalled => "未安装",
VersionStatus::Unknown => "未知",
VersionStatus::FetchFailed => "获取失败",
}
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Chinese text for status display uses hardcoded Chinese strings ("最新", "可升级", etc.) instead of using the i18n system. This is inconsistent with the rest of the codebase which uses the i18n module for internationalization. Consider using the i18n system for consistency.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,1023 @@
//! Memory service for session context capture and semantic search.
//!
//! Uses SQLite + FTS5 for full-text search. Database stored at `~/.cc-switch/memory.db`.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module-level documentation states the database is stored at ~/.cc-switch/memory.db, but the actual path comes from get_app_config_dir() which may not necessarily be ~/.cc-switch/. Consider updating the documentation to be more accurate or reference the configuration system.

Suggested change
//! Uses SQLite + FTS5 for full-text search. Database stored at `~/.cc-switch/memory.db`.
//! Uses SQLite + FTS5 for full-text search. Database is stored in the app config directory
//! (for example, `~/.cc-switch/memory.db` by default) as determined by `get_app_config_dir()`.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +155
if let Some(conn) = DB_CONNECTION.get() {
return Ok(conn);
}

let path = get_db_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}

let conn = Connection::open(&path)
.map_err(|e| AppError::Message(format!("Failed to open memory database: {e}")))?;

init_schema(&conn)?;

// If another thread raced us, that's fine — just use theirs.
let _ = DB_CONNECTION.set(Mutex::new(conn));
Ok(DB_CONNECTION.get().unwrap())
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global database connection using OnceLock<Mutex<Connection>> has a potential race condition at initialization. If two threads call get_connection() simultaneously before initialization completes, both will attempt to create connections. While the second one is discarded, this could lead to issues. Consider using get_or_try_init to handle errors more gracefully, or add explicit synchronization.

Suggested change
if let Some(conn) = DB_CONNECTION.get() {
return Ok(conn);
}
let path = get_db_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let conn = Connection::open(&path)
.map_err(|e| AppError::Message(format!("Failed to open memory database: {e}")))?;
init_schema(&conn)?;
// If another thread raced us, that's fine — just use theirs.
let _ = DB_CONNECTION.set(Mutex::new(conn));
Ok(DB_CONNECTION.get().unwrap())
DB_CONNECTION.get_or_try_init(|| {
let path = get_db_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let conn = Connection::open(&path)
.map_err(|e| AppError::Message(format!("Failed to open memory database: {e}")))?;
init_schema(&conn)?;
Ok(Mutex::new(conn))
})

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +232
/// Estimate token count for text (rough approximation: ~4 chars per token)
fn estimate_tokens(text: &str) -> i32 {
(text.len() as f64 / 4.0).ceil() as i32
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token estimation uses a rough approximation of 4 characters per token, which can be inaccurate for non-English text (especially CJK characters) and code. This could lead to budget overruns or underutilization. Consider documenting this limitation or using a more accurate tokenizer if precise token counts are important.

Suggested change
/// Estimate token count for text (rough approximation: ~4 chars per token)
fn estimate_tokens(text: &str) -> i32 {
(text.len() as f64 / 4.0).ceil() as i32
/// Estimate token count for text.
///
/// This first tries to use a real tokenizer (OpenAI `cl100k_base` via `tiktoken-rs`)
/// to compute an accurate token count. If the tokenizer cannot be initialized or
/// encoding fails for any reason, it falls back to a rough heuristic of
/// approximately 4 characters per token, using Unicode scalar values
/// (`text.chars().count()`) rather than raw bytes. The fallback is still only
/// an approximation, especially for non-English/CJK text and code, so callers
/// should not rely on it for strict budgeting when exact token counts are required.
fn estimate_tokens(text: &str) -> i32 {
// Try a real tokenizer first; fall back to heuristic on any error.
if let Ok(bpe) = tiktoken_rs::cl100k_base() {
if let Ok(tokens) = bpe.encode_with_special_tokens(text) {
return tokens.len() as i32;
}
}
// Fallback: rough estimate based on Unicode scalar values (~4 chars per token).
let char_count = text.chars().count() as f64;
char_count.div_euclid(4.0) as i32 + if (char_count % 4.0).abs() > f64::EPSILON { 1 } else { 0 }

Copilot uses AI. Check for mistakes.
Comment on lines +1010 to +1021
arr.retain(|item| {
if let Some(hooks) = item.get("hooks").and_then(|h| h.as_array()) {
for hook in hooks {
if let Some(cmd) = hook.get("command").and_then(|c| c.as_str()) {
if cmd.contains("cc-switch memory") {
return false;
}
}
}
}
true
});
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook removal logic only checks if the command contains "cc-switch memory" but doesn't remove the item if the hooks array becomes empty. This could leave empty hook matchers in the configuration. Consider also removing the matcher if its hooks array is empty after removal.

Suggested change
arr.retain(|item| {
if let Some(hooks) = item.get("hooks").and_then(|h| h.as_array()) {
for hook in hooks {
if let Some(cmd) = hook.get("command").and_then(|c| c.as_str()) {
if cmd.contains("cc-switch memory") {
return false;
}
}
}
}
true
});
// Rebuild the array while removing only hooks whose command contains
// "cc-switch memory". If an item's hooks array becomes empty after
// removal, drop the entire item.
let mut new_items = Vec::with_capacity(arr.len());
for mut item in arr.drain(..) {
let mut remove_item = false;
if let Some(hooks_value) = item.get_mut("hooks") {
if let Some(hooks_arr) = hooks_value.as_array_mut() {
hooks_arr.retain(|hook| {
if let Some(cmd) = hook.get("command").and_then(|c| c.as_str()) {
// Keep hooks whose command does NOT contain "cc-switch memory"
!cmd.contains("cc-switch memory")
} else {
true
}
});
if hooks_arr.is_empty() {
// If no hooks remain, remove this item entirely.
remove_item = true;
}
}
}
if !remove_item {
new_items.push(item);
}
}
*arr = new_items;

Copilot uses AI. Check for mistakes.
Comment on lines +463 to +536
/// Get context with progressive disclosure and token budget
pub fn get_context(
query: Option<&str>,
max_tokens: i32,
project_dir: Option<&str>,
) -> Result<Vec<ContextItem>, AppError> {
let mut items: Vec<ContextItem> = Vec::new();
let mut used_tokens = 0;
let mut seen_ids = std::collections::HashSet::new();

// Layer 1: FTS5 matches (highest priority)
if let Some(q) = query {
if !q.trim().is_empty() {
let search_results = Self::search(q, Some(10))?;
for obs in search_results {
if used_tokens + obs.tokens > max_tokens {
continue;
}
if seen_ids.contains(&obs.id) {
continue;
}
seen_ids.insert(obs.id);
used_tokens += obs.tokens;
items.push(ContextItem {
observation: obs,
priority: 1,
match_reason: "FTS match".to_string(),
});
}
}
}

// Layer 2: Project-specific observations
if let Some(proj) = project_dir {
let project_obs = Self::list_observations(Some(20), None, Some(proj))?;
for obs in project_obs {
if used_tokens + obs.tokens > max_tokens {
continue;
}
if seen_ids.contains(&obs.id) {
continue;
}
seen_ids.insert(obs.id);
used_tokens += obs.tokens;
items.push(ContextItem {
observation: obs,
priority: 2,
match_reason: "Project match".to_string(),
});
}
}

// Layer 3: Recent observations (lowest priority)
let recent = Self::list_observations(Some(50), None, None)?;
for obs in recent {
if used_tokens + obs.tokens > max_tokens {
continue;
}
if seen_ids.contains(&obs.id) {
continue;
}
seen_ids.insert(obs.id);
used_tokens += obs.tokens;
items.push(ContextItem {
observation: obs,
priority: 3,
match_reason: "Recent".to_string(),
});
}

// Sort by priority (lower number = higher priority)
items.sort_by(|a, b| a.priority.cmp(&b.priority));

Ok(items)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progressive disclosure context retrieval performs three separate database queries sequentially (FTS search, project-specific, recent observations), each potentially returning 10-50 results that are then filtered. This could be inefficient for large databases. Consider using a single SQL query with UNION and proper ordering/filtering to reduce database round-trips.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant