@team-agent/installer 0.5.109 → 0.5.111
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +33 -0
- package/README.zh.md +30 -0
- package/crates/team-agent/src/cli/adapters.rs +234 -73
- package/crates/team-agent/src/cli/emit.rs +494 -201
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -161
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +212 -0
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +56 -106
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +22 -13
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/start.rs +5 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +1457 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/provider/argv_route.rs +409 -0
- package/crates/team-agent/src/provider/mod.rs +3 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +32 -0
|
@@ -298,7 +298,8 @@ mod tests {
|
|
|
298
298
|
#[cfg(unix)]
|
|
299
299
|
#[test]
|
|
300
300
|
fn search_survives_unrelated_spaced_model_ids() {
|
|
301
|
-
let catalog =
|
|
301
|
+
let catalog =
|
|
302
|
+
include_str!("../lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt");
|
|
302
303
|
let path = fixture(&format!("printf '%s' '{catalog}'"));
|
|
303
304
|
let result = cmd_models_with(
|
|
304
305
|
&ModelsArgs {
|
|
@@ -370,7 +371,8 @@ mod tests {
|
|
|
370
371
|
&["models", "--provider", "cursor_agent", "--json"],
|
|
371
372
|
);
|
|
372
373
|
let text = public_cli_text(&output);
|
|
373
|
-
let calls =
|
|
374
|
+
let calls =
|
|
375
|
+
std::fs::read_to_string(agent.program.with_extension("count")).unwrap_or_default();
|
|
374
376
|
drop(agent);
|
|
375
377
|
assert!(output.status.success(), "{text}");
|
|
376
378
|
assert_eq!(calls, "1:--list-models\n");
|
|
@@ -405,12 +407,19 @@ mod tests {
|
|
|
405
407
|
let no_match = run_public_models_cli(
|
|
406
408
|
&grok.program,
|
|
407
409
|
grok.response.as_deref(),
|
|
408
|
-
&[
|
|
410
|
+
&[
|
|
411
|
+
"models",
|
|
412
|
+
"--provider",
|
|
413
|
+
"grok",
|
|
414
|
+
"--search",
|
|
415
|
+
"definitely-absent",
|
|
416
|
+
],
|
|
409
417
|
);
|
|
410
418
|
let json_text = public_cli_text(&json);
|
|
411
419
|
let human_text = public_cli_text(&human);
|
|
412
420
|
let no_match_text = public_cli_text(&no_match);
|
|
413
|
-
let calls =
|
|
421
|
+
let calls =
|
|
422
|
+
std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
414
423
|
drop(grok);
|
|
415
424
|
assert!(json.status.success(), "{json_text}");
|
|
416
425
|
assert!(human.status.success(), "{human_text}");
|
|
@@ -422,10 +431,15 @@ mod tests {
|
|
|
422
431
|
assert_eq!(value["auth_basis"], "catalog_visibility");
|
|
423
432
|
let models = value["models"].as_array().unwrap();
|
|
424
433
|
assert_eq!(
|
|
425
|
-
models
|
|
434
|
+
models
|
|
435
|
+
.iter()
|
|
436
|
+
.map(|model| model["model_id"].as_str().unwrap())
|
|
437
|
+
.collect::<Vec<_>>(),
|
|
426
438
|
vec!["grok-4.7", "grok-4.7-build-fast", "grok-4.5", "grok-4.6"]
|
|
427
439
|
);
|
|
428
|
-
assert!(models.iter().all(|model| model["provider"] == "grok"
|
|
440
|
+
assert!(models.iter().all(|model| model["provider"] == "grok"
|
|
441
|
+
&& model["vendor"] == "xai"
|
|
442
|
+
&& model["aliases"] == json!([])));
|
|
429
443
|
assert_eq!(models[0]["default"], false);
|
|
430
444
|
assert_eq!(models[3]["default"], true);
|
|
431
445
|
assert!(human_text.contains("grok-4.7-build-fast"));
|
|
@@ -471,7 +485,8 @@ mod tests {
|
|
|
471
485
|
&["models", "--provider", "grok", "--json"],
|
|
472
486
|
);
|
|
473
487
|
let text = public_cli_text(&output);
|
|
474
|
-
let calls =
|
|
488
|
+
let calls =
|
|
489
|
+
std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
475
490
|
outcomes.push((name, output.status.success(), text, calls));
|
|
476
491
|
}
|
|
477
492
|
for (name, success, text, calls) in outcomes {
|
|
@@ -481,21 +496,32 @@ mod tests {
|
|
|
481
496
|
assert_eq!(value["schema_version"], "models.v1", "{name}");
|
|
482
497
|
assert_eq!(value["ok"], false, "{name}");
|
|
483
498
|
assert_eq!(value["models"], json!([]), "{name}");
|
|
484
|
-
assert!(
|
|
499
|
+
assert!(
|
|
500
|
+
!value["error"]
|
|
501
|
+
.as_str()
|
|
502
|
+
.unwrap()
|
|
503
|
+
.to_ascii_lowercase()
|
|
504
|
+
.contains("unsupported model provider"),
|
|
505
|
+
"{name}: {text}"
|
|
506
|
+
);
|
|
485
507
|
}
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
#[cfg(unix)]
|
|
489
511
|
#[test]
|
|
490
512
|
fn grok_nonzero_native_command_is_not_a_successful_empty_catalog() {
|
|
491
|
-
let grok = CliFixture::new(fixture_program(
|
|
513
|
+
let grok = CliFixture::new(fixture_program(
|
|
514
|
+
"grok",
|
|
515
|
+
"printf 'private diagnostic' >&2\nexit 7",
|
|
516
|
+
));
|
|
492
517
|
let output = run_public_models_cli(
|
|
493
518
|
&grok.program,
|
|
494
519
|
None,
|
|
495
520
|
&["models", "--provider", "grok", "--json"],
|
|
496
521
|
);
|
|
497
522
|
let text = public_cli_text(&output);
|
|
498
|
-
let calls =
|
|
523
|
+
let calls =
|
|
524
|
+
std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
499
525
|
drop(grok);
|
|
500
526
|
assert!(!output.status.success(), "{text}");
|
|
501
527
|
assert_eq!(calls, "1:models\n");
|
|
@@ -503,7 +529,14 @@ mod tests {
|
|
|
503
529
|
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
504
530
|
assert_eq!(value["ok"], false);
|
|
505
531
|
assert_eq!(value["models"], json!([]));
|
|
506
|
-
assert!(
|
|
532
|
+
assert!(
|
|
533
|
+
value["error"]
|
|
534
|
+
.as_str()
|
|
535
|
+
.unwrap()
|
|
536
|
+
.to_ascii_lowercase()
|
|
537
|
+
.contains("grok"),
|
|
538
|
+
"{text}"
|
|
539
|
+
);
|
|
507
540
|
}
|
|
508
541
|
|
|
509
542
|
#[cfg(unix)]
|
|
@@ -518,10 +551,20 @@ mod tests {
|
|
|
518
551
|
let text = public_cli_text(&output);
|
|
519
552
|
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
520
553
|
assert_eq!(value["ok"], false);
|
|
521
|
-
assert!(
|
|
554
|
+
assert!(
|
|
555
|
+
value["error"]
|
|
556
|
+
.as_str()
|
|
557
|
+
.unwrap()
|
|
558
|
+
.to_ascii_lowercase()
|
|
559
|
+
.contains("grok"),
|
|
560
|
+
"{text}"
|
|
561
|
+
);
|
|
522
562
|
assert!(value["action"].as_str().unwrap().contains("grok"), "{text}");
|
|
523
563
|
assert!(crate::cli::emit::default_help().contains("grok"));
|
|
524
|
-
assert!(crate::cli::spec::command_spec("models")
|
|
564
|
+
assert!(crate::cli::spec::command_spec("models")
|
|
565
|
+
.unwrap()
|
|
566
|
+
.usage
|
|
567
|
+
.contains("grok"));
|
|
525
568
|
|
|
526
569
|
let unsupported = run_public_models_cli(
|
|
527
570
|
&missing.program,
|
|
@@ -533,7 +576,13 @@ mod tests {
|
|
|
533
576
|
drop(missing);
|
|
534
577
|
assert!(!output.status.success());
|
|
535
578
|
assert!(!unsupported.status.success());
|
|
536
|
-
assert!(
|
|
579
|
+
assert!(
|
|
580
|
+
unsupported_value["action"]
|
|
581
|
+
.as_str()
|
|
582
|
+
.unwrap()
|
|
583
|
+
.contains("grok"),
|
|
584
|
+
"{unsupported_text}"
|
|
585
|
+
);
|
|
537
586
|
}
|
|
538
587
|
|
|
539
588
|
#[cfg(unix)]
|
|
@@ -579,7 +628,10 @@ mod tests {
|
|
|
579
628
|
#[cfg(unix)]
|
|
580
629
|
impl CliFixture {
|
|
581
630
|
fn new(program: std::path::PathBuf) -> Self {
|
|
582
|
-
Self {
|
|
631
|
+
Self {
|
|
632
|
+
program,
|
|
633
|
+
response: None,
|
|
634
|
+
}
|
|
583
635
|
}
|
|
584
636
|
}
|
|
585
637
|
|
|
@@ -614,7 +666,10 @@ mod tests {
|
|
|
614
666
|
"--nocapture",
|
|
615
667
|
])
|
|
616
668
|
.env("TEAM_AGENT_MODELS_CHILD", "1")
|
|
617
|
-
.env(
|
|
669
|
+
.env(
|
|
670
|
+
"TEAM_AGENT_MODELS_ARGS",
|
|
671
|
+
serde_json::to_string(args).unwrap(),
|
|
672
|
+
)
|
|
618
673
|
.env("PATH", path.parent().unwrap())
|
|
619
674
|
.env_remove("PI_PROVIDER")
|
|
620
675
|
.env_remove("PI_MODEL")
|
|
@@ -719,7 +774,10 @@ mod tests {
|
|
|
719
774
|
fn with_native_mode<T>(
|
|
720
775
|
mode: &str,
|
|
721
776
|
action: impl FnOnce() -> T,
|
|
722
|
-
) -> (
|
|
777
|
+
) -> (
|
|
778
|
+
T,
|
|
779
|
+
crate::lifecycle::launch::pi_mcp::PiCatalogTestObservation,
|
|
780
|
+
) {
|
|
723
781
|
let receipt = native_timeout_receipt();
|
|
724
782
|
assert!(!receipt.exists());
|
|
725
783
|
let result = crate::lifecycle::launch::pi_mcp::with_pi_catalog_test_observation(
|
|
@@ -1122,9 +1180,8 @@ mod tests {
|
|
|
1122
1180
|
fn runner_fails_closed_for_nonzero_oversize_timeout_and_unavailable() {
|
|
1123
1181
|
let fail = native_timeout_fixture();
|
|
1124
1182
|
let started = Instant::now();
|
|
1125
|
-
let (failed_result, fail_observation) =
|
|
1126
|
-
run_catalog(&fail, Duration::from_secs(1), 1024)
|
|
1127
|
-
});
|
|
1183
|
+
let (failed_result, fail_observation) =
|
|
1184
|
+
with_native_mode("exit7", || run_catalog(&fail, Duration::from_secs(1), 1024));
|
|
1128
1185
|
let elapsed = started.elapsed();
|
|
1129
1186
|
assert_eq!(
|
|
1130
1187
|
failed_result.unwrap_err(),
|
|
@@ -1161,9 +1218,8 @@ mod tests {
|
|
|
1161
1218
|
assert_eq!(failed.exit, ExitCode::Error);
|
|
1162
1219
|
assert!(!text.contains("sensitive-token"));
|
|
1163
1220
|
let big = native_timeout_fixture();
|
|
1164
|
-
let (big_result, big_observation) =
|
|
1165
|
-
run_catalog(&big, Duration::from_secs(1), 8)
|
|
1166
|
-
});
|
|
1221
|
+
let (big_result, big_observation) =
|
|
1222
|
+
with_native_mode("oversize", || run_catalog(&big, Duration::from_secs(1), 8));
|
|
1167
1223
|
assert!(big_result.unwrap_err().contains("bounded output"));
|
|
1168
1224
|
assert_eq!(big_observation.spawn_count, 1);
|
|
1169
1225
|
assert_eq!(big_observation.argv, vec!["--list-models"]);
|
|
@@ -1217,7 +1273,7 @@ mod tests {
|
|
|
1217
1273
|
);
|
|
1218
1274
|
assert_eq!(
|
|
1219
1275
|
crate::cli::spec::command_spec("models").unwrap().usage,
|
|
1220
|
-
"
|
|
1276
|
+
"team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]"
|
|
1221
1277
|
);
|
|
1222
1278
|
}
|
|
1223
1279
|
|
|
@@ -71,6 +71,25 @@ pub(crate) struct NamedAddressError {
|
|
|
71
71
|
/// `requested_name`/`suggested_name` without re-parsing. Absent
|
|
72
72
|
/// for exact refusals like empty-workspace.
|
|
73
73
|
pub requested_name: Option<String>,
|
|
74
|
+
// Presentation only: never serialized or used as routing authority.
|
|
75
|
+
human_scope: Option<PathBuf>,
|
|
76
|
+
human_team: Option<String>,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub(crate) fn human_address_reason(reason: &str) -> Option<&'static str> {
|
|
80
|
+
Some(match reason {
|
|
81
|
+
"name_invalid" => "收信队友名不正确;请使用队友列表中的名称。",
|
|
82
|
+
"workspace_not_found" => "所选项目目录不存在;请核对项目路径。",
|
|
83
|
+
"state_not_found" | "workspace_no_state" => {
|
|
84
|
+
"所选项目还没有可用队伍;首次起队用 quick-start,已有队伍恢复用 restart。"
|
|
85
|
+
}
|
|
86
|
+
"team_key_not_found" => "所选项目没有这支队伍;请核对 --workspace 和 --team。",
|
|
87
|
+
"leader_not_attached" => "主控还没有连接到这支队伍;请先体检,按提示恢复连接。",
|
|
88
|
+
"name_not_resolvable" => "当前队伍没有这个队友;请先查看队友列表。",
|
|
89
|
+
"name_not_live" => "该队友当前未运行或连接已失效;请查看状态再恢复。",
|
|
90
|
+
"name_ambiguous" => "有多个同名对象;请明确选择项目和队伍,不猜对象。",
|
|
91
|
+
_ => return None,
|
|
92
|
+
})
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
impl NamedAddressError {
|
|
@@ -86,7 +105,77 @@ impl NamedAddressError {
|
|
|
86
105
|
available_team_keys: Vec::new(),
|
|
87
106
|
suggested_name: None,
|
|
88
107
|
requested_name: None,
|
|
108
|
+
human_scope: None,
|
|
109
|
+
human_team: None,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn with_human_scope(mut self, workspace: Option<&Path>, team: Option<&str>) -> Self {
|
|
114
|
+
self.human_scope = workspace.map(Path::to_path_buf);
|
|
115
|
+
if self.human_team.is_none() {
|
|
116
|
+
self.human_team = team.map(str::to_string);
|
|
89
117
|
}
|
|
118
|
+
self
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub(crate) fn human_guidance(&self, requested: &str) -> (String, String) {
|
|
122
|
+
let workspace = self.human_scope.as_deref();
|
|
123
|
+
let mut message = format!(
|
|
124
|
+
"{}\n你刚才填写的是 {requested:?}。",
|
|
125
|
+
human_address_reason(self.kind.as_str()).unwrap_or("未能找到可用的收信队友。")
|
|
126
|
+
);
|
|
127
|
+
if let Some(workspace) = workspace {
|
|
128
|
+
message.push_str(&format!("\n所选项目:{:?}。", workspace.to_string_lossy()));
|
|
129
|
+
}
|
|
130
|
+
if let Some(team) = self.human_team.as_deref() {
|
|
131
|
+
message.push_str(&format!("\n所选队伍:{team:?}。"));
|
|
132
|
+
}
|
|
133
|
+
let mut action = "没有可靠的拼写建议;请先核对项目和队伍,不猜对象。".to_string();
|
|
134
|
+
for candidate in &self.candidates {
|
|
135
|
+
if let (Some(agent), Some(team)) = (
|
|
136
|
+
candidate.get("agent_id").and_then(Value::as_str),
|
|
137
|
+
candidate.get("team_key").and_then(Value::as_str),
|
|
138
|
+
) {
|
|
139
|
+
message.push_str(&format!(
|
|
140
|
+
"\n候选队伍 {team:?},队友 {agent:?};请确认是否是你要找的对象。"
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if let Some(candidate) = self.suggested_name.as_deref().and_then(|suggested| {
|
|
145
|
+
self.candidates
|
|
146
|
+
.iter()
|
|
147
|
+
.find(|candidate| candidate.get("name").and_then(Value::as_str) == Some(suggested))
|
|
148
|
+
}) {
|
|
149
|
+
if let (Some(workspace), Some(agent), Some(team)) = (
|
|
150
|
+
workspace,
|
|
151
|
+
candidate.get("agent_id").and_then(Value::as_str),
|
|
152
|
+
candidate.get("team_key").and_then(Value::as_str),
|
|
153
|
+
) {
|
|
154
|
+
if !agent.is_empty()
|
|
155
|
+
&& !team.is_empty()
|
|
156
|
+
&& !agent
|
|
157
|
+
.chars()
|
|
158
|
+
.chain(team.chars())
|
|
159
|
+
.chain(workspace.to_string_lossy().chars())
|
|
160
|
+
.any(char::is_control)
|
|
161
|
+
{
|
|
162
|
+
if let Some(command) =
|
|
163
|
+
super::adapters::send_command(agent, workspace, Some(team))
|
|
164
|
+
{
|
|
165
|
+
action = format!("你是否想发给 {agent:?}、队伍 {team:?}?确认后将任务内容替换到这个例子;本次没有发送:\n{command}");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if let Some(workspace) =
|
|
171
|
+
workspace.filter(|path| !path.to_string_lossy().chars().any(char::is_control))
|
|
172
|
+
{
|
|
173
|
+
action.push_str(&format!(
|
|
174
|
+
"\n也可查看可用队友列表:\nteam-agent status --workspace {}",
|
|
175
|
+
super::adapters::shell_quote(&workspace.to_string_lossy())
|
|
176
|
+
));
|
|
177
|
+
}
|
|
178
|
+
(message, action)
|
|
90
179
|
}
|
|
91
180
|
|
|
92
181
|
pub(crate) fn n38_message(&self) -> String {
|
|
@@ -188,9 +277,7 @@ impl NamedAddressError {
|
|
|
188
277
|
})
|
|
189
278
|
})
|
|
190
279
|
.collect();
|
|
191
|
-
let best = ranked
|
|
192
|
-
.first()
|
|
193
|
-
.map(|agent_id| format!("{team}/{agent_id}"));
|
|
280
|
+
let best = ranked.first().map(|agent_id| format!("{team}/{agent_id}"));
|
|
194
281
|
self.with_scoped_suggestions(requested_display, candidate_values, best)
|
|
195
282
|
}
|
|
196
283
|
|
|
@@ -216,9 +303,7 @@ impl NamedAddressError {
|
|
|
216
303
|
// returned candidate (RED-4). Falls back to a status-JSON
|
|
217
304
|
// hint when no candidate cleared the threshold.
|
|
218
305
|
if let Some(best) = best_copyable {
|
|
219
|
-
self.action = format!(
|
|
220
|
-
"Did you mean `{best}`? Rerun with `--to-name {best}`."
|
|
221
|
-
);
|
|
306
|
+
self.action = format!("Did you mean `{best}`? Rerun with `--to-name {best}`.");
|
|
222
307
|
} else {
|
|
223
308
|
self.action = format!(
|
|
224
309
|
"No close matches for `{requested}`. \
|
|
@@ -430,8 +515,14 @@ pub(crate) fn resolve_name_for_cli(
|
|
|
430
515
|
// registry-to-E6 internal helper deliberately pass `None` so
|
|
431
516
|
// caller-supplied `--team` never overrides a full address.
|
|
432
517
|
let parsed = normalize_bare_with_team_scope(parsed, bare_team_scope);
|
|
433
|
-
let
|
|
434
|
-
|
|
518
|
+
let requested_team = match &parsed.target {
|
|
519
|
+
ParsedTarget::TeamEntity { team, .. } => Some(team.as_str()),
|
|
520
|
+
_ => None,
|
|
521
|
+
};
|
|
522
|
+
let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())
|
|
523
|
+
.map_err(|error| error.with_human_scope(None, requested_team))?;
|
|
524
|
+
let state = load_state_checked(&target_workspace)
|
|
525
|
+
.map_err(|error| error.with_human_scope(Some(&target_workspace), requested_team))?;
|
|
435
526
|
let transport = transport_for_cli_target(&target_workspace, &state, &parsed);
|
|
436
527
|
let resolved = resolve_in_workspace(
|
|
437
528
|
sender_workspace,
|
|
@@ -439,7 +530,8 @@ pub(crate) fn resolve_name_for_cli(
|
|
|
439
530
|
&state,
|
|
440
531
|
&parsed,
|
|
441
532
|
transport.as_ref(),
|
|
442
|
-
)
|
|
533
|
+
)
|
|
534
|
+
.map_err(|error| error.with_human_scope(Some(&target_workspace), requested_team))?;
|
|
443
535
|
Ok((resolved, transport))
|
|
444
536
|
}
|
|
445
537
|
|
|
@@ -487,8 +579,8 @@ pub(crate) fn parse_leader_target_workspace_and_team(
|
|
|
487
579
|
match parsed.target {
|
|
488
580
|
ParsedTarget::TeamEntity { team, entity } if entity == "leader" => {
|
|
489
581
|
let state = load_state_checked(&target_workspace)?;
|
|
490
|
-
let canonical =
|
|
491
|
-
.unwrap_or(team);
|
|
582
|
+
let canonical =
|
|
583
|
+
canonical_named_team_key(&target_workspace, &state, &team).unwrap_or(team);
|
|
492
584
|
Ok(Some((target_workspace, canonical)))
|
|
493
585
|
}
|
|
494
586
|
_ => Ok(None),
|
|
@@ -594,7 +686,7 @@ fn resolve_in_workspace(
|
|
|
594
686
|
ParsedTarget::TeamEntity { team, entity } => {
|
|
595
687
|
let canonical = canonical_named_team_key(target_workspace, state, team)
|
|
596
688
|
.unwrap_or_else(|| team.clone());
|
|
597
|
-
if entity == "leader" {
|
|
689
|
+
let result = if entity == "leader" {
|
|
598
690
|
resolve_leader(
|
|
599
691
|
sender_workspace,
|
|
600
692
|
target_workspace,
|
|
@@ -613,7 +705,11 @@ fn resolve_in_workspace(
|
|
|
613
705
|
parsed,
|
|
614
706
|
transport,
|
|
615
707
|
)
|
|
616
|
-
}
|
|
708
|
+
};
|
|
709
|
+
result.map_err(|mut error| {
|
|
710
|
+
error.human_team = Some(canonical);
|
|
711
|
+
error
|
|
712
|
+
})
|
|
617
713
|
}
|
|
618
714
|
ParsedTarget::SessionWindow { session, window } => resolve_session_window(
|
|
619
715
|
sender_workspace,
|
|
@@ -976,11 +1072,7 @@ fn resolve_leader(
|
|
|
976
1072
|
let socket = string_field(receiver, "tmux_socket")
|
|
977
1073
|
.map(str::to_string)
|
|
978
1074
|
.or_else(|| transport.tmux_endpoint());
|
|
979
|
-
match crate::messaging::resolve_live_leader_channel(
|
|
980
|
-
target_workspace,
|
|
981
|
-
receiver,
|
|
982
|
-
transport,
|
|
983
|
-
) {
|
|
1075
|
+
match crate::messaging::resolve_live_leader_channel(target_workspace, receiver, transport) {
|
|
984
1076
|
crate::messaging::LeaderChannelResolution::Live(
|
|
985
1077
|
crate::messaging::LiveLeaderChannel::DirectTmux(channel),
|
|
986
1078
|
) => Ok(ResolvedNamedAddress {
|
|
@@ -1194,8 +1286,8 @@ fn transport_for_cli_target(
|
|
|
1194
1286
|
parsed: &ParsedNamedAddress,
|
|
1195
1287
|
) -> Box<dyn Transport> {
|
|
1196
1288
|
if let ParsedTarget::TeamEntity { team, entity } = &parsed.target {
|
|
1197
|
-
let canonical =
|
|
1198
|
-
.unwrap_or_else(|| team.clone());
|
|
1289
|
+
let canonical =
|
|
1290
|
+
canonical_named_team_key(target_workspace, state, team).unwrap_or_else(|| team.clone());
|
|
1199
1291
|
if entity == "leader" {
|
|
1200
1292
|
let team_scoped_socket = team_entry(state, &canonical)
|
|
1201
1293
|
.and_then(|entry| entry.get("leader_receiver"))
|
|
@@ -1302,11 +1394,8 @@ fn canonical_named_team_key(
|
|
|
1302
1394
|
if team_entry(state, requested).is_some() || is_legacy_single_team_active(state, requested) {
|
|
1303
1395
|
return Some(requested.to_string());
|
|
1304
1396
|
}
|
|
1305
|
-
let selected =
|
|
1306
|
-
target_workspace,
|
|
1307
|
-
Some(requested),
|
|
1308
|
-
)
|
|
1309
|
-
.ok()?;
|
|
1397
|
+
let selected =
|
|
1398
|
+
crate::state::projection::select_runtime_state(target_workspace, Some(requested)).ok()?;
|
|
1310
1399
|
let canonical = selected
|
|
1311
1400
|
.get("active_team_key")
|
|
1312
1401
|
.and_then(Value::as_str)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
//! Host-global route management: raw argv, no workspace selection or error log.
|
|
2
|
+
|
|
3
|
+
use super::{CmdOutput, CmdResult, ExitCode};
|
|
4
|
+
use crate::provider::argv_route::{self, Config, Mutation, Override, RouteError};
|
|
5
|
+
use crate::provider::Provider;
|
|
6
|
+
use serde_json::{json, Value};
|
|
7
|
+
|
|
8
|
+
pub(crate) const HELP: &str = "做什么:\n设置工具启动时额外使用的命令行参数,默认关闭。\n用法:team-agent route [status|enable|disable|show [TOOL]|set TOOL -- ARG...|add TOOL -- ARG...|clear TOOL] [--json]\n\n怎么用:\nstatus 看开关;enable 开启;disable 关闭;show 看全部或指定工具的参数。\nset 替换参数;add 追加参数;clear 清除指定工具的参数;--json 给程序读取。\nTOOL 是已支持的工具名称,如 pi/codex/claude/copilot/grok/cursor_agent。\n第一个 -- 后全是工具的字面参数,包含 --help/--json 也不会被这里解析。\n配置保存在 ~/.team-agent/argv-routing.json;影响全机,不按项目或队伍隔离。\nTEAM_AGENT_CLI_ARGV_ROUTING 环境变量优先于保存的开关。\nExamples(可复制):\nteam-agent route status\nteam-agent route set pi -- --mode text\nteam-agent route enable\n\n下一步(Next Action):\n用 route show pi 核对参数;仅影响下一次启动,不改变正在运行的对话。\n没有开启或没有配置时,工具仍按原参数启动;不了解参数用途时保持关闭。";
|
|
9
|
+
|
|
10
|
+
struct Request {
|
|
11
|
+
operation: String,
|
|
12
|
+
provider: Option<Provider>,
|
|
13
|
+
tokens: Vec<String>,
|
|
14
|
+
json: bool,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn usage() -> RouteError {
|
|
18
|
+
RouteError {
|
|
19
|
+
error: "参数不完整或不正确;set/add 需要工具名、-- 和至少一个工具参数".to_string(),
|
|
20
|
+
reason: "argv_route_usage",
|
|
21
|
+
action: "请按 team-agent route --help 的 Examples 填写;例如 team-agent route set pi -- --mode text".to_string(),
|
|
22
|
+
config_path: None,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn parse(args: &[String], delimiter: usize) -> Result<Request, RouteError> {
|
|
27
|
+
let mut json = false;
|
|
28
|
+
let mut positionals = Vec::new();
|
|
29
|
+
for token in &args[..delimiter] {
|
|
30
|
+
if token == "--json" {
|
|
31
|
+
if json {
|
|
32
|
+
return Err(usage());
|
|
33
|
+
}
|
|
34
|
+
json = true;
|
|
35
|
+
} else if token.starts_with('-') {
|
|
36
|
+
return Err(usage());
|
|
37
|
+
} else {
|
|
38
|
+
positionals.push(token.as_str());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let operation = positionals.first().copied().unwrap_or("status");
|
|
42
|
+
let provider_required = matches!(operation, "set" | "add" | "clear");
|
|
43
|
+
let provider_allowed = provider_required || operation == "show";
|
|
44
|
+
if !matches!(
|
|
45
|
+
operation,
|
|
46
|
+
"status" | "enable" | "disable" | "show" | "set" | "add" | "clear"
|
|
47
|
+
) || positionals.len() > if provider_allowed { 2 } else { 1 }
|
|
48
|
+
|| (provider_required && positionals.len() != 2)
|
|
49
|
+
{
|
|
50
|
+
return Err(usage());
|
|
51
|
+
}
|
|
52
|
+
let provider = positionals
|
|
53
|
+
.get(1)
|
|
54
|
+
.map(|raw| argv_route::parse_route_provider(raw).ok_or_else(usage))
|
|
55
|
+
.transpose()?;
|
|
56
|
+
let tokens = if matches!(operation, "set" | "add") {
|
|
57
|
+
if delimiter == args.len() || delimiter + 1 == args.len() {
|
|
58
|
+
return Err(usage());
|
|
59
|
+
}
|
|
60
|
+
let tokens = args[delimiter + 1..].to_vec();
|
|
61
|
+
if !argv_route::valid_tokens(&tokens) {
|
|
62
|
+
return Err(usage());
|
|
63
|
+
}
|
|
64
|
+
tokens
|
|
65
|
+
} else {
|
|
66
|
+
if delimiter != args.len() {
|
|
67
|
+
return Err(usage());
|
|
68
|
+
}
|
|
69
|
+
Vec::new()
|
|
70
|
+
};
|
|
71
|
+
Ok(Request {
|
|
72
|
+
operation: operation.to_string(),
|
|
73
|
+
provider,
|
|
74
|
+
tokens,
|
|
75
|
+
json,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn switch_fields(
|
|
80
|
+
path: Option<&std::path::Path>,
|
|
81
|
+
persisted: Option<bool>,
|
|
82
|
+
present: bool,
|
|
83
|
+
override_state: Override,
|
|
84
|
+
) -> Value {
|
|
85
|
+
json!({
|
|
86
|
+
"config_path": path,
|
|
87
|
+
"persisted_enabled": persisted,
|
|
88
|
+
"effective_enabled": override_state.effective(persisted.unwrap_or(false)),
|
|
89
|
+
"enabled_source": override_state.source(present),
|
|
90
|
+
"override_status": override_state.status(),
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn execute(request: &Request) -> Result<Value, RouteError> {
|
|
95
|
+
let path = argv_route::config_path()?;
|
|
96
|
+
let (config, present) = match request.operation.as_str() {
|
|
97
|
+
"enable" => argv_route::mutate(&path, Mutation::Enable(true))?,
|
|
98
|
+
"disable" => argv_route::mutate(&path, Mutation::Enable(false))?,
|
|
99
|
+
"set" | "add" | "clear" => {
|
|
100
|
+
let provider = request.provider.ok_or_else(usage)?;
|
|
101
|
+
let mutation = match request.operation.as_str() {
|
|
102
|
+
"set" => Mutation::Set(provider, request.tokens.clone()),
|
|
103
|
+
"add" => Mutation::Add(provider, request.tokens.clone()),
|
|
104
|
+
_ => Mutation::Clear(provider),
|
|
105
|
+
};
|
|
106
|
+
argv_route::mutate(&path, mutation)?
|
|
107
|
+
}
|
|
108
|
+
_ => argv_route::read_config(&path)?,
|
|
109
|
+
};
|
|
110
|
+
let override_state = Override::current();
|
|
111
|
+
let mut value = switch_fields(Some(&path), Some(config.enabled), present, override_state);
|
|
112
|
+
value["ok"] = json!(true);
|
|
113
|
+
value["operation"] = json!(request.operation);
|
|
114
|
+
if let Some(provider) = request.provider {
|
|
115
|
+
add_provider_fields(&mut value, &config, provider);
|
|
116
|
+
} else if request.operation == "show" {
|
|
117
|
+
value["providers"] = json!(config.providers);
|
|
118
|
+
}
|
|
119
|
+
if override_state != Override::Unset {
|
|
120
|
+
value["notice"] = json!(format!(
|
|
121
|
+
"当前开关由环境变量 {} 决定({})",
|
|
122
|
+
argv_route::ENV_NAME,
|
|
123
|
+
override_state.status()
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
Ok(value)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn add_provider_fields(value: &mut Value, config: &Config, provider: Provider) {
|
|
130
|
+
if let Some(key) = argv_route::route_key(provider) {
|
|
131
|
+
value["provider"] = json!(key);
|
|
132
|
+
value["configured"] = json!(config.providers.contains_key(key));
|
|
133
|
+
value["argv"] = json!(config.providers.get(key).cloned().unwrap_or_default());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Route-only error renderer: reuse redaction/formatting, never cwd log policy.
|
|
138
|
+
fn emit_error(error: RouteError, args: &[String], delimiter: usize, as_json: bool) -> ExitCode {
|
|
139
|
+
let is_usage = error.reason == "argv_route_usage";
|
|
140
|
+
let mut value = json!({
|
|
141
|
+
"ok": false, "error": error.error, "reason": error.reason,
|
|
142
|
+
"action": error.action, "config_path": error.config_path,
|
|
143
|
+
});
|
|
144
|
+
if !is_usage {
|
|
145
|
+
// Best-effort switch observation even when strict management read fails.
|
|
146
|
+
// No config content or invalid argv is included in the error channel.
|
|
147
|
+
let path = error.config_path.as_deref();
|
|
148
|
+
let loose = path
|
|
149
|
+
.and_then(|path| std::fs::read(path).ok())
|
|
150
|
+
.and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok());
|
|
151
|
+
let persisted = loose
|
|
152
|
+
.as_ref()
|
|
153
|
+
.and_then(|v| v.get("enabled"))
|
|
154
|
+
.and_then(Value::as_bool);
|
|
155
|
+
let fields = switch_fields(
|
|
156
|
+
path,
|
|
157
|
+
persisted,
|
|
158
|
+
path.is_some_and(std::path::Path::exists),
|
|
159
|
+
Override::current(),
|
|
160
|
+
);
|
|
161
|
+
if let (Some(target), Some(fields)) = (value.as_object_mut(), fields.as_object()) {
|
|
162
|
+
target.extend(fields.clone());
|
|
163
|
+
}
|
|
164
|
+
value["operation"] = json!(args[..delimiter]
|
|
165
|
+
.iter()
|
|
166
|
+
.find(|arg| !arg.starts_with('-'))
|
|
167
|
+
.map(String::as_str)
|
|
168
|
+
.unwrap_or("status"));
|
|
169
|
+
}
|
|
170
|
+
if is_usage {
|
|
171
|
+
value["next_actions"] = json!(["team-agent route --help"]);
|
|
172
|
+
}
|
|
173
|
+
if let Some(text) = super::emit(&CmdOutput::Json(value), as_json) {
|
|
174
|
+
if as_json {
|
|
175
|
+
println!("{text}");
|
|
176
|
+
} else {
|
|
177
|
+
eprintln!("{text}");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if is_usage {
|
|
181
|
+
if !as_json {
|
|
182
|
+
eprintln!("\n{HELP}");
|
|
183
|
+
}
|
|
184
|
+
ExitCode::Usage
|
|
185
|
+
} else {
|
|
186
|
+
ExitCode::Error
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pub(crate) fn run(args: &[String]) -> ExitCode {
|
|
191
|
+
let delimiter = args
|
|
192
|
+
.iter()
|
|
193
|
+
.position(|arg| arg == "--")
|
|
194
|
+
.unwrap_or(args.len());
|
|
195
|
+
// Help and output-format controls stop at the first delimiter. Neither
|
|
196
|
+
// generic help scanning nor the old --no-display compatibility filter runs.
|
|
197
|
+
if args[..delimiter]
|
|
198
|
+
.iter()
|
|
199
|
+
.any(|arg| matches!(arg.as_str(), "--help" | "-h"))
|
|
200
|
+
{
|
|
201
|
+
println!("{HELP}");
|
|
202
|
+
return ExitCode::Ok;
|
|
203
|
+
}
|
|
204
|
+
let as_json = args[..delimiter].iter().any(|arg| arg == "--json");
|
|
205
|
+
match parse(args, delimiter) {
|
|
206
|
+
Ok(request) => match execute(&request) {
|
|
207
|
+
Ok(value) => super::emit::emit_result(CmdResult::from_json(value, request.json)),
|
|
208
|
+
Err(error) => emit_error(error, args, delimiter, as_json),
|
|
209
|
+
},
|
|
210
|
+
Err(error) => emit_error(error, args, delimiter, as_json),
|
|
211
|
+
}
|
|
212
|
+
}
|