Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 71 additions & 6 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,18 @@ enum Cmd {
format: String,
},

/// Delete a memory item by ID.
///
/// Shows item details before deleting. Dry-run by default; pass --confirm to actually delete.
/// Cleans up embeddings, edges, and vector data associated with the item.
Forget {
/// The ID of the memory item to delete.
id: String,
/// Actually delete (default is dry-run showing what would be deleted).
#[arg(long)]
confirm: bool,
},

/// Sync local database with Turso Cloud (embedded replica).
///
/// Requires WAGL_SYNC_URL and WAGL_SYNC_TOKEN environment variables.
Expand Down Expand Up @@ -1893,7 +1905,7 @@ fn main() -> anyhow::Result<()> {
findings.push(serde_json::json!({
"type": "expired_hypothesis",
"id": it.id,
"text_preview": &it.text[..std::cmp::min(120, it.text.len())],
"text_preview": it.text.chars().take(120).collect::<String>(),
"expired_at": exp,
"message": "hypothesis has expired and needs revalidation",
"suggested_action": "Review this hypothesis: update if still valid, delete if disproven, or extend the expiration."
Expand Down Expand Up @@ -3084,8 +3096,8 @@ fn main() -> anyhow::Result<()> {
"relation": relation,
"weight": weight
},
"source_summary": format!("[{}] {}", src_item.r#type, &src_item.text[..std::cmp::min(80, src_item.text.len())]),
"target_summary": format!("[{}] {}", tgt_item.r#type, &tgt_item.text[..std::cmp::min(80, tgt_item.text.len())])
"source_summary": format!("[{}] {}", src_item.r#type, src_item.text.chars().take(80).collect::<String>()),
"target_summary": format!("[{}] {}", tgt_item.r#type, tgt_item.text.chars().take(80).collect::<String>())
}))?;
}

Expand All @@ -3106,7 +3118,7 @@ fn main() -> anyhow::Result<()> {
let direction = if edge.source_id == id { "outgoing" } else { "incoming" };
let neighbor_item = db.get(neighbor_id).await?;
let summary = neighbor_item.as_ref().map(|it| {
format!("[{}] {}", it.r#type, &it.text[..std::cmp::min(80, it.text.len())])
format!("[{}] {}", it.r#type, it.text.chars().take(80).collect::<String>())
}).unwrap_or_else(|| "(item not found)".to_string());
neighbors.push(serde_json::json!({
"id": neighbor_id,
Expand Down Expand Up @@ -3144,7 +3156,7 @@ fn main() -> anyhow::Result<()> {
next_layer.push(neighbor_id.clone());
let neighbor_item = db.get(neighbor_id).await?;
let summary = neighbor_item.as_ref().map(|it| {
format!("[{}] {}", it.r#type, &it.text[..std::cmp::min(80, it.text.len())])
format!("[{}] {}", it.r#type, it.text.chars().take(80).collect::<String>())
}).unwrap_or_else(|| "(item not found)".to_string());
all_results.push(serde_json::json!({
"id": neighbor_id,
Expand Down Expand Up @@ -3668,7 +3680,7 @@ fn main() -> anyhow::Result<()> {
"type": it.r#type,
"salience": it.salience,
"created_at": it.created_at,
"text_preview": &it.text[..std::cmp::min(80, it.text.len())]
"text_preview": it.text.chars().take(80).collect::<String>()
}))
.collect();

Expand Down Expand Up @@ -4017,6 +4029,59 @@ fn main() -> anyhow::Result<()> {
}
}

Cmd::Forget { id, confirm } => {
ensure_parent_dir(&db_path)?;
let db = MemoryDb::connect_auto(&db_path).await?;
db.init().await?;

let item = db.get(&id).await?;
let Some(item) = item else {
if !json_mode {
eprintln!("Error: memory item not found: {}", id);
}
Comment on lines +4038 to +4041
Copy link

Choose a reason for hiding this comment

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

Inconsistent exit code when item is not found in non-JSON mode

When the item is missing and json_mode = true, a CliError::NotFound is returned, which the top-level handler maps to exit code 2. When json_mode = false, the code calls print_ok(false, …) and returns Ok(()), which exits with code 0.

For a destructive command like forget, scripts that do not use --json and rely on exit-code semantics will silently receive a success status even when the target item never existed. Emitting a user-readable message to stderr and using a non-zero exit in this branch (consistent with how Cmd::Sync handles misconfiguration) would make the behaviour match callers' expectations.

return Err(anyhow::Error::new(CliError::NotFound(format!(
"memory item not found: {}",
id
))));
};

let text_preview: String = item.text.chars().take(120).collect();
let preview = serde_json::json!({
"id": item.id,
"type": item.r#type,
"salience": item.salience,
"created_at": item.created_at,
"tags": item.tags,
"text_preview": text_preview
});

if confirm {
let affected = db.delete(&id).await?;
if affected == 0 {
if !json_mode {
eprintln!("Error: memory item {} disappeared before deletion", id);
}
return Err(anyhow::Error::new(CliError::NotFound(format!(
"memory item {} disappeared before deletion",
id
))));
}
print_ok(json_mode, serde_json::json!({
"deleted": true,
"item": preview
}))?;
if !json_mode {
eprintln!("deleted memory item {}", id);
}
} else {
print_ok(json_mode, serde_json::json!({
"mode": "dry-run",
"item": preview,
"hint": "use --confirm to actually delete"
}))?;
}
}

Cmd::Sync => {
ensure_parent_dir(&db_path)?;
let db = MemoryDb::connect_auto(&db_path).await?;
Expand Down
5 changes: 3 additions & 2 deletions crates/db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2125,11 +2125,12 @@ impl MemoryDb {
let pe = item.primary_emotion.as_deref();
let affected = self.conn
.execute(
"UPDATE memory_items SET type = ?1, text = ?2, tags = ?3, salience = ?4, primary_emotion = ?5, secondary_emotions = ?6, d_score = ?7, i_score = ?8, ev = ?9, files = ?10 WHERE id = ?11",
"UPDATE memory_items SET type = ?1, text = ?2, tags = ?3, salience = ?4, primary_emotion = ?5, secondary_emotions = ?6, d_score = ?7, i_score = ?8, ev = ?9, files = ?10, actionable = ?11 WHERE id = ?12",
params![
type_str, text_str, tags_json, item.salience,
pe, secondary_json,
item.d_score, item.i_score, item.ev, files_json, id_str
item.d_score, item.i_score, item.ev, files_json,
item.actionable, id_str
],
)
.await
Expand Down
Loading