@team-agent/installer 0.5.66 → 0.5.68
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 +8 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/cli/adapters.rs +5 -0
- package/crates/team-agent/src/cli/diagnose.rs +82 -0
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +21 -7
- package/crates/team-agent/src/cli/leaders.rs +125 -1
- package/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/cli/send/presentation.rs +7 -1
- package/crates/team-agent/src/cli/spec.rs +2 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
- package/crates/team-agent/src/cli/tests/base.rs +26 -3
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
- package/crates/team-agent/src/communication_mode/mod.rs +6 -4
- package/crates/team-agent/src/compiler.rs +59 -0
- package/crates/team-agent/src/coordinator/backoff.rs +55 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
- package/crates/team-agent/src/coordinator/health.rs +173 -0
- package/crates/team-agent/src/coordinator/mod.rs +31 -0
- package/crates/team-agent/src/coordinator/orphan.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
- package/crates/team-agent/src/coordinator/tick.rs +133 -22
- package/crates/team-agent/src/coordinator/types.rs +49 -0
- package/crates/team-agent/src/db/message_store.rs +39 -5
- package/crates/team-agent/src/layout/worker_env.rs +20 -3
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
- package/crates/team-agent/src/leader/registry.rs +65 -0
- package/crates/team-agent/src/leader/start.rs +920 -63
- package/crates/team-agent/src/lifecycle/display.rs +56 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
- package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
- package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
- package/crates/team-agent/src/lifecycle/launch.rs +99 -29
- package/crates/team-agent/src/lifecycle/lock.rs +42 -1
- package/crates/team-agent/src/lifecycle/mod.rs +11 -0
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
- package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
- package/crates/team-agent/src/lifecycle/restart.rs +29 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
- package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
- package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
- package/crates/team-agent/src/lifecycle/tests.rs +23 -13
- package/crates/team-agent/src/lifecycle/types.rs +61 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
- package/crates/team-agent/src/mcp_server/tests.rs +196 -2
- package/crates/team-agent/src/messaging/delivery.rs +405 -40
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
- package/crates/team-agent/src/messaging/results.rs +4 -0
- package/crates/team-agent/src/messaging/send.rs +34 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
- package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/types.rs +4 -1
- package/crates/team-agent/src/model/enums.rs +14 -5
- package/crates/team-agent/src/model/permissions.rs +3 -0
- package/crates/team-agent/src/os_probe.rs +73 -2
- package/crates/team-agent/src/provider/adapter.rs +231 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
- package/crates/team-agent/src/provider/classify.rs +4 -2
- package/crates/team-agent/src/provider/faults.rs +2 -1
- package/crates/team-agent/src/provider/mod.rs +11 -0
- package/crates/team-agent/src/provider/session/capture.rs +158 -20
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
- package/crates/team-agent/src/provider/session/mod.rs +18 -0
- package/crates/team-agent/src/provider/session/resume.rs +57 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
- package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
- package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
- package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +23 -1
- package/crates/team-agent/src/state/persist.rs +301 -8
- package/crates/team-agent/src/state/repository.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
- package/crates/team-agent/src/tmux_backend.rs +1064 -75
- package/crates/team-agent/src/transport/tests/wire.rs +28 -2
- package/crates/team-agent/src/transport.rs +248 -1
- package/npm/install.mjs +129 -73
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +33 -238
- package/skills/team-agent/command-coverage.json +31 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
- package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 把 team_orchestrator 身份写进 cursor 实际会读的 mcp.json
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: apply_cursor_mcp_overlay
|
|
6
|
+
//! what: 写 <workspace>/.cursor/mcp.json,env 必须带 TEAM_AGENT_ID
|
|
7
|
+
//! - name: cursor_mcp_enable_argv
|
|
8
|
+
//! what: 组 `agent mcp enable team_orchestrator`(不认 --workspace)
|
|
9
|
+
//! - name: enable_cursor_workspace_mcp
|
|
10
|
+
//! what: 在物理工作目录跑 enable;测试隔离下跳过以免写 ~/.cursor
|
|
11
|
+
//! - name: physical_workspace_path
|
|
12
|
+
//! what: pwd -P 等价路径;mcp enable 按 getcwd 分片
|
|
13
|
+
//! - name: refuse_second_cursor_occupant
|
|
14
|
+
//! what: 同一物理 workspace 第二 CursorAgent 拒绝(mcp.json last-writer)
|
|
15
|
+
//! boundary:
|
|
16
|
+
//! - 同 workspace 第二 CursorAgent 拒绝;U-07 过前不装「已支持多席」
|
|
17
|
+
//! - 不把身份从 json env 删掉(cursor 不继承父进程 TEAM_AGENT_*)
|
|
18
|
+
//! - 不写 ~/.cursor/mcp.json 全局文件
|
|
19
|
+
//! - 不调 --approve-mcps(未验证能代替 enable)
|
|
20
|
+
//! maturity: wired
|
|
21
|
+
//! ---
|
|
22
|
+
|
|
23
|
+
use std::collections::BTreeMap;
|
|
24
|
+
use std::path::{Path, PathBuf};
|
|
25
|
+
use std::process::Command;
|
|
26
|
+
|
|
27
|
+
use crate::lifecycle::LifecycleError;
|
|
28
|
+
use crate::model::yaml::Value as YamlValue;
|
|
29
|
+
use crate::provider::wire::command_name;
|
|
30
|
+
use crate::provider::Provider;
|
|
31
|
+
|
|
32
|
+
/// Keys that must appear in mcp.json env. Cursor strips parent env down to
|
|
33
|
+
/// HOME/PATH/TERM/… — TEAM_AGENT_ID only survives if it is in this table.
|
|
34
|
+
const REQUIRED_IDENTITY_KEYS: &[&str] = &[
|
|
35
|
+
"TEAM_AGENT_WORKSPACE",
|
|
36
|
+
"TEAM_AGENT_ID",
|
|
37
|
+
"TEAM_AGENT_OWNER_TEAM_ID",
|
|
38
|
+
"TEAM_AGENT_AUTH_MODE",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/// ---
|
|
42
|
+
/// purpose: 给出 workspace 的物理路径
|
|
43
|
+
/// returns: 能 canonicalize 就用它,否则原样返回
|
|
44
|
+
/// ---
|
|
45
|
+
pub fn physical_workspace_path(workspace: &Path) -> PathBuf {
|
|
46
|
+
std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn status_is_live(status: &str) -> bool {
|
|
50
|
+
!matches!(
|
|
51
|
+
status,
|
|
52
|
+
"stopped" | "stopping" | "removed" | "spawn_failed" | "failed"
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn agent_is_cursor(provider: Option<&str>) -> bool {
|
|
57
|
+
matches!(
|
|
58
|
+
provider.and_then(crate::lifecycle::profile_launch::parse_provider),
|
|
59
|
+
Some(Provider::CursorAgent)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// ---
|
|
64
|
+
/// purpose: 同一物理 workspace 拒绝第二 CursorAgent
|
|
65
|
+
/// params:
|
|
66
|
+
/// workspace: 物理工作目录
|
|
67
|
+
/// incoming_id: 正要起的席位 id
|
|
68
|
+
/// spec: 可选 yaml spec,用来在 state 尚未写入时看见第二席
|
|
69
|
+
/// returns: 已有其它 CursorAgent 则 RequirementUnmet,文案含 TEAM_AGENT_ID 与 mcp.json
|
|
70
|
+
/// contract:
|
|
71
|
+
/// provides:
|
|
72
|
+
/// - name: refuse_second_cursor_occupant
|
|
73
|
+
/// what: mcp.json last-writer 闸;U-07 过前不装多席
|
|
74
|
+
/// boundary:
|
|
75
|
+
/// - 不改 grok 独占实现
|
|
76
|
+
/// ---
|
|
77
|
+
pub fn refuse_second_cursor_occupant(
|
|
78
|
+
workspace: &Path,
|
|
79
|
+
incoming_id: &str,
|
|
80
|
+
spec: Option<&YamlValue>,
|
|
81
|
+
) -> Result<(), LifecycleError> {
|
|
82
|
+
let mut others = Vec::new();
|
|
83
|
+
if let Some(spec) = spec {
|
|
84
|
+
if let Some(agents) = spec.get("agents").and_then(YamlValue::as_list) {
|
|
85
|
+
for agent in agents {
|
|
86
|
+
let Some(id) = agent.get("id").and_then(YamlValue::as_str) else {
|
|
87
|
+
continue;
|
|
88
|
+
};
|
|
89
|
+
if id == incoming_id {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if agent_is_paused_yaml(agent) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if agent_is_cursor(agent.get("provider").and_then(YamlValue::as_str)) {
|
|
96
|
+
others.push(id.to_string());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if let Ok(state) = crate::state::persist::load_runtime_state(workspace) {
|
|
102
|
+
if let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) {
|
|
103
|
+
for (id, agent) in agents {
|
|
104
|
+
if id == incoming_id {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
let provider = agent.get("provider").and_then(serde_json::Value::as_str);
|
|
108
|
+
if !agent_is_cursor(provider) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
let status = agent
|
|
112
|
+
.get("status")
|
|
113
|
+
.and_then(serde_json::Value::as_str)
|
|
114
|
+
.unwrap_or("running");
|
|
115
|
+
if status_is_live(status) {
|
|
116
|
+
others.push(id.clone());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
others.sort();
|
|
122
|
+
others.dedup();
|
|
123
|
+
if others.is_empty() {
|
|
124
|
+
return Ok(());
|
|
125
|
+
}
|
|
126
|
+
Err(LifecycleError::RequirementUnmet(format!(
|
|
127
|
+
"error: cursor_agent seat already occupies this workspace\n\
|
|
128
|
+
incoming: {incoming_id}\n\
|
|
129
|
+
occupants: {}\n\
|
|
130
|
+
reason: <workspace>/.cursor/mcp.json is directory-scoped; a second seat overwrites TEAM_AGENT_ID (last-writer)\n\
|
|
131
|
+
workspace: {}\n\
|
|
132
|
+
action: do not add another CursorAgent in this workspace until per-seat MCP identity is isolated",
|
|
133
|
+
others.join(", "),
|
|
134
|
+
workspace.display(),
|
|
135
|
+
)))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn agent_is_paused_yaml(agent: &YamlValue) -> bool {
|
|
139
|
+
matches!(agent.get("paused"), Some(YamlValue::Bool(true)))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// ---
|
|
143
|
+
/// purpose: 写 workspace 下 cursor 会读的 mcp.json,env 里必须带席位身份键
|
|
144
|
+
/// params:
|
|
145
|
+
/// mcp_config: 已解析的 MCP 配置,须含 team_orchestrator 与 command
|
|
146
|
+
/// returns: 写出的文件路径
|
|
147
|
+
/// errors: 缺条目或缺 command 时返回 StatePersist,写盘失败也返回 StatePersist
|
|
148
|
+
/// ---
|
|
149
|
+
pub fn apply_cursor_mcp_overlay(
|
|
150
|
+
workspace: &Path,
|
|
151
|
+
mcp_config: &crate::provider::McpConfig,
|
|
152
|
+
) -> Result<PathBuf, LifecycleError> {
|
|
153
|
+
let server = mcp_config
|
|
154
|
+
.raw
|
|
155
|
+
.get("team_orchestrator")
|
|
156
|
+
.or_else(|| mcp_config.raw.get("team-agent"))
|
|
157
|
+
.ok_or_else(|| {
|
|
158
|
+
LifecycleError::StatePersist(
|
|
159
|
+
"cursor MCP overlay requires team_orchestrator in resolved mcp config".to_string(),
|
|
160
|
+
)
|
|
161
|
+
})?;
|
|
162
|
+
let command = server
|
|
163
|
+
.get("command")
|
|
164
|
+
.and_then(serde_json::Value::as_str)
|
|
165
|
+
.filter(|value| !value.is_empty())
|
|
166
|
+
.ok_or_else(|| {
|
|
167
|
+
LifecycleError::StatePersist("cursor MCP overlay missing command".to_string())
|
|
168
|
+
})?;
|
|
169
|
+
let args = server
|
|
170
|
+
.get("args")
|
|
171
|
+
.and_then(serde_json::Value::as_array)
|
|
172
|
+
.map(|items| {
|
|
173
|
+
items
|
|
174
|
+
.iter()
|
|
175
|
+
.filter_map(serde_json::Value::as_str)
|
|
176
|
+
.map(str::to_string)
|
|
177
|
+
.collect::<Vec<_>>()
|
|
178
|
+
})
|
|
179
|
+
.unwrap_or_default();
|
|
180
|
+
let env = server
|
|
181
|
+
.get("env")
|
|
182
|
+
.and_then(serde_json::Value::as_object)
|
|
183
|
+
.map(|map| {
|
|
184
|
+
map.iter()
|
|
185
|
+
.filter_map(|(key, value)| {
|
|
186
|
+
value.as_str().map(|text| (key.clone(), text.to_string()))
|
|
187
|
+
})
|
|
188
|
+
.collect::<BTreeMap<_, _>>()
|
|
189
|
+
})
|
|
190
|
+
.unwrap_or_default();
|
|
191
|
+
for key in REQUIRED_IDENTITY_KEYS {
|
|
192
|
+
match env.get(*key).map(String::as_str) {
|
|
193
|
+
Some(value) if !value.trim().is_empty() && !value.contains('{') => {}
|
|
194
|
+
_ => {
|
|
195
|
+
return Err(LifecycleError::StatePersist(format!(
|
|
196
|
+
"cursor MCP overlay missing resolved {key} in json env \
|
|
197
|
+
(cursor does not inherit parent TEAM_AGENT_* into the MCP child)"
|
|
198
|
+
)));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let dir = workspace.join(".cursor");
|
|
204
|
+
std::fs::create_dir_all(&dir)
|
|
205
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", dir.display())))?;
|
|
206
|
+
let path = dir.join("mcp.json");
|
|
207
|
+
let mut root = read_existing_mcp_json(&path);
|
|
208
|
+
let servers = root
|
|
209
|
+
.as_object_mut()
|
|
210
|
+
.ok_or_else(|| {
|
|
211
|
+
LifecycleError::StatePersist(format!(
|
|
212
|
+
"{}: cursor mcp.json root must be an object",
|
|
213
|
+
path.display()
|
|
214
|
+
))
|
|
215
|
+
})?
|
|
216
|
+
.entry("mcpServers")
|
|
217
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
218
|
+
let servers = servers.as_object_mut().ok_or_else(|| {
|
|
219
|
+
LifecycleError::StatePersist(format!("{}: mcpServers must be an object", path.display()))
|
|
220
|
+
})?;
|
|
221
|
+
// 0.5.67 overlay 曾误用 team-agent 作 inbound key。cursor 按 server 名
|
|
222
|
+
// 给工具加命名空间,旧名会变成另一套工具。写新表时摘掉旧名。
|
|
223
|
+
servers.remove("team-agent");
|
|
224
|
+
let mut env_json = serde_json::Map::new();
|
|
225
|
+
for (key, value) in &env {
|
|
226
|
+
env_json.insert(key.clone(), serde_json::Value::String(value.clone()));
|
|
227
|
+
}
|
|
228
|
+
servers.insert(
|
|
229
|
+
"team_orchestrator".to_string(),
|
|
230
|
+
serde_json::json!({
|
|
231
|
+
"command": command,
|
|
232
|
+
"args": args,
|
|
233
|
+
"env": env_json,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
let body = serde_json::to_string_pretty(&root)
|
|
237
|
+
.map_err(|e| LifecycleError::StatePersist(format!("serialize cursor mcp.json: {e}")))?;
|
|
238
|
+
let tmp = dir.join("mcp.json.tmp");
|
|
239
|
+
std::fs::write(&tmp, body.as_bytes())
|
|
240
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", tmp.display())))?;
|
|
241
|
+
std::fs::rename(&tmp, &path)
|
|
242
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", path.display())))?;
|
|
243
|
+
Ok(path)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// ---
|
|
247
|
+
/// purpose: 组出启用 team_orchestrator 的 cursor 命令行
|
|
248
|
+
/// returns: 不带 workspace 参数的 argv,该命令按进程工作目录分片
|
|
249
|
+
/// ---
|
|
250
|
+
pub fn cursor_mcp_enable_argv() -> Vec<String> {
|
|
251
|
+
vec![
|
|
252
|
+
command_name(Provider::CursorAgent).to_string(),
|
|
253
|
+
"mcp".to_string(),
|
|
254
|
+
"enable".to_string(),
|
|
255
|
+
"team_orchestrator".to_string(),
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// ---
|
|
260
|
+
/// purpose: 在物理工作目录下执行 cursor 的 MCP 启用命令
|
|
261
|
+
/// returns: 测试隔离环境或显式跳过标志下直接成功,避免写用户全局配置
|
|
262
|
+
/// errors: 命令跑不起来或退出码非零时返回 RequirementUnmet,错误里只记输出长度不记内容
|
|
263
|
+
/// ---
|
|
264
|
+
pub fn enable_cursor_workspace_mcp(workspace: &Path) -> Result<(), LifecycleError> {
|
|
265
|
+
if skip_cursor_mcp_enable() {
|
|
266
|
+
return Ok(());
|
|
267
|
+
}
|
|
268
|
+
let physical = physical_workspace_path(workspace);
|
|
269
|
+
let argv = cursor_mcp_enable_argv();
|
|
270
|
+
let output = Command::new(&argv[0])
|
|
271
|
+
.args(&argv[1..])
|
|
272
|
+
.current_dir(&physical)
|
|
273
|
+
.output()
|
|
274
|
+
.map_err(|e| {
|
|
275
|
+
LifecycleError::RequirementUnmet(format!(
|
|
276
|
+
"error: cannot run `{} mcp enable team_orchestrator`\n\
|
|
277
|
+
reason: {e}\n\
|
|
278
|
+
action: install cursor-agent on PATH (same binary as `agent`) and retry",
|
|
279
|
+
argv[0]
|
|
280
|
+
))
|
|
281
|
+
})?;
|
|
282
|
+
if output.status.success() {
|
|
283
|
+
return Ok(());
|
|
284
|
+
}
|
|
285
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
286
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
287
|
+
Err(LifecycleError::RequirementUnmet(format!(
|
|
288
|
+
"error: `{} mcp enable team_orchestrator` failed (exit {:?})\n\
|
|
289
|
+
reason: without enable, cursor keeps MCP as not loaded (needs approval)\n\
|
|
290
|
+
cwd: {}\n\
|
|
291
|
+
stdout_len: {}\n\
|
|
292
|
+
stderr_len: {}\n\
|
|
293
|
+
action: from that directory run `{} mcp enable team_orchestrator`",
|
|
294
|
+
argv[0],
|
|
295
|
+
output.status.code(),
|
|
296
|
+
physical.display(),
|
|
297
|
+
stdout.len(),
|
|
298
|
+
stderr.len(),
|
|
299
|
+
argv[0]
|
|
300
|
+
)))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// ---
|
|
304
|
+
/// purpose: 把 argv 里 workspace 参数的值换成物理路径
|
|
305
|
+
/// params:
|
|
306
|
+
/// argv: 就地改写;没有该参数时什么都不做
|
|
307
|
+
/// ---
|
|
308
|
+
pub fn apply_cursor_workspace_physical_path(argv: &mut [String], workspace: &Path) {
|
|
309
|
+
let physical = physical_workspace_path(workspace);
|
|
310
|
+
let Some(index) = argv.iter().position(|arg| arg == "--workspace") else {
|
|
311
|
+
return;
|
|
312
|
+
};
|
|
313
|
+
if let Some(value) = argv.get_mut(index.saturating_add(1)) {
|
|
314
|
+
*value = physical.to_string_lossy().into_owned();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn skip_cursor_mcp_enable() -> bool {
|
|
319
|
+
cfg!(test)
|
|
320
|
+
|| std::env::var_os("TEAM_AGENT_TEST_TMP").is_some()
|
|
321
|
+
|| matches!(
|
|
322
|
+
std::env::var("TEAM_AGENT_SKIP_CURSOR_MCP_ENABLE").as_deref(),
|
|
323
|
+
Ok("1") | Ok("true") | Ok("yes")
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fn read_existing_mcp_json(path: &Path) -> serde_json::Value {
|
|
328
|
+
let Ok(text) = std::fs::read_to_string(path) else {
|
|
329
|
+
return serde_json::json!({"mcpServers": {}});
|
|
330
|
+
};
|
|
331
|
+
serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!({"mcpServers": {}}))
|
|
332
|
+
}
|