buildwithnexus 0.10.3 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,7 +49,7 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
49
49
 
50
50
  [[package]]
51
51
  name = "buildwithnexus"
52
- version = "0.10.3"
52
+ version = "0.10.4"
53
53
  dependencies = [
54
54
  "criterion",
55
55
  "crossterm",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "buildwithnexus"
3
- version = "0.10.3"
3
+ version = "0.10.4"
4
4
  edition = "2021"
5
5
  description = "A hilariously fast agentic AI CLI harness — remote or local models, full TUI with modes, memory, skills, hooks, and image input"
6
6
  license = "MIT"
@@ -134,9 +134,10 @@ pub enum Permission {
134
134
  }
135
135
 
136
136
  pub fn permission(s: &str) -> Permission {
137
- match s {
138
- "auto" => Permission::Auto,
139
- "readonly" => Permission::ReadOnly,
137
+ let normalized = s.trim().to_ascii_lowercase().replace(['_', '-'], "");
138
+ match normalized.as_str() {
139
+ "auto" | "acceptedits" | "acceptedit" | "bypasspermissions" => Permission::Auto,
140
+ "readonly" | "read" | "plan" | "dontask" => Permission::ReadOnly,
140
141
  _ => Permission::Ask,
141
142
  }
142
143
  }
@@ -200,7 +201,8 @@ fn context_prefix() -> String {
200
201
 
201
202
  fn tool_manifest() -> String {
202
203
  "[Built-in tools — always available, no install needed]\n\
203
- • read_file / list_dir — read any file or directory on the filesystem\n\
204
+ • read_file / list_dir — read any file or directory on the filesystem; read_file supports start_line/end_line\n\
205
+ • find_files / grep_files — search the codebase without shelling out; prefer these for navigation before run_command\n\
204
206
  • write_file / edit_file — create or surgically modify files\n\
205
207
  • run_command — run any shell command: grep, find, git, cargo, make, npm, python3, etc.\n\
206
208
  • fetch_url — HTTP GET (no curl/wget needed)\n\
@@ -820,7 +822,11 @@ mod tests {
820
822
  #[test]
821
823
  fn permission_parsing() {
822
824
  assert!(matches!(permission("auto"), Permission::Auto));
825
+ assert!(matches!(permission("acceptEdits"), Permission::Auto));
826
+ assert!(matches!(permission("bypass-permissions"), Permission::Auto));
823
827
  assert!(matches!(permission("readonly"), Permission::ReadOnly));
828
+ assert!(matches!(permission("read-only"), Permission::ReadOnly));
829
+ assert!(matches!(permission("plan"), Permission::ReadOnly));
824
830
  assert!(matches!(permission("ask"), Permission::Ask));
825
831
  assert!(matches!(permission("anything-else"), Permission::Ask));
826
832
  assert!(matches!(permission(""), Permission::Ask));
@@ -0,0 +1,104 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::time::{SystemTime, UNIX_EPOCH};
4
+
5
+ use serde::{Deserialize, Serialize};
6
+
7
+ #[cfg(not(test))]
8
+ use crate::config;
9
+
10
+ const MAX_SNAPSHOT_BYTES: u64 = 2 * 1024 * 1024;
11
+
12
+ #[derive(Clone, Debug, Serialize, Deserialize)]
13
+ pub struct Checkpoint {
14
+ pub id: String,
15
+ pub cwd: PathBuf,
16
+ pub path: PathBuf,
17
+ pub action: String,
18
+ pub created_ms: u128,
19
+ pub existed: bool,
20
+ pub content: String,
21
+ }
22
+
23
+ #[cfg(not(test))]
24
+ fn dir(_cwd: &Path) -> PathBuf {
25
+ config::home().join("checkpoints")
26
+ }
27
+
28
+ #[cfg(test)]
29
+ fn dir(cwd: &Path) -> PathBuf {
30
+ std::env::temp_dir()
31
+ .join("bwn-checkpoints-test")
32
+ .join(sanitize_id_part(&cwd.to_string_lossy()))
33
+ }
34
+
35
+ fn now_ms() -> u128 {
36
+ SystemTime::now()
37
+ .duration_since(UNIX_EPOCH)
38
+ .map(|d| d.as_millis())
39
+ .unwrap_or(0)
40
+ }
41
+
42
+ fn sanitize_id_part(s: &str) -> String {
43
+ s.chars()
44
+ .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
45
+ .collect()
46
+ }
47
+
48
+ pub fn record(cwd: &Path, path: &Path, action: &str) {
49
+ let existed = path.exists();
50
+ let content = if existed {
51
+ match fs::metadata(path) {
52
+ Ok(m) if m.len() <= MAX_SNAPSHOT_BYTES => fs::read_to_string(path).unwrap_or_default(),
53
+ _ => String::new(),
54
+ }
55
+ } else {
56
+ String::new()
57
+ };
58
+ let created_ms = now_ms();
59
+ let id = format!("{}-{}", created_ms, sanitize_id_part(action));
60
+ let cp = Checkpoint {
61
+ id: id.clone(),
62
+ cwd: cwd.to_path_buf(),
63
+ path: path.to_path_buf(),
64
+ action: action.to_string(),
65
+ created_ms,
66
+ existed,
67
+ content,
68
+ };
69
+ let checkpoint_dir = dir(cwd);
70
+ let _ = fs::create_dir_all(&checkpoint_dir);
71
+ if let Ok(body) = serde_json::to_string_pretty(&cp) {
72
+ let _ = fs::write(checkpoint_dir.join(format!("{id}.json")), body);
73
+ }
74
+ }
75
+
76
+ pub fn list(cwd: &Path) -> Vec<Checkpoint> {
77
+ let Ok(rd) = fs::read_dir(dir(cwd)) else {
78
+ return Vec::new();
79
+ };
80
+ let mut items: Vec<Checkpoint> = rd
81
+ .filter_map(|e| e.ok())
82
+ .filter_map(|e| fs::read_to_string(e.path()).ok())
83
+ .filter_map(|s| serde_json::from_str::<Checkpoint>(&s).ok())
84
+ .filter(|cp| cp.cwd == cwd)
85
+ .collect();
86
+ items.sort_by_key(|cp| std::cmp::Reverse(cp.created_ms));
87
+ items
88
+ }
89
+
90
+ pub fn undo_latest(cwd: &Path) -> Result<Checkpoint, String> {
91
+ let Some(cp) = list(cwd).into_iter().next() else {
92
+ return Err("no checkpoints for this directory".into());
93
+ };
94
+ if cp.existed {
95
+ if let Some(parent) = cp.path.parent() {
96
+ fs::create_dir_all(parent).map_err(|e| format!("cannot create {}: {e}", parent.display()))?;
97
+ }
98
+ fs::write(&cp.path, &cp.content).map_err(|e| format!("cannot restore {}: {e}", cp.path.display()))?;
99
+ } else if cp.path.exists() {
100
+ fs::remove_file(&cp.path).map_err(|e| format!("cannot remove {}: {e}", cp.path.display()))?;
101
+ }
102
+ let _ = fs::remove_file(dir(cwd).join(format!("{}.json", cp.id)));
103
+ Ok(cp)
104
+ }
@@ -260,7 +260,7 @@ pub fn scaffold_home() {
260
260
 
261
261
  // Sub-directories (created silently; errors ignored — missing dirs are
262
262
  // handled gracefully everywhere they are used).
263
- for sub in &["skills", "commands", "hooks/PreToolUse", "hooks/PostToolUse",
263
+ for sub in &["skills", "commands", "checkpoints", "hooks/PreToolUse", "hooks/PostToolUse",
264
264
  "hooks/SessionStart", "hooks/SessionEnd", "hooks/UserPromptSubmit", "hooks/Stop"] {
265
265
  let _ = fs::create_dir_all(h.join(sub));
266
266
  }