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.
- package/harness/Cargo.lock +1 -1
- package/harness/Cargo.toml +1 -1
- package/harness/src/agent.rs +10 -4
- package/harness/src/checkpoint.rs +104 -0
- package/harness/src/config.rs +1 -1
- package/harness/src/lib.rs +296 -39
- package/harness/src/onboarding.rs +50 -8
- package/harness/src/report.rs +2 -2
- package/harness/src/tools.rs +218 -3
- package/harness/src/tui.rs +184 -56
- package/package.json +3 -3
- package/scripts/postinstall.js +6 -1
package/harness/Cargo.lock
CHANGED
package/harness/Cargo.toml
CHANGED
package/harness/src/agent.rs
CHANGED
|
@@ -134,9 +134,10 @@ pub enum Permission {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
pub fn permission(s: &str) -> Permission {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"
|
|
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
|
+
}
|
package/harness/src/config.rs
CHANGED
|
@@ -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
|
}
|