Skip to content

Commit 4af5963

Browse files
zhoujh01TRAE CLITRAE CLI
committed
feat(tags): support tagged write and filesystem filtering
Add first-upsert tags for content writes, AND filtering and projection for ls/tree/grep, and unified --tags CLI plus Python, Go, and TypeScript SDK support. Preserve URI-scoped ingestion options across semantic and memory paths, including replace, append, and explicit empty-tag handling. Co-authored-by: TRAE CLI <noreply@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
1 parent 66dc4c6 commit 4af5963

36 files changed

Lines changed: 902 additions & 72 deletions

crates/ov_cli/src/client.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,19 @@ impl HttpClient {
412412
wait: bool,
413413
timeout: Option<f64>,
414414
processing_mode: &str,
415+
tags: Vec<String>,
416+
tag_mode: &str,
415417
) -> Result<serde_json::Value> {
416-
let body = Self::build_write_body(uri, content, mode, wait, timeout, processing_mode);
418+
let body = Self::build_write_body(
419+
uri,
420+
content,
421+
mode,
422+
wait,
423+
timeout,
424+
processing_mode,
425+
tags,
426+
tag_mode,
427+
);
417428
self.post("/api/v1/content/write", &body).await
418429
}
419430

@@ -474,6 +485,8 @@ impl HttpClient {
474485
wait: bool,
475486
timeout: Option<f64>,
476487
processing_mode: &str,
488+
tags: Vec<String>,
489+
tag_mode: &str,
477490
) -> Value {
478491
let mut body = serde_json::json!({
479492
"uri": uri,
@@ -483,6 +496,13 @@ impl HttpClient {
483496
"timeout": timeout,
484497
"processing_mode": processing_mode,
485498
});
499+
if !tags.is_empty() {
500+
let obj = body
501+
.as_object_mut()
502+
.expect("write request body must be an object");
503+
obj.insert("tags".to_string(), serde_json::json!(tags));
504+
obj.insert("tag_mode".to_string(), serde_json::json!(tag_mode));
505+
}
486506
compact_request_body(&mut body);
487507
body
488508
}
@@ -584,8 +604,9 @@ impl HttpClient {
584604
abs_limit: i32,
585605
show_all_hidden: bool,
586606
node_limit: i32,
607+
tags: &[String],
587608
) -> Result<serde_json::Value> {
588-
let params = vec![
609+
let mut params = vec![
589610
("uri".to_string(), uri.to_string()),
590611
("simple".to_string(), simple.to_string()),
591612
("recursive".to_string(), recursive.to_string()),
@@ -594,6 +615,9 @@ impl HttpClient {
594615
("show_all_hidden".to_string(), show_all_hidden.to_string()),
595616
("node_limit".to_string(), node_limit.to_string()),
596617
];
618+
for tag in tags {
619+
params.push(("tags".to_string(), tag.clone()));
620+
}
597621
self.get("/api/v1/fs/ls", &params).await
598622
}
599623

@@ -605,15 +629,19 @@ impl HttpClient {
605629
show_all_hidden: bool,
606630
node_limit: i32,
607631
level_limit: i32,
632+
tags: &[String],
608633
) -> Result<serde_json::Value> {
609-
let params = vec![
634+
let mut params = vec![
610635
("uri".to_string(), uri.to_string()),
611636
("output".to_string(), output.to_string()),
612637
("abs_limit".to_string(), abs_limit.to_string()),
613638
("show_all_hidden".to_string(), show_all_hidden.to_string()),
614639
("node_limit".to_string(), node_limit.to_string()),
615640
("level_limit".to_string(), level_limit.to_string()),
616641
];
642+
for tag in tags {
643+
params.push(("tags".to_string(), tag.clone()));
644+
}
617645
self.get("/api/v1/fs/tree", &params).await
618646
}
619647

@@ -741,6 +769,7 @@ impl HttpClient {
741769
ignore_case: bool,
742770
node_limit: i32,
743771
level_limit: i32,
772+
tags: &[String],
744773
) -> Result<serde_json::Value> {
745774
let body = serde_json::json!({
746775
"uri": uri,
@@ -749,6 +778,7 @@ impl HttpClient {
749778
"case_insensitive": ignore_case,
750779
"node_limit": node_limit,
751780
"level_limit": level_limit,
781+
"tags": (!tags.is_empty()).then(|| tags),
752782
});
753783
self.post("/api/v1/search/grep", &body).await
754784
}
@@ -2196,6 +2226,8 @@ mod tests {
21962226
true,
21972227
Some(3.0),
21982228
"semantic_and_vectors",
2229+
vec![],
2230+
"replace",
21992231
);
22002232

22012233
assert_eq!(
@@ -2221,6 +2253,8 @@ mod tests {
22212253
true,
22222254
None,
22232255
"semantic_and_vectors",
2256+
vec![],
2257+
"replace",
22242258
);
22252259

22262260
assert!(body.get("processing_mode").is_none());
@@ -2235,6 +2269,8 @@ mod tests {
22352269
true,
22362270
None,
22372271
"vectors_only",
2272+
vec![],
2273+
"replace",
22382274
);
22392275

22402276
assert_eq!(body["processing_mode"], "vectors_only");
@@ -2246,7 +2282,16 @@ mod tests {
22462282
let client = HttpClient::new(base_url, None, None, None, None, 5.0, false, None);
22472283

22482284
client
2249-
.ls("viking://resources", false, false, "agent", 256, false, 1)
2285+
.ls(
2286+
"viking://resources",
2287+
false,
2288+
false,
2289+
"agent",
2290+
256,
2291+
false,
2292+
1,
2293+
&[],
2294+
)
22502295
.await
22512296
.expect("ls request should succeed");
22522297

@@ -2372,7 +2417,7 @@ mod tests {
23722417
let client = HttpClient::new(base_url, None, None, None, None, 5.0, false, None);
23732418

23742419
client
2375-
.tree("viking://resources", "agent", 256, false, 1, 3)
2420+
.tree("viking://resources", "agent", 256, false, 1, 3, &[])
23762421
.await
23772422
.expect("tree request should succeed");
23782423

crates/ov_cli/src/commands/content.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,22 @@ pub async fn write(
4444
wait: bool,
4545
timeout: Option<f64>,
4646
processing_mode: &str,
47+
tags: Vec<String>,
48+
tag_mode: &str,
4749
output_format: OutputFormat,
4850
compact: bool,
4951
) -> Result<()> {
5052
let result = client
51-
.write(uri, content, mode, wait, timeout, processing_mode)
53+
.write(
54+
uri,
55+
content,
56+
mode,
57+
wait,
58+
timeout,
59+
processing_mode,
60+
tags,
61+
tag_mode,
62+
)
5263
.await?;
5364
crate::output::output_success(result, output_format, compact);
5465
Ok(())

crates/ov_cli/src/commands/filesystem.rs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub async fn ls(
2626
abs_limit: i32,
2727
show_all_hidden: bool,
2828
node_limit: i32,
29+
tags: &[String],
30+
show_tags: bool,
2931
output_format: OutputFormat,
3032
compact: bool,
3133
) -> Result<()> {
@@ -38,9 +40,10 @@ pub async fn ls(
3840
abs_limit,
3941
show_all_hidden,
4042
node_limit,
43+
tags,
4144
)
4245
.await?;
43-
output_filesystem_entries(&result, output_format, compact, false);
46+
output_filesystem_entries(&result, output_format, compact, false, show_tags);
4447
Ok(())
4548
}
4649

@@ -52,6 +55,7 @@ pub async fn tree(
5255
show_all_hidden: bool,
5356
node_limit: i32,
5457
level_limit: i32,
58+
tags: &[String],
5559
output_format: OutputFormat,
5660
compact: bool,
5761
) -> Result<()> {
@@ -63,9 +67,10 @@ pub async fn tree(
6367
show_all_hidden,
6468
node_limit,
6569
level_limit,
70+
tags,
6671
)
6772
.await?;
68-
output_filesystem_entries(&result, output_format, compact, true);
73+
output_filesystem_entries(&result, output_format, compact, true, false);
6974
Ok(())
7075
}
7176

@@ -74,8 +79,11 @@ fn output_filesystem_entries(
7479
output_format: OutputFormat,
7580
compact: bool,
7681
is_tree: bool,
82+
show_tags: bool,
7783
) {
78-
if let Some(rendered) = render_filesystem_entries_for_table(result, output_format, is_tree) {
84+
if let Some(rendered) =
85+
render_filesystem_entries_for_table(result, output_format, is_tree, show_tags)
86+
{
7987
println!("{rendered}");
8088
} else {
8189
output_success(result, output_format, compact);
@@ -86,18 +94,24 @@ fn render_filesystem_entries_for_table(
8694
value: &Value,
8795
output_format: OutputFormat,
8896
is_tree: bool,
97+
show_tags: bool,
8998
) -> Option<String> {
9099
if matches!(output_format, OutputFormat::Json) {
91100
return None;
92101
}
93102
if is_tree {
94103
render_tree_entries_for_table(value)
95104
} else {
96-
render_ls_entries_for_table(value)
105+
render_ls_entries_for_table_with_tags(value, show_tags)
97106
}
98107
}
99108

109+
#[cfg(test)]
100110
fn render_ls_entries_for_table(value: &Value) -> Option<String> {
111+
render_ls_entries_for_table_with_tags(value, false)
112+
}
113+
114+
fn render_ls_entries_for_table_with_tags(value: &Value, show_tags: bool) -> Option<String> {
101115
let (entries, profile) = filesystem_entries(value)?;
102116
let mut lines = Vec::new();
103117
let text_width = entry_text_width();
@@ -112,7 +126,7 @@ fn render_ls_entries_for_table(value: &Value) -> Option<String> {
112126
if index > 0 {
113127
lines.push(String::new());
114128
}
115-
render_ls_entry(index + 1, entry, text_width, &mut lines);
129+
render_ls_entry(index + 1, entry, text_width, show_tags, &mut lines);
116130
}
117131

118132
append_profile_lines(profile, &mut lines);
@@ -155,7 +169,13 @@ fn filesystem_entries(value: &Value) -> Option<(Vec<&Value>, Option<&Value>)> {
155169
Some((entries.iter().collect(), profile))
156170
}
157171

158-
fn render_ls_entry(rank: usize, entry: &Value, text_width: usize, lines: &mut Vec<String>) {
172+
fn render_ls_entry(
173+
rank: usize,
174+
entry: &Value,
175+
text_width: usize,
176+
show_tags: bool,
177+
lines: &mut Vec<String>,
178+
) {
159179
let object = entry.as_object();
160180
let metadata = entry_metadata(object);
161181
lines.push(format!(
@@ -171,6 +191,24 @@ fn render_ls_entry(rank: usize, entry: &Value, text_width: usize, lines: &mut Ve
171191
}
172192

173193
append_entry_abstract(object, ENTRY_INDENT, text_width, lines);
194+
if show_tags {
195+
let tags = object
196+
.and_then(|object| object.get("tags"))
197+
.and_then(Value::as_array)
198+
.map(|items| {
199+
items
200+
.iter()
201+
.filter_map(Value::as_str)
202+
.collect::<Vec<_>>()
203+
.join(", ")
204+
})
205+
.filter(|tags| !tags.is_empty())
206+
.unwrap_or_else(|| "-".to_string());
207+
lines.push(format!(
208+
"{ENTRY_INDENT}{}",
209+
theme::muted(format!("tags: {tags}"))
210+
));
211+
}
174212
}
175213

176214
fn render_tree_entry(rank: usize, entry: &Value, text_width: usize, lines: &mut Vec<String>) {
@@ -497,7 +535,7 @@ fn output_message_result(
497535
mod tests {
498536
use super::{
499537
render_filesystem_entries_for_table, render_ls_entries_for_table,
500-
render_tree_entries_for_table,
538+
render_ls_entries_for_table_with_tags, render_tree_entries_for_table,
501539
};
502540
use crate::output::render_profiled_scalar_result;
503541
use serde_json::json;
@@ -681,11 +719,30 @@ mod tests {
681719
]);
682720

683721
assert!(
684-
render_filesystem_entries_for_table(&result, crate::output::OutputFormat::Json, false)
685-
.is_none()
722+
render_filesystem_entries_for_table(
723+
&result,
724+
crate::output::OutputFormat::Json,
725+
false,
726+
false,
727+
)
728+
.is_none()
686729
);
687730
}
688731

732+
#[test]
733+
fn ls_table_output_shows_requested_tags() {
734+
let result = json!([
735+
{"uri": "viking://resources/a.md", "isDir": false, "tags": ["env=prod", "team=search"]},
736+
{"uri": "viking://resources/b.md", "isDir": false, "tags": []}
737+
]);
738+
739+
let rendered =
740+
strip_ansi(&render_ls_entries_for_table_with_tags(&result, true).expect("ls"));
741+
742+
assert!(rendered.contains("tags: env=prod, team=search"));
743+
assert!(rendered.contains("tags: -"));
744+
}
745+
689746
fn strip_ansi(input: &str) -> String {
690747
let mut output = String::with_capacity(input.len());
691748
let mut chars = input.chars().peekable();

crates/ov_cli/src/commands/search.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ pub async fn grep(
637637
ignore_case: bool,
638638
node_limit: i32,
639639
level_limit: i32,
640+
tags: &[String],
640641
output_format: OutputFormat,
641642
compact: bool,
642643
) -> Result<()> {
@@ -648,6 +649,7 @@ pub async fn grep(
648649
ignore_case,
649650
node_limit,
650651
level_limit,
652+
tags,
651653
)
652654
.await?;
653655
output_grep_results(&result, output_format, compact);
@@ -833,7 +835,10 @@ mod tests {
833835

834836
assert!(rendered.contains("Deployment summary."));
835837
for line in ["# Deploy", "Step one.", "Step two.", "Step three."] {
836-
assert!(rendered.contains(line), "missing inlined content line: {line}");
838+
assert!(
839+
rendered.contains(line),
840+
"missing inlined content line: {line}"
841+
);
837842
}
838843
}
839844

0 commit comments

Comments
 (0)