@team-agent/installer 0.3.39 → 0.4.0

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.
Files changed (61) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +7 -0
  4. package/crates/team-agent/src/cli/adapters.rs +8 -3
  5. package/crates/team-agent/src/cli/diagnose.rs +12 -3
  6. package/crates/team-agent/src/cli/emit.rs +1 -0
  7. package/crates/team-agent/src/cli/mod.rs +250 -9
  8. package/crates/team-agent/src/cli/send.rs +11 -2
  9. package/crates/team-agent/src/cli/status_port.rs +39 -4
  10. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +189 -6
  11. package/crates/team-agent/src/cli/tests/status_send.rs +189 -0
  12. package/crates/team-agent/src/cli/tests/verb_settle.rs +2 -0
  13. package/crates/team-agent/src/cli/types.rs +5 -0
  14. package/crates/team-agent/src/coordinator/mod.rs +1 -0
  15. package/crates/team-agent/src/coordinator/steps/abnormal.rs +4 -0
  16. package/crates/team-agent/src/coordinator/steps/delivery.rs +4 -0
  17. package/crates/team-agent/src/coordinator/steps/health_sync.rs +4 -0
  18. package/crates/team-agent/src/coordinator/steps/mod.rs +83 -0
  19. package/crates/team-agent/src/coordinator/steps/persist.rs +4 -0
  20. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +4 -0
  21. package/crates/team-agent/src/coordinator/steps/session_gate.rs +5 -0
  22. package/crates/team-agent/src/coordinator/tests/health_sync.rs +156 -0
  23. package/crates/team-agent/src/coordinator/tick.rs +126 -30
  24. package/crates/team-agent/src/{message_store.rs → db/message_store.rs} +6 -0
  25. package/crates/team-agent/src/db/mod.rs +1 -0
  26. package/crates/team-agent/src/layout/mod.rs +7 -0
  27. package/crates/team-agent/src/layout/runtime_sessions.rs +312 -0
  28. package/crates/team-agent/src/layout/tmux_endpoint.rs +162 -0
  29. package/crates/team-agent/src/leader/start.rs +27 -1
  30. package/crates/team-agent/src/leader/tests/identity.rs +50 -0
  31. package/crates/team-agent/src/lib.rs +6 -2
  32. package/crates/team-agent/src/lifecycle/launch/readiness.rs +32 -0
  33. package/crates/team-agent/src/lifecycle/launch/spawn.rs +32 -0
  34. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +51 -0
  35. package/crates/team-agent/src/lifecycle/launch.rs +34 -0
  36. package/crates/team-agent/src/lifecycle/restart/agent.rs +17 -0
  37. package/crates/team-agent/src/lifecycle/restart/common.rs +143 -23
  38. package/crates/team-agent/src/lifecycle/restart/preflight.rs +87 -0
  39. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +308 -0
  40. package/crates/team-agent/src/lifecycle/restart/selection.rs +87 -22
  41. package/crates/team-agent/src/lifecycle/restart.rs +6 -0
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +6 -1
  43. package/crates/team-agent/src/lifecycle/tests/restart.rs +187 -0
  44. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  45. package/crates/team-agent/src/lifecycle/types.rs +20 -1
  46. package/crates/team-agent/src/mcp_server/helpers.rs +72 -1
  47. package/crates/team-agent/src/mcp_server/tools.rs +29 -4
  48. package/crates/team-agent/src/provider/adapter.rs +31 -1
  49. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +34 -0
  50. package/crates/team-agent/src/provider/classify.rs +138 -0
  51. package/crates/team-agent/src/provider/command.rs +71 -0
  52. package/crates/team-agent/src/provider/mod.rs +3 -0
  53. package/crates/team-agent/src/{session_capture.rs → provider/session/capture.rs} +139 -6
  54. package/crates/team-agent/src/provider/session/mod.rs +13 -0
  55. package/crates/team-agent/src/provider/session/resume.rs +417 -0
  56. package/crates/team-agent/src/provider/session_scan.rs +63 -0
  57. package/crates/team-agent/src/provider/types.rs +5 -0
  58. package/crates/team-agent/src/state/persist.rs +238 -0
  59. package/crates/team-agent/src/tmux_backend.rs +25 -0
  60. package/npm/install.mjs +27 -1
  61. package/package.json +4 -4
@@ -0,0 +1,63 @@
1
+ //! unit-7 (Stage 3) — provider session-scan boundary.
2
+ //!
3
+ //! Dedicated home for provider transcript/backing scanning concerns
4
+ //! (today co-located in adapter.rs around lines 511-546 and 969-1078).
5
+ //! Created as part of the unit-7 boundary establishment; full relocation
6
+ //! of the scanning fns is staged into follow-up commits so each diff
7
+ //! stays reviewable.
8
+ //!
9
+ //! See sibling [`crate::provider::session::capture`] for the existing
10
+ //! polling layer (moved under provider/session in unit-6).
11
+
12
+ /// Marker label for which scan strategy a provider uses. The Codex jsonl
13
+ /// scan and the Claude transcript scan share the same outer "decide what
14
+ /// to call" hook in adapter.rs; this enum exists to name the branch.
15
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
16
+ pub enum SessionScanStrategy {
17
+ /// Codex rollout jsonl scan (the existing
18
+ /// `agent_rollout_path`-based check).
19
+ CodexJsonl,
20
+ /// Claude transcript scan (per-cwd `.claude/projects/<cwd>/*.jsonl`).
21
+ ClaudeTranscript,
22
+ /// Copilot scan (different home; provider-specific layout).
23
+ CopilotHome,
24
+ /// Provider does not require a backing scan to decide resume — the
25
+ /// session-id alone is the authority.
26
+ NotRequired,
27
+ }
28
+
29
+ impl SessionScanStrategy {
30
+ pub fn for_provider(provider: crate::provider::Provider) -> Self {
31
+ match provider {
32
+ crate::provider::Provider::Codex => Self::CodexJsonl,
33
+ crate::provider::Provider::Claude | crate::provider::Provider::ClaudeCode => {
34
+ Self::ClaudeTranscript
35
+ }
36
+ crate::provider::Provider::Copilot => Self::CopilotHome,
37
+ crate::provider::Provider::GeminiCli => Self::NotRequired,
38
+ crate::provider::Provider::Fake => Self::NotRequired,
39
+ }
40
+ }
41
+ }
42
+
43
+ #[cfg(test)]
44
+ mod tests {
45
+ use super::*;
46
+ use crate::provider::Provider;
47
+
48
+ #[test]
49
+ fn strategy_per_provider() {
50
+ assert_eq!(
51
+ SessionScanStrategy::for_provider(Provider::Codex),
52
+ SessionScanStrategy::CodexJsonl
53
+ );
54
+ assert_eq!(
55
+ SessionScanStrategy::for_provider(Provider::Claude),
56
+ SessionScanStrategy::ClaudeTranscript
57
+ );
58
+ assert_eq!(
59
+ SessionScanStrategy::for_provider(Provider::Fake),
60
+ SessionScanStrategy::NotRequired
61
+ );
62
+ }
63
+ }
@@ -342,6 +342,11 @@ pub struct ProviderCommandContext<'a> {
342
342
  pub model: Option<&'a str>,
343
343
  pub tools: &'a [&'a str],
344
344
  pub profile_launch: Option<&'a ProviderProfileLaunch>,
345
+ /// Optional agent id, used as a launch-time display-name hint for
346
+ /// providers that accept `--name <name>` (Claude, Copilot). Codex has
347
+ /// no `--name` CLI flag (probe verdict 2026-06-22) and ignores this
348
+ /// field. None = legacy callers (the field is purely additive).
349
+ pub agent_id_hint: Option<&'a str>,
345
350
  }
346
351
 
347
352
  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -642,6 +642,72 @@ pub fn migrate_active_team_key(state: &mut Value) -> bool {
642
642
  true
643
643
  }
644
644
 
645
+ /// RM-039-STAT-001 second-round compat normalizer (architect verdict
646
+ /// 2026-06-22). When `active_team_key` names an existing `teams` entry
647
+ /// but root `team_key` is missing, set root `team_key = active_team_key`
648
+ /// (and mirror it into `teams[active_team_key].team_key` for callers
649
+ /// that read the team-scoped slot). This keeps the cascade in
650
+ /// `state::projection::team_state_key` honest: the first branch
651
+ /// (`state.team_key`) now matches what `active_team_key` says, so
652
+ /// coordinator tick's `save_team_scoped_state` writes activity to the
653
+ /// SAME teams entry the status selector later reads.
654
+ ///
655
+ /// Narrow on purpose:
656
+ /// * Returns `false` (no-op) when root `team_key` already exists,
657
+ /// even if it disagrees with `active_team_key`. Conflict cases
658
+ /// stay observable; we don't silently rewrite.
659
+ /// * Returns `false` when `active_team_key` is null/empty.
660
+ /// * Returns `false` when `teams[active_team_key]` does not exist.
661
+ /// * Does NOT touch any other field (no generic deep merge).
662
+ ///
663
+ /// Returns `true` if state was mutated and the caller should persist.
664
+ pub fn migrate_team_key_to_match_active_team(state: &mut Value) -> bool {
665
+ let Some(obj) = state.as_object() else {
666
+ return false;
667
+ };
668
+ let already_has_root = obj
669
+ .get("team_key")
670
+ .and_then(Value::as_str)
671
+ .is_some_and(|s| !s.is_empty());
672
+ if already_has_root {
673
+ return false;
674
+ }
675
+ let active = match obj.get("active_team_key").and_then(Value::as_str) {
676
+ Some(s) if !s.is_empty() => s.to_string(),
677
+ _ => return false,
678
+ };
679
+ let teams_has_active = obj
680
+ .get("teams")
681
+ .and_then(Value::as_object)
682
+ .is_some_and(|teams| teams.contains_key(&active));
683
+ if !teams_has_active {
684
+ return false;
685
+ }
686
+ let Some(obj_mut) = state.as_object_mut() else {
687
+ return false;
688
+ };
689
+ obj_mut.insert("team_key".to_string(), Value::String(active.clone()));
690
+ // Mirror inside teams[active] so any code reading the team-scoped
691
+ // slot sees the same canonical key. Skip if the entry isn't an
692
+ // object (defensive — should always be).
693
+ if let Some(team_entry) = obj_mut
694
+ .get_mut("teams")
695
+ .and_then(Value::as_object_mut)
696
+ .and_then(|teams| teams.get_mut(&active))
697
+ .and_then(Value::as_object_mut)
698
+ {
699
+ let needs_team_key = team_entry
700
+ .get("team_key")
701
+ .and_then(Value::as_str)
702
+ .map(|s| s != active)
703
+ .unwrap_or(true);
704
+ if needs_team_key {
705
+ team_entry.insert("team_key".to_string(), Value::String(active));
706
+ }
707
+ }
708
+ true
709
+ }
710
+
645
711
  pub fn load_runtime_state(workspace: &Path) -> Result<Value, StateError> {
646
712
  let path = runtime_state_path(workspace);
647
713
  if !path.exists() {
@@ -657,6 +723,13 @@ pub fn load_runtime_state(workspace: &Path) -> Result<Value, StateError> {
657
723
  if migrate_active_team_key(&mut state) {
658
724
  changed = true;
659
725
  }
726
+ // RM-039-STAT-001 second-round compat (architect verdict 2026-06-22):
727
+ // for states written before the launch-side `team_key` fix landed,
728
+ // promote `active_team_key` into root `team_key` so coordinator tick
729
+ // and status selector agree on which `teams` entry to read/write.
730
+ if migrate_team_key_to_match_active_team(&mut state) {
731
+ changed = true;
732
+ }
660
733
  if changed {
661
734
  save_runtime_state(workspace, &state)?;
662
735
  }
@@ -1037,4 +1110,169 @@ mod tests {
1037
1110
  "deleted_agent_ids 豁免:被 remove 的 `gone` 不得被 preserve 复活;saved={saved}"
1038
1111
  );
1039
1112
  }
1113
+
1114
+ /// RM-039-STAT-001 second-round regression (architect verdict
1115
+ /// 2026-06-22): `migrate_team_key_to_match_active_team` must
1116
+ /// promote `team_key = active_team_key` when root `team_key` is
1117
+ /// missing AND `teams[active_team_key]` exists, and mirror it into
1118
+ /// the team-scoped slot. Coordinator tick then writes to the same
1119
+ /// teams entry that status selector reads.
1120
+ #[test]
1121
+ fn migrate_team_key_to_match_active_team_promotes_when_missing() {
1122
+ let mut state = json!({
1123
+ "active_team_key": "rm039-status-working-891",
1124
+ "session_name": "team-rm039-status-working",
1125
+ "team_dir": "./.team/current",
1126
+ "teams": {
1127
+ "rm039-status-working-891": {
1128
+ "active_team_key": "rm039-status-working-891",
1129
+ "session_name": "team-rm039-status-working",
1130
+ "agents": {}
1131
+ }
1132
+ }
1133
+ });
1134
+ let changed = migrate_team_key_to_match_active_team(&mut state);
1135
+ assert!(changed, "state must be mutated when team_key is missing");
1136
+ assert_eq!(
1137
+ state.get("team_key").and_then(Value::as_str),
1138
+ Some("rm039-status-working-891"),
1139
+ "root team_key must equal active_team_key after migration"
1140
+ );
1141
+ // mirrored into the team-scoped entry
1142
+ assert_eq!(
1143
+ state
1144
+ .pointer("/teams/rm039-status-working-891/team_key")
1145
+ .and_then(Value::as_str),
1146
+ Some("rm039-status-working-891"),
1147
+ "teams[active].team_key must be set as part of the migration"
1148
+ );
1149
+ // team_state_key cascade now agrees with active_team_key.
1150
+ assert_eq!(
1151
+ crate::state::projection::team_state_key(&state),
1152
+ "rm039-status-working-891",
1153
+ "team_state_key first branch must hit `state.team_key` and \
1154
+ return the canonical key"
1155
+ );
1156
+ }
1157
+
1158
+ /// Conflict cases stay observable: if `team_key` already exists,
1159
+ /// the migrator does NOT silently overwrite it, even when it
1160
+ /// disagrees with `active_team_key`. The user/team can resolve.
1161
+ #[test]
1162
+ fn migrate_team_key_to_match_active_team_is_noop_when_team_key_present() {
1163
+ let mut state = json!({
1164
+ "team_key": "explicit-key",
1165
+ "active_team_key": "different-active",
1166
+ "teams": {
1167
+ "different-active": {"agents": {}},
1168
+ "explicit-key": {"agents": {}}
1169
+ }
1170
+ });
1171
+ let changed = migrate_team_key_to_match_active_team(&mut state);
1172
+ assert!(!changed, "existing team_key must NOT be overwritten");
1173
+ assert_eq!(
1174
+ state.get("team_key").and_then(Value::as_str),
1175
+ Some("explicit-key")
1176
+ );
1177
+ }
1178
+
1179
+ /// Narrow: only promote when teams[active_team_key] actually exists.
1180
+ /// Otherwise we'd be claiming a key for a non-existent team slot.
1181
+ #[test]
1182
+ fn migrate_team_key_to_match_active_team_is_noop_when_active_team_entry_missing() {
1183
+ let mut state = json!({
1184
+ "active_team_key": "missing-team",
1185
+ "teams": {
1186
+ "other": {"agents": {}}
1187
+ }
1188
+ });
1189
+ let changed = migrate_team_key_to_match_active_team(&mut state);
1190
+ assert!(
1191
+ !changed,
1192
+ "no migration when teams[active_team_key] does not exist"
1193
+ );
1194
+ assert!(state.get("team_key").is_none());
1195
+ }
1196
+
1197
+ /// Null / empty active_team_key is not actionable.
1198
+ #[test]
1199
+ fn migrate_team_key_to_match_active_team_is_noop_when_active_team_key_absent() {
1200
+ for active in [json!(null), json!(""), json!(serde_json::Value::Null)] {
1201
+ let mut state = json!({
1202
+ "active_team_key": active,
1203
+ "teams": {}
1204
+ });
1205
+ assert!(
1206
+ !migrate_team_key_to_match_active_team(&mut state),
1207
+ "no migration when active_team_key is null/empty"
1208
+ );
1209
+ }
1210
+ }
1211
+
1212
+ /// RM-039-STAT-001 second-round end-to-end at the persistence layer
1213
+ /// (architect verdict 2026-06-22): loading a state file in the dirty
1214
+ /// shape (active_team_key disagrees with team_dir basename, root
1215
+ /// team_key absent) must persist the canonical `team_key` so the
1216
+ /// next `save_team_scoped_state` writes the team-scoped slot at
1217
+ /// `teams[active_team_key]`, not at `teams[team_dir_basename]`.
1218
+ /// This is the bridging contract that decides whether coordinator
1219
+ /// tick's activity write lands where the status selector later reads.
1220
+ #[test]
1221
+ fn load_runtime_state_pins_team_key_from_active_team_key_on_dirty_shape() {
1222
+ let ws = temp_ws();
1223
+ let active = "rm039-status-working-891";
1224
+ let raw = json!({
1225
+ "session_name": "team-rm039-status-working",
1226
+ "team_dir": "./.team/current",
1227
+ "active_team_key": active,
1228
+ // intentionally NO root "team_key" — the dirty shape.
1229
+ "agents": {
1230
+ "coder": {"status": "running", "first_send_at": "2026-01-01T00:00:00Z"}
1231
+ },
1232
+ "teams": {
1233
+ "current": {
1234
+ "active_team_key": active,
1235
+ "agents": {"coder": {"status": "running"}}
1236
+ },
1237
+ active: {
1238
+ "active_team_key": active,
1239
+ "agents": {"coder": {"status": "running"}}
1240
+ }
1241
+ }
1242
+ });
1243
+ let runtime_dir = ws.join(".team").join("runtime");
1244
+ std::fs::create_dir_all(&runtime_dir).unwrap();
1245
+ std::fs::write(
1246
+ runtime_dir.join("state.json"),
1247
+ serde_json::to_vec_pretty(&raw).unwrap(),
1248
+ )
1249
+ .unwrap();
1250
+ // temp_ws() returns a unique path per call, so the cache
1251
+ // (keyed by path) does not collide with previous tests.
1252
+
1253
+ let loaded = load_runtime_state(&ws).expect("loaded state");
1254
+ assert_eq!(
1255
+ loaded.get("team_key").and_then(Value::as_str),
1256
+ Some(active),
1257
+ "load_runtime_state must promote team_key=active_team_key on the dirty shape"
1258
+ );
1259
+ // team_state_key now cascades to state.team_key, returning the
1260
+ // canonical key instead of the team_dir basename.
1261
+ assert_eq!(
1262
+ crate::state::projection::team_state_key(&loaded),
1263
+ active,
1264
+ "team_state_key must return the canonical key after migration"
1265
+ );
1266
+
1267
+ // Re-read from disk (not cache) to confirm the migration was persisted.
1268
+ let on_disk: Value = serde_json::from_str(
1269
+ &std::fs::read_to_string(runtime_dir.join("state.json")).unwrap(),
1270
+ )
1271
+ .unwrap();
1272
+ assert_eq!(
1273
+ on_disk.get("team_key").and_then(Value::as_str),
1274
+ Some(active),
1275
+ "migration must be persisted to disk so subsequent processes see it"
1276
+ );
1277
+ }
1040
1278
  }
@@ -502,6 +502,31 @@ pub(crate) fn attach_command_for_workspace(
502
502
  ))
503
503
  }
504
504
 
505
+ /// Bug #7 (prerelease 0.4.0 gate review §6): when the runtime state carries a
506
+ /// persisted `tmux_endpoint` / `tmux_socket` (e.g. `/private/tmp/tmux-501/default`),
507
+ /// the attach command MUST point at THAT endpoint, not the workspace-hash
508
+ /// socket — otherwise operators are told to attach to a socket where the
509
+ /// session does not exist. Falls back to workspace-hash when state has no
510
+ /// persisted endpoint.
511
+ pub(crate) fn attach_command_for_runtime_state_or_workspace(
512
+ workspace: &Path,
513
+ state: Option<&serde_json::Value>,
514
+ session_name: &SessionName,
515
+ window_name: &str,
516
+ ) -> Option<String> {
517
+ if let Some((endpoint, _source)) = runtime_tmux_endpoint_from_state(state) {
518
+ let display = endpoint.to_string();
519
+ // Distinguish absolute path (`-S <path>`) from short socket name (`-L <name>`).
520
+ let flag = if Path::new(endpoint).is_absolute() { "-S" } else { "-L" };
521
+ return Some(format!(
522
+ "tmux {flag} {display} attach -t {}:{}",
523
+ session_name.as_str(),
524
+ window_name
525
+ ));
526
+ }
527
+ attach_command_for_workspace(workspace, session_name, window_name)
528
+ }
529
+
505
530
  pub(crate) fn attach_commands_for_windows<'a>(
506
531
  workspace: &Path,
507
532
  session_name: &SessionName,
package/npm/install.mjs CHANGED
@@ -139,7 +139,11 @@ function install(argv) {
139
139
  fs.mkdirSync(runtimeRoot, { recursive: true });
140
140
  fs.rmSync(tmp, { recursive: true, force: true });
141
141
  fs.mkdirSync(path.join(tmp, "bin"), { recursive: true });
142
- copyExecutable(platformBinary.path, path.join(tmp, "bin", "team-agent"));
142
+ const tmpBinary = path.join(tmp, "bin", "team-agent");
143
+ copyExecutable(platformBinary.path, tmpBinary);
144
+ // Sign the staged binary before swap. Failure aborts install before any
145
+ // rename touches the existing runtime directory.
146
+ prepareDarwinExecutable(tmpBinary);
143
147
 
144
148
  fs.rmSync(backup, { recursive: true, force: true });
145
149
  if (fs.existsSync(dest)) {
@@ -668,6 +672,28 @@ function copyExecutable(src, dest) {
668
672
  fs.chmodSync(dest, 0o755);
669
673
  }
670
674
 
675
+ // Defence-in-depth: on Darwin, ad-hoc re-sign the staged binary before the
676
+ // atomic rename swaps it into place. Atomic temp+rename already avoids the
677
+ // classic in-place vnode/signature-cache failure, but it does not repair an
678
+ // invalid or stale Mach-O signature that survived the npm extraction. The
679
+ // ad-hoc signature (-) is sufficient for local execution and avoids requiring
680
+ // a Developer ID. Fail closed before runtime swap if codesign exits non-zero.
681
+ function prepareDarwinExecutable(dest) {
682
+ if (process.platform !== "darwin") {
683
+ return;
684
+ }
685
+ const res = spawnSync("/usr/bin/codesign", ["--force", "--sign", "-", dest], {
686
+ encoding: "utf8",
687
+ });
688
+ if (res.status !== 0) {
689
+ const stderr = (res.stderr || "").trim();
690
+ const signal = res.signal ? ` signal=${res.signal}` : "";
691
+ throw new Error(
692
+ `codesign failed on ${dest}: status=${res.status}${signal} stderr=${stderr || "(empty)"}`,
693
+ );
694
+ }
695
+ }
696
+
671
697
  function writeExecWrapper(file, binary, fixedArgs, options = {}) {
672
698
  if (fs.existsSync(file) && !options.allowForeign && !isInstallerManagedWrapper(file)) {
673
699
  throw new Error(`refusing to overwrite non-Team Agent installer wrapper: ${file}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.39",
3
+ "version": "0.4.0",
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.3.39",
24
- "@team-agent/cli-darwin-x64": "0.3.39",
25
- "@team-agent/cli-linux-x64": "0.3.39"
23
+ "@team-agent/cli-darwin-arm64": "0.4.0",
24
+ "@team-agent/cli-darwin-x64": "0.4.0",
25
+ "@team-agent/cli-linux-x64": "0.4.0"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",