@team-agent/installer 0.5.83 → 0.5.84
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/crates/team-agent/src/cli/emit.rs +38 -9
- package/crates/team-agent/src/cli/models.rs +231 -24
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/types.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +16 -18
- package/crates/team-agent/src/mcp_server/tests/tools.rs +31 -20
- package/crates/team-agent/src/mcp_server/tests/wire.rs +30 -0
- package/crates/team-agent/src/mcp_server/tools.rs +113 -11
- package/crates/team-agent/src/mcp_server/wire.rs +20 -1
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -418,7 +418,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
418
418
|
.to_string(),
|
|
419
419
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
420
420
|
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影。缺少可靠定位或 nodeprobe 证据时显示 unknown;tmux_command 可复制到对应目标。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
|
|
421
|
-
Some("models") => "usage: team-agent models --provider pi [
|
|
421
|
+
Some("models") => "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, and display name. Cursor uses the local `agent --list-models` catalog.".to_string(),
|
|
422
422
|
Some("leaders") => "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]\n\nLists LIVE leaders by default. Use --all to include retained STALE entries, --stale to inspect only STALE entries, QUERY or --search TEXT to match workspace/team/name fields, and --prune to remove only entries proven terminal by canonical state. --dry-run is valid only with --prune.".to_string(),
|
|
423
423
|
Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
|
|
424
424
|
Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
|
|
@@ -1007,6 +1007,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
1007
1007
|
other if other.starts_with("--provider=") => {
|
|
1008
1008
|
parsed.provider = Some(other.trim_start_matches("--provider=").to_string());
|
|
1009
1009
|
}
|
|
1010
|
+
other if other.starts_with("--search=") => {
|
|
1011
|
+
parsed.search = Some(other.trim_start_matches("--search=").to_string());
|
|
1012
|
+
}
|
|
1010
1013
|
other if other.starts_with("--socket=") => {
|
|
1011
1014
|
parsed.socket = Some(other.trim_start_matches("--socket=").to_string());
|
|
1012
1015
|
}
|
|
@@ -1106,17 +1109,19 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
1106
1109
|
|
|
1107
1110
|
fn models_args(args: &[String]) -> Result<ModelsArgs, CliError> {
|
|
1108
1111
|
let parsed = parse_args(args);
|
|
1109
|
-
let provider = parsed
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
"models supports only --provider pi, got {provider:?}"
|
|
1115
|
-
)));
|
|
1112
|
+
let provider = parsed.provider.unwrap_or_else(|| "pi".to_string());
|
|
1113
|
+
if parsed.search.is_some() && !parsed.positionals.is_empty() {
|
|
1114
|
+
return Err(CliError::Usage(
|
|
1115
|
+
"models accepts either QUERY or --search TEXT, not both".to_string(),
|
|
1116
|
+
));
|
|
1116
1117
|
}
|
|
1118
|
+
let search = parsed
|
|
1119
|
+
.search
|
|
1120
|
+
.or_else(|| (!parsed.positionals.is_empty()).then(|| parsed.positionals.join(" ")))
|
|
1121
|
+
.filter(|query| !query.trim().is_empty());
|
|
1117
1122
|
Ok(ModelsArgs {
|
|
1118
1123
|
provider,
|
|
1119
|
-
search
|
|
1124
|
+
search,
|
|
1120
1125
|
json: parsed.json,
|
|
1121
1126
|
})
|
|
1122
1127
|
}
|
|
@@ -2385,6 +2390,30 @@ mod tests {
|
|
|
2385
2390
|
assert!(!help.contains("错误细分走 status --summary"));
|
|
2386
2391
|
}
|
|
2387
2392
|
|
|
2393
|
+
#[test]
|
|
2394
|
+
fn models_defaults_provider_and_accepts_one_query_form() {
|
|
2395
|
+
let defaulted = models_args(&cli_argv(&["gpt", "5.6", "luna"])).unwrap();
|
|
2396
|
+
assert_eq!(defaulted.provider, "pi");
|
|
2397
|
+
assert_eq!(defaulted.search.as_deref(), Some("gpt 5.6 luna"));
|
|
2398
|
+
|
|
2399
|
+
let cursor = models_args(&cli_argv(&[
|
|
2400
|
+
"--provider",
|
|
2401
|
+
"cursor_agent",
|
|
2402
|
+
"--search",
|
|
2403
|
+
"GPT Luna",
|
|
2404
|
+
]))
|
|
2405
|
+
.unwrap();
|
|
2406
|
+
assert_eq!(cursor.provider, "cursor_agent");
|
|
2407
|
+
assert_eq!(cursor.search.as_deref(), Some("GPT Luna"));
|
|
2408
|
+
|
|
2409
|
+
let both = models_args(&cli_argv(&["luna", "--search", "gpt"])).unwrap_err();
|
|
2410
|
+
assert!(
|
|
2411
|
+
matches!(both, CliError::Usage(message) if message.contains("either QUERY or --search TEXT"))
|
|
2412
|
+
);
|
|
2413
|
+
let blank = models_args(&cli_argv(&["--search", " "])).unwrap();
|
|
2414
|
+
assert_eq!(blank.search, None);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2388
2417
|
#[test]
|
|
2389
2418
|
fn send_pane_positionals_are_message_not_target() {
|
|
2390
2419
|
let cwd = tmp_workspace();
|
|
@@ -1,53 +1,98 @@
|
|
|
1
|
-
//! Read-only
|
|
1
|
+
//! Read-only provider model catalog discovery for `team-agent models`.
|
|
2
2
|
use super::{CliError, CmdOutput, CmdResult, ExitCode, ModelsArgs};
|
|
3
3
|
use serde_json::{json, Value};
|
|
4
4
|
#[cfg(test)]
|
|
5
5
|
use std::collections::BTreeMap;
|
|
6
6
|
use std::path::Path;
|
|
7
|
-
use std::process::Command;
|
|
8
7
|
use std::time::Duration;
|
|
9
8
|
const CATALOG_TIMEOUT: Duration = Duration::from_secs(10);
|
|
10
9
|
const MAX_CATALOG_BYTES: u64 = 1024 * 1024;
|
|
11
10
|
|
|
11
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
12
|
+
enum CatalogFormat {
|
|
13
|
+
Pi,
|
|
14
|
+
CursorAgent,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[derive(Clone, Debug)]
|
|
18
|
+
struct ModelRecord {
|
|
19
|
+
provider: String,
|
|
20
|
+
vendor: String,
|
|
21
|
+
id: String,
|
|
22
|
+
display_name: String,
|
|
23
|
+
default: Option<bool>,
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
pub fn cmd_models(args: &ModelsArgs) -> Result<CmdResult, CliError> {
|
|
13
|
-
|
|
27
|
+
let (program, format) = match args.provider.as_str() {
|
|
28
|
+
"pi" => (Path::new("pi"), CatalogFormat::Pi),
|
|
29
|
+
"cursor_agent" => (Path::new("agent"), CatalogFormat::CursorAgent),
|
|
30
|
+
provider => {
|
|
31
|
+
return Ok(failure(
|
|
32
|
+
args,
|
|
33
|
+
&format!("unsupported model provider {provider:?}"),
|
|
34
|
+
))
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
cmd_models_with_format(args, program, format, CATALOG_TIMEOUT, MAX_CATALOG_BYTES)
|
|
14
38
|
}
|
|
15
39
|
|
|
40
|
+
/// Existing injectable Pi boundary retained for deterministic command/timeout tests.
|
|
16
41
|
fn cmd_models_with(
|
|
17
42
|
args: &ModelsArgs,
|
|
18
43
|
program: &Path,
|
|
19
44
|
timeout: Duration,
|
|
20
45
|
max_bytes: u64,
|
|
46
|
+
) -> Result<CmdResult, CliError> {
|
|
47
|
+
cmd_models_with_format(args, program, CatalogFormat::Pi, timeout, max_bytes)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn cmd_models_with_format(
|
|
51
|
+
args: &ModelsArgs,
|
|
52
|
+
program: &Path,
|
|
53
|
+
format: CatalogFormat,
|
|
54
|
+
timeout: Duration,
|
|
55
|
+
max_bytes: u64,
|
|
21
56
|
) -> Result<CmdResult, CliError> {
|
|
22
57
|
let bytes = match run_catalog(program, timeout, max_bytes) {
|
|
23
58
|
Ok(bytes) => bytes,
|
|
24
|
-
Err(message) => return Ok(failure(args, &message)),
|
|
59
|
+
Err(message) => return Ok(failure(args, &catalog_error_prefix(format, message))),
|
|
25
60
|
};
|
|
26
|
-
let all = match
|
|
61
|
+
let all = match parse_catalog(&bytes, format) {
|
|
27
62
|
Ok(models) => models,
|
|
28
|
-
Err(
|
|
63
|
+
Err(message) => return Ok(failure(args, &message)),
|
|
29
64
|
};
|
|
30
65
|
let visible = args.search.as_deref().map_or_else(
|
|
31
66
|
|| all.clone(),
|
|
32
67
|
|search| {
|
|
33
68
|
all.iter()
|
|
34
|
-
.filter(|model| model
|
|
69
|
+
.filter(|model| model_matches(model, search))
|
|
35
70
|
.cloned()
|
|
36
71
|
.collect::<Vec<_>>()
|
|
37
72
|
},
|
|
38
73
|
);
|
|
39
|
-
let current =
|
|
74
|
+
let current = current_model_for(format, &all);
|
|
40
75
|
let entries: Vec<Value> = visible
|
|
41
76
|
.iter()
|
|
42
77
|
.map(|model| {
|
|
43
78
|
json!({
|
|
44
|
-
"role_model": model,
|
|
45
|
-
"current":
|
|
79
|
+
"role_model": model.id,
|
|
80
|
+
"current": if format == CatalogFormat::Pi {
|
|
81
|
+
json!(current.as_deref() == Some(model.id.as_str()))
|
|
82
|
+
} else {
|
|
83
|
+
Value::Null
|
|
84
|
+
},
|
|
85
|
+
"provider": model.provider,
|
|
86
|
+
"vendor": model.vendor,
|
|
87
|
+
"model_id": model.id,
|
|
88
|
+
"display_name": model.display_name,
|
|
89
|
+
"default": model.default,
|
|
46
90
|
})
|
|
47
91
|
})
|
|
48
92
|
.collect();
|
|
93
|
+
let provider = args.provider.as_str();
|
|
49
94
|
let value = json!({
|
|
50
|
-
"schema_version": "models.v1", "ok": true, "provider":
|
|
95
|
+
"schema_version": "models.v1", "ok": true, "provider": provider, "models": entries,
|
|
51
96
|
"auth": "ok", "auth_basis": "catalog_visibility", "current_role_model": current, "search": args.search,
|
|
52
97
|
});
|
|
53
98
|
if args.json {
|
|
@@ -59,11 +104,25 @@ fn cmd_models_with(
|
|
|
59
104
|
preserve_json_order: true,
|
|
60
105
|
})
|
|
61
106
|
} else {
|
|
62
|
-
let mut lines = vec![
|
|
107
|
+
let mut lines = vec![format!(
|
|
108
|
+
"models.v1 | {provider} models (copyable model_id):"
|
|
109
|
+
)];
|
|
110
|
+
lines.push("PROVIDER VENDOR MODEL_ID DISPLAY_NAME CURRENT DEFAULT".to_string());
|
|
63
111
|
lines.extend(visible.iter().map(|model| {
|
|
64
112
|
format!(
|
|
65
|
-
"
|
|
66
|
-
|
|
113
|
+
"{} {} {} {} {} {}",
|
|
114
|
+
model.provider,
|
|
115
|
+
model.vendor,
|
|
116
|
+
model.id,
|
|
117
|
+
model.display_name,
|
|
118
|
+
if format == CatalogFormat::Pi {
|
|
119
|
+
(current.as_deref() == Some(model.id.as_str())).to_string()
|
|
120
|
+
} else {
|
|
121
|
+
"—".to_string()
|
|
122
|
+
},
|
|
123
|
+
model
|
|
124
|
+
.default
|
|
125
|
+
.map_or_else(|| "—".to_string(), |value| value.to_string()),
|
|
67
126
|
)
|
|
68
127
|
}));
|
|
69
128
|
lines.push(format!(
|
|
@@ -71,14 +130,135 @@ fn cmd_models_with(
|
|
|
71
130
|
visible.len()
|
|
72
131
|
));
|
|
73
132
|
if visible.is_empty() {
|
|
74
|
-
lines.push("No models matched --search.".to_string());
|
|
133
|
+
lines.push("No models matched --search/query.".to_string());
|
|
75
134
|
}
|
|
76
135
|
Ok(CmdResult::human(lines.join("\n")))
|
|
77
136
|
}
|
|
78
137
|
}
|
|
79
138
|
|
|
139
|
+
fn parse_catalog(bytes: &[u8], format: CatalogFormat) -> Result<Vec<ModelRecord>, String> {
|
|
140
|
+
match format {
|
|
141
|
+
CatalogFormat::Pi => crate::lifecycle::launch::pi_mcp::parse_pi_list_models_table(bytes)
|
|
142
|
+
.map(|models| {
|
|
143
|
+
models
|
|
144
|
+
.into_iter()
|
|
145
|
+
.map(|id| {
|
|
146
|
+
let vendor = id
|
|
147
|
+
.split_once('/')
|
|
148
|
+
.map_or_else(|| "pi".to_string(), |(vendor, _)| vendor.to_string());
|
|
149
|
+
ModelRecord {
|
|
150
|
+
provider: "pi".to_string(),
|
|
151
|
+
vendor,
|
|
152
|
+
display_name: id.clone(),
|
|
153
|
+
id,
|
|
154
|
+
default: None,
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.collect()
|
|
158
|
+
})
|
|
159
|
+
.map_err(safe_catalog_error),
|
|
160
|
+
CatalogFormat::CursorAgent => parse_cursor_catalog(bytes),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn parse_cursor_catalog(bytes: &[u8]) -> Result<Vec<ModelRecord>, String> {
|
|
165
|
+
let text = std::str::from_utf8(bytes)
|
|
166
|
+
.map_err(|_| "Cursor model catalog is not valid UTF-8".to_string())?;
|
|
167
|
+
let mut models = Vec::new();
|
|
168
|
+
for line in text.lines() {
|
|
169
|
+
let line = line.trim();
|
|
170
|
+
if line.is_empty()
|
|
171
|
+
|| line
|
|
172
|
+
.trim_end_matches(':')
|
|
173
|
+
.eq_ignore_ascii_case("available models")
|
|
174
|
+
{
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let Some((id, display)) = line.split_once(" - ") else {
|
|
178
|
+
return Err("Cursor model catalog contains a malformed row".to_string());
|
|
179
|
+
};
|
|
180
|
+
let id = id.trim();
|
|
181
|
+
if id.is_empty() || id.chars().any(char::is_whitespace) || display.trim().is_empty() {
|
|
182
|
+
return Err("Cursor model catalog contains a malformed row".to_string());
|
|
183
|
+
}
|
|
184
|
+
if models.iter().any(|model: &ModelRecord| model.id == id) {
|
|
185
|
+
return Err("Cursor model catalog contains duplicate model ids".to_string());
|
|
186
|
+
}
|
|
187
|
+
let default = display.trim_end().ends_with("(default)");
|
|
188
|
+
let display_name = display.trim().strip_suffix("(default)").map_or_else(
|
|
189
|
+
|| display.trim().to_string(),
|
|
190
|
+
|name| name.trim().to_string(),
|
|
191
|
+
);
|
|
192
|
+
let vendor = id
|
|
193
|
+
.split_once('-')
|
|
194
|
+
.map_or_else(|| "cursor".to_string(), |(vendor, _)| vendor.to_string());
|
|
195
|
+
models.push(ModelRecord {
|
|
196
|
+
provider: "cursor_agent".to_string(),
|
|
197
|
+
vendor,
|
|
198
|
+
id: id.to_string(),
|
|
199
|
+
display_name,
|
|
200
|
+
default: Some(default),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if models.is_empty() {
|
|
204
|
+
return Err("Cursor model catalog is empty".to_string());
|
|
205
|
+
}
|
|
206
|
+
Ok(models)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn model_matches(model: &ModelRecord, search: &str) -> bool {
|
|
210
|
+
let fields = [
|
|
211
|
+
model.provider.as_str(),
|
|
212
|
+
model.vendor.as_str(),
|
|
213
|
+
model.id.as_str(),
|
|
214
|
+
model.display_name.as_str(),
|
|
215
|
+
];
|
|
216
|
+
search
|
|
217
|
+
.split_whitespace()
|
|
218
|
+
.map(str::to_lowercase)
|
|
219
|
+
.all(|needle| {
|
|
220
|
+
fields
|
|
221
|
+
.iter()
|
|
222
|
+
.any(|field| field.to_lowercase().contains(needle.as_str()))
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn current_model_for(format: CatalogFormat, models: &[ModelRecord]) -> Option<String> {
|
|
227
|
+
match format {
|
|
228
|
+
CatalogFormat::Pi => current_role_model(
|
|
229
|
+
&models
|
|
230
|
+
.iter()
|
|
231
|
+
.map(|model| model.id.clone())
|
|
232
|
+
.collect::<Vec<_>>(),
|
|
233
|
+
),
|
|
234
|
+
// Cursor's catalog does not expose the active role model; never mark a
|
|
235
|
+
// catalog default as current without an explicit runtime identity.
|
|
236
|
+
CatalogFormat::CursorAgent => None,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn catalog_error_prefix(format: CatalogFormat, message: String) -> String {
|
|
241
|
+
match format {
|
|
242
|
+
CatalogFormat::Pi => message,
|
|
243
|
+
CatalogFormat::CursorAgent => message.replace("Pi", "Cursor"),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
80
247
|
fn failure(args: &ModelsArgs, message: &str) -> CmdResult {
|
|
81
|
-
let
|
|
248
|
+
let executable = if args.provider == "cursor_agent" {
|
|
249
|
+
"agent"
|
|
250
|
+
} else {
|
|
251
|
+
"pi"
|
|
252
|
+
};
|
|
253
|
+
let action = if message.starts_with("unsupported model provider") {
|
|
254
|
+
"choose a supported provider: pi or cursor_agent".to_string()
|
|
255
|
+
} else {
|
|
256
|
+
format!(
|
|
257
|
+
"install or repair the PATH-first `{executable}` executable, then retry `team-agent models --provider {}`",
|
|
258
|
+
args.provider
|
|
259
|
+
)
|
|
260
|
+
};
|
|
261
|
+
let value = json!({ "schema_version": "models.v1", "ok": false, "provider": args.provider, "auth": "not_ready", "auth_basis": "catalog_visibility", "models": Value::Array(Vec::new()), "current_role_model": Value::Null, "error": message, "action": action });
|
|
82
262
|
if args.json {
|
|
83
263
|
let text = serde_json::to_string_pretty(&value)
|
|
84
264
|
.unwrap_or_else(|_| "{\"schema_version\":\"models.v1\",\"ok\":false}".to_string());
|
|
@@ -89,7 +269,7 @@ fn failure(args: &ModelsArgs, message: &str) -> CmdResult {
|
|
|
89
269
|
preserve_json_order: true,
|
|
90
270
|
}
|
|
91
271
|
} else {
|
|
92
|
-
CmdResult { output: CmdOutput::Human(format!("models.v1 | error: {message}\naction:
|
|
272
|
+
CmdResult { output: CmdOutput::Human(format!("models.v1 | error: {message}\naction: {action}\nauth: not_ready (catalog_visibility)")), exit: ExitCode::Error, as_json: false, preserve_json_order: false }
|
|
93
273
|
}
|
|
94
274
|
}
|
|
95
275
|
|
|
@@ -279,6 +459,38 @@ mod tests {
|
|
|
279
459
|
assert_eq!(models[1]["role_model"], "team-agent/gpt-5.6-luna");
|
|
280
460
|
}
|
|
281
461
|
|
|
462
|
+
#[cfg(unix)]
|
|
463
|
+
#[test]
|
|
464
|
+
fn cursor_catalog_parses_copyable_ids_and_multiword_search() {
|
|
465
|
+
let path = fixture(
|
|
466
|
+
"printf 'Available models\\ngpt-5.6-luna-high - GPT-5.6 Luna 1M High\\ncursor-grok-4.6-high - Cursor Grok 4.6\\nauto - Auto (default)\\n'",
|
|
467
|
+
);
|
|
468
|
+
let result = cmd_models_with_format(
|
|
469
|
+
&ModelsArgs {
|
|
470
|
+
provider: "cursor_agent".into(),
|
|
471
|
+
search: Some("GPT luna".into()),
|
|
472
|
+
json: true,
|
|
473
|
+
},
|
|
474
|
+
&path,
|
|
475
|
+
CatalogFormat::CursorAgent,
|
|
476
|
+
Duration::from_secs(1),
|
|
477
|
+
MAX_CATALOG_BYTES,
|
|
478
|
+
)
|
|
479
|
+
.unwrap();
|
|
480
|
+
let CmdOutput::Human(text) = result.output else {
|
|
481
|
+
panic!("expected JSON projection")
|
|
482
|
+
};
|
|
483
|
+
let value: Value = serde_json::from_str(&text).unwrap();
|
|
484
|
+
assert_eq!(value["ok"], true);
|
|
485
|
+
assert_eq!(value["provider"], "cursor_agent");
|
|
486
|
+
assert_eq!(value["models"].as_array().unwrap().len(), 1);
|
|
487
|
+
assert_eq!(value["models"][0]["role_model"], "gpt-5.6-luna-high");
|
|
488
|
+
assert_eq!(value["models"][0]["model_id"], "gpt-5.6-luna-high");
|
|
489
|
+
assert_eq!(value["models"][0]["display_name"], "GPT-5.6 Luna 1M High");
|
|
490
|
+
assert_eq!(value["models"][0]["current"], Value::Null);
|
|
491
|
+
cleanup_fixture(&path);
|
|
492
|
+
}
|
|
493
|
+
|
|
282
494
|
#[cfg(unix)]
|
|
283
495
|
fn fixture(body: &str) -> std::path::PathBuf {
|
|
284
496
|
use std::os::unix::fs::PermissionsExt;
|
|
@@ -785,12 +997,7 @@ mod tests {
|
|
|
785
997
|
&["--provider".into(), "codex".into()],
|
|
786
998
|
Path::new("/tmp"),
|
|
787
999
|
);
|
|
788
|
-
|
|
789
|
-
Err(CliError::Usage(message)) => {
|
|
790
|
-
assert_eq!(message, "models supports only --provider pi, got \"codex\"")
|
|
791
|
-
}
|
|
792
|
-
other => panic!("expected provider refusal, got {other:?}"),
|
|
793
|
-
}
|
|
1000
|
+
assert!(matches!(refusal, Ok(ExitCode::Error)));
|
|
794
1001
|
cleanup_fixture(&path);
|
|
795
1002
|
}
|
|
796
1003
|
|
|
@@ -894,7 +1101,7 @@ mod tests {
|
|
|
894
1101
|
);
|
|
895
1102
|
assert_eq!(
|
|
896
1103
|
crate::cli::spec::command_spec("models").unwrap().usage,
|
|
897
|
-
"usage: team-agent models --provider pi [
|
|
1104
|
+
"usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]"
|
|
898
1105
|
);
|
|
899
1106
|
}
|
|
900
1107
|
|
|
@@ -169,7 +169,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
169
169
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
170
170
|
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
171
171
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
172
|
-
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models --provider pi [
|
|
172
|
+
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
173
173
|
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
174
174
|
CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
175
175
|
CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -163,30 +163,28 @@ fn verify_cleanup_surviving_receipt(
|
|
|
163
163
|
assert!(!receipt.exists(), "cleanup must remove only the exact receipt path");
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// ── #36 report_result
|
|
167
|
-
// GOLDEN (probe_setdefault.py): envelope {agent_id:env-agent, task_id:env-task,...}
|
|
168
|
-
// + explicit args agent_id=ARG-agent, task_id=ARG-task → returned dict keeps
|
|
169
|
-
// env-agent / env-task (setdefault). Rust unconditionally insert-overrides.
|
|
166
|
+
// ── #36 report_result identity: neither envelope nor args can override capture ─
|
|
170
167
|
#[test]
|
|
171
|
-
fn
|
|
168
|
+
fn report_result_rejects_envelope_and_argument_identity_mismatch() {
|
|
172
169
|
let tools = TeamOrchestratorTools::with_identity(
|
|
173
170
|
&unique_ws("report-setdefault"),
|
|
174
171
|
Some(AgentId::new("env-id")),
|
|
175
172
|
None,
|
|
176
173
|
);
|
|
177
|
-
let
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
assert_eq!(
|
|
189
|
-
|
|
174
|
+
let error = tools
|
|
175
|
+
.report_result(
|
|
176
|
+
Some(&json!({
|
|
177
|
+
"agent_id": "env-agent", "task_id": "env-task",
|
|
178
|
+
"status": "blocked", "summary": "env summary"
|
|
179
|
+
})),
|
|
180
|
+
Some("ARG summary"), ResultStatus::Success,
|
|
181
|
+
None, None, None, None, None,
|
|
182
|
+
Some("ARG-task"), Some("ARG-agent"),
|
|
183
|
+
)
|
|
184
|
+
.expect_err("envelope and argument identities must be bound to the worker capture");
|
|
185
|
+
assert_eq!(error.reason, ToolErrorReason::McpScopeRefused);
|
|
186
|
+
assert!(error.message.contains("identity_mismatch"));
|
|
187
|
+
assert_eq!(error.extra.get("expected_agent_id"), Some(&json!("env-id")));
|
|
190
188
|
}
|
|
191
189
|
|
|
192
190
|
// ── #44 report_result task_id inference from state (_latest_task_for_assignee)
|
|
@@ -25,31 +25,42 @@
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
#[test]
|
|
28
|
-
fn
|
|
28
|
+
fn report_result_rejects_explicit_agent_mismatch() {
|
|
29
29
|
let tools = TeamOrchestratorTools::with_identity(
|
|
30
|
-
&unique_ws("report-explicit"),
|
|
30
|
+
&unique_ws("report-explicit-mismatch"),
|
|
31
31
|
Some(AgentId::new("worker-7")),
|
|
32
32
|
Some(TeamKey::new("teamA")),
|
|
33
33
|
);
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
assert_eq!(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
34
|
+
let error = tools
|
|
35
|
+
.report_result(
|
|
36
|
+
None, Some("done"), ResultStatus::Success,
|
|
37
|
+
None, None, None, None, None,
|
|
38
|
+
Some("task-9"), Some("explicit-agent"),
|
|
39
|
+
)
|
|
40
|
+
.expect_err("a request agent_id cannot override the captured identity");
|
|
41
|
+
assert_eq!(error.reason, ToolErrorReason::McpScopeRefused);
|
|
42
|
+
assert!(error.message.contains("identity_mismatch"));
|
|
43
|
+
assert_eq!(error.extra.get("expected_agent_id"), Some(&json!("worker-7")));
|
|
44
|
+
assert_eq!(error.extra.get("provided_agent_id"), Some(&json!("explicit-agent")));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn report_result_accepts_explicit_captured_agent() {
|
|
49
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
50
|
+
&unique_ws("report-explicit-match"),
|
|
51
|
+
Some(AgentId::new("worker-7")),
|
|
52
|
+
Some(TeamKey::new("teamA")),
|
|
52
53
|
);
|
|
54
|
+
let ok = tools
|
|
55
|
+
.report_result(
|
|
56
|
+
None, Some("done"), ResultStatus::Success,
|
|
57
|
+
None, None, None, None, None,
|
|
58
|
+
Some("task-9"), Some("worker-7"),
|
|
59
|
+
)
|
|
60
|
+
.expect("matching explicit agent_id remains compatible");
|
|
61
|
+
let v = serde_json::to_value(&ok).unwrap();
|
|
62
|
+
assert_eq!(v.get("agent_id"), Some(&json!("worker-7")));
|
|
63
|
+
assert_eq!(v.get("task_id"), Some(&json!("task-9")));
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
#[test]
|
|
@@ -264,6 +264,36 @@ fn dispatch_unknown_tool_returns_unknown_tool_error() {
|
|
|
264
264
|
assert_eq!(err.message, "unknown tool 'nope'");
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
#[test]
|
|
268
|
+
fn worker_identity_is_bound_before_send_or_report_dispatch() {
|
|
269
|
+
let tools = TeamOrchestratorTools::with_identity(
|
|
270
|
+
Path::new("/tmp/ws"),
|
|
271
|
+
Some(AgentId::new("worker-7")),
|
|
272
|
+
None,
|
|
273
|
+
);
|
|
274
|
+
let send = dispatch(
|
|
275
|
+
&tools,
|
|
276
|
+
&json!({
|
|
277
|
+
"tool": "send_message",
|
|
278
|
+
"arguments": {"to": "leader", "content": "spoof", "agent_id": "in-dev2"}
|
|
279
|
+
}),
|
|
280
|
+
)
|
|
281
|
+
.expect_err("send_message must reject a spoofed request identity");
|
|
282
|
+
assert!(send.message.contains("identity_mismatch"));
|
|
283
|
+
assert_eq!(send.extra.get("expected_agent_id"), Some(&json!("worker-7")));
|
|
284
|
+
|
|
285
|
+
let report = dispatch(
|
|
286
|
+
&tools,
|
|
287
|
+
&json!({
|
|
288
|
+
"tool": "report_result",
|
|
289
|
+
"arguments": {"envelope": {"agent_id": "in-dev2", "summary": "spoof"}}
|
|
290
|
+
}),
|
|
291
|
+
)
|
|
292
|
+
.expect_err("report_result envelope must not override captured identity");
|
|
293
|
+
assert!(report.message.contains("identity_mismatch"));
|
|
294
|
+
assert_eq!(report.extra.get("provided_agent_id"), Some(&json!("in-dev2")));
|
|
295
|
+
}
|
|
296
|
+
|
|
267
297
|
// ════════════════════════════════════════════════════════════════════════
|
|
268
298
|
// requires_ack_for_target — leader-only → false (tools.py:16)
|
|
269
299
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -82,6 +82,107 @@ impl TeamOrchestratorTools {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/// Validate an identity supplied by an MCP request against the identity captured
|
|
86
|
+
/// when this worker server started. Missing request fields are intentionally valid;
|
|
87
|
+
/// the captured identity is the only fallback. Present values must be non-empty and
|
|
88
|
+
/// equal to that capture, otherwise reject before routing or persistence.
|
|
89
|
+
pub(crate) fn validate_identity_argument(
|
|
90
|
+
&self,
|
|
91
|
+
tool: &str,
|
|
92
|
+
value: Option<&Value>,
|
|
93
|
+
) -> Result<(), ToolError> {
|
|
94
|
+
let expected = self.require_captured_identity(tool)?;
|
|
95
|
+
let Some(value) = value else {
|
|
96
|
+
return Ok(());
|
|
97
|
+
};
|
|
98
|
+
let Some(provided) = value.as_str().and_then(non_empty_string) else {
|
|
99
|
+
return Err(self.identity_mismatch(tool, expected.as_str(), None));
|
|
100
|
+
};
|
|
101
|
+
if provided != expected.as_str() {
|
|
102
|
+
return Err(self.identity_mismatch(tool, expected.as_str(), Some(provided)));
|
|
103
|
+
}
|
|
104
|
+
Ok(())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Report requests can carry identity in both the top-level argument and the
|
|
108
|
+
/// envelope. Validate both independently so an envelope cannot overwrite the
|
|
109
|
+
/// framework-bound sender after the top-level check.
|
|
110
|
+
pub(crate) fn validate_report_identity_request(
|
|
111
|
+
&self,
|
|
112
|
+
envelope: Option<&Value>,
|
|
113
|
+
explicit_agent_id: Option<&Value>,
|
|
114
|
+
) -> Result<(), ToolError> {
|
|
115
|
+
self.validate_identity_argument("report_result", explicit_agent_id)?;
|
|
116
|
+
if let Some(value) = envelope
|
|
117
|
+
.and_then(Value::as_object)
|
|
118
|
+
.and_then(|obj| obj.get("agent_id"))
|
|
119
|
+
{
|
|
120
|
+
self.validate_identity_argument("report_result", Some(value))?;
|
|
121
|
+
}
|
|
122
|
+
Ok(())
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn require_captured_identity(&self, tool: &str) -> Result<&AgentId, ToolError> {
|
|
126
|
+
self.agent_id
|
|
127
|
+
.as_ref()
|
|
128
|
+
.filter(|agent| non_empty_string(agent.as_str()).is_some())
|
|
129
|
+
.ok_or_else(|| self.identity_mismatch(tool, "<missing>", None))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn validate_report_identity_text(
|
|
133
|
+
&self,
|
|
134
|
+
envelope: Option<&Value>,
|
|
135
|
+
explicit_agent_id: Option<&str>,
|
|
136
|
+
) -> Result<(), ToolError> {
|
|
137
|
+
let expected = self.require_captured_identity("report_result")?;
|
|
138
|
+
if let Some(provided) = explicit_agent_id.and_then(non_empty_string) {
|
|
139
|
+
if provided != expected.as_str() {
|
|
140
|
+
return Err(self.identity_mismatch(
|
|
141
|
+
"report_result",
|
|
142
|
+
expected.as_str(),
|
|
143
|
+
Some(provided),
|
|
144
|
+
));
|
|
145
|
+
}
|
|
146
|
+
} else if explicit_agent_id.is_some() {
|
|
147
|
+
return Err(self.identity_mismatch("report_result", expected.as_str(), None));
|
|
148
|
+
}
|
|
149
|
+
if let Some(value) = envelope
|
|
150
|
+
.and_then(Value::as_object)
|
|
151
|
+
.and_then(|obj| obj.get("agent_id"))
|
|
152
|
+
{
|
|
153
|
+
self.validate_identity_argument("report_result", Some(value))?;
|
|
154
|
+
}
|
|
155
|
+
Ok(())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn identity_mismatch(&self, tool: &str, expected: &str, provided: Option<&str>) -> ToolError {
|
|
159
|
+
let mut error = ToolError::new(
|
|
160
|
+
ToolErrorReason::McpScopeRefused,
|
|
161
|
+
format!(
|
|
162
|
+
"identity_mismatch: {tool} agent_id must match framework-injected TEAM_AGENT_ID"
|
|
163
|
+
),
|
|
164
|
+
"IdentityError",
|
|
165
|
+
);
|
|
166
|
+
error
|
|
167
|
+
.extra
|
|
168
|
+
.insert("status".to_string(), Value::String("refused".to_string()));
|
|
169
|
+
error
|
|
170
|
+
.extra
|
|
171
|
+
.insert("identity_mismatch".to_string(), Value::Bool(true));
|
|
172
|
+
error
|
|
173
|
+
.extra
|
|
174
|
+
.insert("tool".to_string(), Value::String(tool.to_string()));
|
|
175
|
+
error.extra.insert(
|
|
176
|
+
"expected_agent_id".to_string(),
|
|
177
|
+
Value::String(expected.to_string()),
|
|
178
|
+
);
|
|
179
|
+
error.extra.insert(
|
|
180
|
+
"provided_agent_id".to_string(),
|
|
181
|
+
provided.map_or(Value::Null, |value| Value::String(value.to_string())),
|
|
182
|
+
);
|
|
183
|
+
error
|
|
184
|
+
}
|
|
185
|
+
|
|
85
186
|
/// `assign_task` (`tools.py:84-133`): C8 Family-B task-view reconcile then deliver.
|
|
86
187
|
/// Resolves team key from owner-team env (or `active_team_key`), appends or
|
|
87
188
|
/// field-updates the task in state, then delegates delivery to
|
|
@@ -214,6 +315,8 @@ impl TeamOrchestratorTools {
|
|
|
214
315
|
mailbox: Option<&Value>,
|
|
215
316
|
presentation: Option<&Value>,
|
|
216
317
|
) -> Result<SendOutcome, ToolError> {
|
|
318
|
+
// A worker without a spawn-time identity cannot safely emit a message.
|
|
319
|
+
self.require_captured_identity("send_message")?;
|
|
217
320
|
let normalized =
|
|
218
321
|
crate::messaging::presentation::normalize_send_presentation(mailbox, presentation)
|
|
219
322
|
.map_err(|error| {
|
|
@@ -430,6 +533,8 @@ impl TeamOrchestratorTools {
|
|
|
430
533
|
agent_id: Option<&str>,
|
|
431
534
|
presentation: Option<&Value>,
|
|
432
535
|
) -> ToolResult {
|
|
536
|
+
// Validate before scope reads, envelope normalization, or result persistence.
|
|
537
|
+
self.validate_report_identity_text(envelope, agent_id)?;
|
|
433
538
|
if let Some(envelope) = envelope {
|
|
434
539
|
self.validate_rpc_scope_args("report_result", envelope)?;
|
|
435
540
|
}
|
|
@@ -538,17 +643,14 @@ impl TeamOrchestratorTools {
|
|
|
538
643
|
.or_insert(Value::String(source.to_string()));
|
|
539
644
|
}
|
|
540
645
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
.unwrap_or_else(|| "unknown".to_string());
|
|
550
|
-
obj.insert("agent_id".to_string(), Value::String(resolved));
|
|
551
|
-
}
|
|
646
|
+
// The framework capture is authoritative even when a matching request
|
|
647
|
+
// value was supplied; this also canonicalizes surrounding whitespace.
|
|
648
|
+
let resolved = self
|
|
649
|
+
.agent_id
|
|
650
|
+
.as_ref()
|
|
651
|
+
.map(|env_agent| env_agent.as_str().to_string())
|
|
652
|
+
.unwrap_or_else(|| "unknown".to_string());
|
|
653
|
+
obj.insert("agent_id".to_string(), Value::String(resolved));
|
|
552
654
|
if !obj.contains_key("changes") {
|
|
553
655
|
obj.insert(
|
|
554
656
|
"changes".to_string(),
|
|
@@ -496,7 +496,9 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
496
496
|
insert_property(
|
|
497
497
|
&mut properties,
|
|
498
498
|
"agent_id",
|
|
499
|
-
string_property(
|
|
499
|
+
string_property(
|
|
500
|
+
"Optional reporting agent id; must match framework-injected TEAM_AGENT_ID.",
|
|
501
|
+
),
|
|
500
502
|
);
|
|
501
503
|
insert_property(
|
|
502
504
|
&mut properties,
|
|
@@ -671,6 +673,23 @@ pub(crate) fn dispatch_tool(
|
|
|
671
673
|
tool: McpTool,
|
|
672
674
|
args: &Value,
|
|
673
675
|
) -> ToolResult {
|
|
676
|
+
// Check framework-bound identity before scope resolution or any tool side effect.
|
|
677
|
+
match tool {
|
|
678
|
+
McpTool::SendMessage => {
|
|
679
|
+
tools.validate_identity_argument("send_message", args.get("agent_id"))?;
|
|
680
|
+
if let Some(value) = args
|
|
681
|
+
.get("envelope")
|
|
682
|
+
.and_then(Value::as_object)
|
|
683
|
+
.and_then(|object| object.get("agent_id"))
|
|
684
|
+
{
|
|
685
|
+
tools.validate_identity_argument("send_message", Some(value))?;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
McpTool::ReportResult => {
|
|
689
|
+
tools.validate_report_identity_request(args.get("envelope"), args.get("agent_id"))?
|
|
690
|
+
}
|
|
691
|
+
_ => {}
|
|
692
|
+
}
|
|
674
693
|
if scope_ceiling_tool(tool) {
|
|
675
694
|
tools.validate_rpc_scope_args(tool.wire_name(), args)?;
|
|
676
695
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.84",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.84",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.84",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.84"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|