Skip to content

Commit 7388e4d

Browse files
authored
feat: dir ignore support in ovcli.conf (#1393)
1 parent e3ac0ba commit 7388e4d

9 files changed

Lines changed: 267 additions & 24 deletions

File tree

crates/ov_cli/src/client.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,11 @@ impl HttpClient {
787787
// Determine target path
788788
let to_path = Path::new(to);
789789
let final_path = if to_path.is_dir() {
790-
let base_name = uri.trim_end_matches('/').split('/').last().unwrap_or("export");
790+
let base_name = uri
791+
.trim_end_matches('/')
792+
.split('/')
793+
.last()
794+
.unwrap_or("export");
791795
to_path.join(format!("{}.ovpack", base_name))
792796
} else if !to.ends_with(".ovpack") {
793797
Path::new(&format!("{}.ovpack", to)).to_path_buf()
@@ -822,10 +826,7 @@ impl HttpClient {
822826
)));
823827
}
824828
if !file_path_obj.is_file() {
825-
return Err(Error::Client(format!(
826-
"Path is not a file: {}",
827-
file_path
828-
)));
829+
return Err(Error::Client(format!("Path is not a file: {}", file_path)));
829830
}
830831

831832
let temp_file_id = self.upload_temp_file(file_path_obj).await?;

crates/ov_cli/src/commands/search.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ pub async fn grep(
5353
compact: bool,
5454
) -> Result<()> {
5555
let result = client
56-
.grep(uri, exclude_uri, pattern, ignore_case, node_limit, level_limit)
56+
.grep(
57+
uri,
58+
exclude_uri,
59+
pattern,
60+
ignore_case,
61+
node_limit,
62+
level_limit,
63+
)
5764
.await?;
5865
output_success(&result, output_format, compact);
5966
Ok(())

crates/ov_cli/src/config.rs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ use crate::error::{Error, Result};
55

66
const OPENVIKING_CLI_CONFIG_ENV: &str = "OPENVIKING_CLI_CONFIG_FILE";
77

8+
#[derive(Debug, Clone, Serialize, Deserialize)]
9+
pub struct UploadConfig {
10+
pub ignore_dirs: Option<String>,
11+
pub include: Option<String>,
12+
pub exclude: Option<String>,
13+
}
14+
15+
impl Default for UploadConfig {
16+
fn default() -> Self {
17+
Self {
18+
ignore_dirs: None,
19+
include: None,
20+
exclude: None,
21+
}
22+
}
23+
}
24+
825
#[derive(Debug, Clone, Serialize, Deserialize)]
926
pub struct Config {
1027
#[serde(default = "default_url")]
@@ -19,6 +36,8 @@ pub struct Config {
1936
pub output: String,
2037
#[serde(default = "default_echo_command")]
2138
pub echo_command: bool,
39+
#[serde(default)]
40+
pub upload: UploadConfig,
2241
}
2342

2443
fn default_url() -> String {
@@ -48,10 +67,35 @@ impl Default for Config {
4867
timeout: 60.0,
4968
output: "table".to_string(),
5069
echo_command: true,
70+
upload: UploadConfig::default(),
5171
}
5272
}
5373
}
5474

75+
fn normalize_csv_option(value: Option<String>) -> Vec<String> {
76+
value
77+
.unwrap_or_default()
78+
.split(',')
79+
.map(str::trim)
80+
.filter(|token| !token.is_empty())
81+
.map(ToString::to_string)
82+
.collect()
83+
}
84+
85+
pub fn merge_csv_options(
86+
config_value: Option<String>,
87+
cli_value: Option<String>,
88+
) -> Option<String> {
89+
let mut merged = normalize_csv_option(config_value);
90+
merged.extend(normalize_csv_option(cli_value));
91+
92+
if merged.is_empty() {
93+
None
94+
} else {
95+
Some(merged.join(","))
96+
}
97+
}
98+
5599
impl Config {
56100
/// Load config from default location or create default
57101
pub fn load() -> Result<Self> {
@@ -115,7 +159,7 @@ pub fn get_or_create_machine_id() -> Result<String> {
115159

116160
#[cfg(test)]
117161
mod tests {
118-
use super::Config;
162+
use super::{Config, merge_csv_options};
119163

120164
#[test]
121165
fn config_deserializes_account_and_user_fields() {
@@ -133,5 +177,77 @@ mod tests {
133177
assert_eq!(config.account.as_deref(), Some("acme"));
134178
assert_eq!(config.user.as_deref(), Some("alice"));
135179
assert_eq!(config.agent_id.as_deref(), Some("assistant-1"));
180+
assert!(config.upload.ignore_dirs.is_none());
181+
assert!(config.upload.include.is_none());
182+
assert!(config.upload.exclude.is_none());
183+
}
184+
185+
#[test]
186+
fn config_deserializes_upload_fields() {
187+
let config: Config = serde_json::from_str(
188+
r#"{
189+
"url": "http://localhost:1933",
190+
"upload": {
191+
"ignore_dirs": "node_modules,dist",
192+
"include": "*.md,*.pdf",
193+
"exclude": "*.tmp,*.log"
194+
}
195+
}"#,
196+
)
197+
.expect("config should deserialize");
198+
199+
assert_eq!(
200+
config.upload.ignore_dirs.as_deref(),
201+
Some("node_modules,dist")
202+
);
203+
assert_eq!(config.upload.include.as_deref(), Some("*.md,*.pdf"));
204+
assert_eq!(config.upload.exclude.as_deref(), Some("*.tmp,*.log"));
205+
}
206+
207+
#[test]
208+
fn merge_csv_options_config_only() {
209+
assert_eq!(
210+
merge_csv_options(Some("node_modules,dist".to_string()), None),
211+
Some("node_modules,dist".to_string())
212+
);
213+
}
214+
215+
#[test]
216+
fn merge_csv_options_cli_only() {
217+
assert_eq!(
218+
merge_csv_options(None, Some("*.md,*.pdf".to_string())),
219+
Some("*.md,*.pdf".to_string())
220+
);
221+
}
222+
223+
#[test]
224+
fn merge_csv_options_additive_merge() {
225+
assert_eq!(
226+
merge_csv_options(
227+
Some("node_modules,dist".to_string()),
228+
Some("build,out".to_string())
229+
),
230+
Some("node_modules,dist,build,out".to_string())
231+
);
232+
}
233+
234+
#[test]
235+
fn merge_csv_options_trims_and_drops_empty_tokens() {
236+
assert_eq!(
237+
merge_csv_options(
238+
Some(" node_modules , , dist ,".to_string()),
239+
Some(" ,*.tmp, *.log ,".to_string())
240+
),
241+
Some("node_modules,dist,*.tmp,*.log".to_string())
242+
);
243+
}
244+
245+
#[test]
246+
fn merge_csv_options_returns_none_when_empty() {
247+
assert_eq!(
248+
merge_csv_options(Some(" , , ".to_string()), Some("".to_string())),
249+
None
250+
);
251+
assert_eq!(merge_csv_options(None, None), None);
136252
}
137253
}

crates/ov_cli/src/main.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ mod tui;
77
mod utils;
88

99
use clap::{ArgAction, Parser, Subcommand};
10-
use config::Config;
10+
use config::{Config, merge_csv_options};
1111
use error::{Error, Result};
1212
use output::OutputFormat;
1313

@@ -784,8 +784,7 @@ async fn main() {
784784
append,
785785
wait,
786786
timeout,
787-
} => handle_write(uri, content, from_file, append, wait, timeout, ctx)
788-
.await,
787+
} => handle_write(uri, content, from_file, append, wait, timeout, ctx).await,
789788
Commands::Reindex {
790789
uri,
791790
regenerate,
@@ -812,7 +811,18 @@ async fn main() {
812811
ignore_case,
813812
node_limit,
814813
level_limit,
815-
} => handle_grep(uri, exclude_uri, pattern, ignore_case, node_limit, level_limit, ctx).await,
814+
} => {
815+
handle_grep(
816+
uri,
817+
exclude_uri,
818+
pattern,
819+
ignore_case,
820+
node_limit,
821+
level_limit,
822+
ctx,
823+
)
824+
.await
825+
}
816826

817827
Commands::Glob {
818828
pattern,
@@ -886,6 +896,11 @@ async fn handle_add_resource(
886896
let strict = strict_mode;
887897
let directly_upload_media = !no_directly_upload_media;
888898

899+
let effective_ignore_dirs =
900+
merge_csv_options(ctx.config.upload.ignore_dirs.clone(), ignore_dirs);
901+
let effective_include = merge_csv_options(ctx.config.upload.include.clone(), include);
902+
let effective_exclude = merge_csv_options(ctx.config.upload.exclude.clone(), exclude);
903+
889904
let effective_timeout = if wait {
890905
timeout.unwrap_or(60.0).max(ctx.config.timeout)
891906
} else {
@@ -909,9 +924,9 @@ async fn handle_add_resource(
909924
wait,
910925
timeout,
911926
strict,
912-
ignore_dirs,
913-
include,
914-
exclude,
927+
effective_ignore_dirs,
928+
effective_include,
929+
effective_exclude,
915930
directly_upload_media,
916931
watch_interval,
917932
ctx.output_format,
@@ -1237,7 +1252,11 @@ async fn handle_write(
12371252
(Some(value), None) => value,
12381253
(None, Some(path)) => std::fs::read_to_string(path)
12391254
.map_err(|e| Error::Client(format!("Failed to read --from-file: {}", e)))?,
1240-
_ => return Err(Error::Client("Specify exactly one of --content or --from-file".into())),
1255+
_ => {
1256+
return Err(Error::Client(
1257+
"Specify exactly one of --content or --from-file".into(),
1258+
));
1259+
}
12411260
};
12421261
commands::content::write(
12431262
&client,
@@ -1454,7 +1473,11 @@ async fn handle_grep(
14541473
std::process::exit(1);
14551474
}
14561475

1457-
let mut params = vec![format!("--uri={}", uri), format!("-n {}", node_limit), format!("-L {}", level_limit)];
1476+
let mut params = vec![
1477+
format!("--uri={}", uri),
1478+
format!("-n {}", node_limit),
1479+
format!("-L {}", level_limit),
1480+
];
14581481
if let Some(excluded) = &exclude_uri {
14591482
params.push(format!("-x {}", excluded));
14601483
}
@@ -1548,6 +1571,7 @@ mod tests {
15481571
timeout: 60.0,
15491572
output: "table".to_string(),
15501573
echo_command: true,
1574+
upload: Default::default(),
15511575
};
15521576

15531577
let ctx = CliContext::from_config(

crates/ov_cli/src/output.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -392,19 +392,22 @@ fn render_session_context(
392392
));
393393
lines.push(format!(
394394
"total_archives {}",
395-
stats.get("totalArchives")
395+
stats
396+
.get("totalArchives")
396397
.map(format_value)
397398
.unwrap_or_else(|| "0".to_string())
398399
));
399400
lines.push(format!(
400401
"included_archives {}",
401-
stats.get("includedArchives")
402+
stats
403+
.get("includedArchives")
402404
.map(format_value)
403405
.unwrap_or_else(|| "0".to_string())
404406
));
405407
lines.push(format!(
406408
"dropped_archives {}",
407-
stats.get("droppedArchives")
409+
stats
410+
.get("droppedArchives")
408411
.map(format_value)
409412
.unwrap_or_else(|| "0".to_string())
410413
));
@@ -457,7 +460,9 @@ fn render_session_archive(
457460
obj: &serde_json::Map<String, serde_json::Value>,
458461
compact: bool,
459462
) -> Option<String> {
460-
if !(obj.contains_key("archive_id") && obj.contains_key("overview") && obj.contains_key("messages"))
463+
if !(obj.contains_key("archive_id")
464+
&& obj.contains_key("overview")
465+
&& obj.contains_key("messages"))
461466
{
462467
return None;
463468
}
@@ -571,8 +576,14 @@ fn summarize_message_content(parts: Option<&Vec<serde_json::Value>>) -> String {
571576
});
572577
}
573578
"tool" => {
574-
let name = obj.get("tool_name").and_then(|v| v.as_str()).unwrap_or("tool");
575-
let status = obj.get("tool_status").and_then(|v| v.as_str()).unwrap_or("");
579+
let name = obj
580+
.get("tool_name")
581+
.and_then(|v| v.as_str())
582+
.unwrap_or("tool");
583+
let status = obj
584+
.get("tool_status")
585+
.and_then(|v| v.as_str())
586+
.unwrap_or("");
576587
chunks.push(if status.is_empty() {
577588
format!("[tool:{}]", name)
578589
} else {

docs/en/guides/01-configuration.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,12 @@ Config file for the HTTP client (`SyncHTTPClient` / `AsyncHTTPClient`) and CLI t
794794
"account": "acme",
795795
"user": "alice",
796796
"agent_id": "my-agent",
797-
"output": "table"
797+
"output": "table",
798+
"upload": {
799+
"ignore_dirs": "node_modules,.cache,.nx",
800+
"include": "*.md,*.pdf",
801+
"exclude": "*.tmp,*.log"
802+
}
798803
}
799804
```
800805

@@ -806,13 +811,24 @@ Config file for the HTTP client (`SyncHTTPClient` / `AsyncHTTPClient`) and CLI t
806811
| `user` | Default user sent as `X-OpenViking-User` | `null` |
807812
| `agent_id` | Agent identifier for agent space isolation | `null` |
808813
| `output` | Default output format: `"table"` or `"json"` | `"table"` |
814+
| `upload.ignore_dirs` | Default directory ignore list for `add-resource` (CSV) | `null` |
815+
| `upload.include` | Default include patterns for `add-resource` (CSV) | `null` |
816+
| `upload.exclude` | Default exclude patterns for `add-resource` (CSV) | `null` |
809817

810818
CLI flags can override these identity fields per command:
811819

812820
```bash
813821
openviking --account acme --user alice --agent-id assistant-2 ls viking://
814822
```
815823

824+
For `add-resource`, upload filter flags are merged additively with `ovcli.conf` defaults:
825+
826+
```bash
827+
# ovcli.conf: upload.exclude="*.log"
828+
openviking add-resource ./docs --exclude "*.tmp"
829+
# effective exclude sent to server: "*.log,*.tmp"
830+
```
831+
816832
See [Deployment](./03-deployment.md) for details.
817833

818834
## server Section

0 commit comments

Comments
 (0)