@team-agent/installer 0.4.3 → 0.4.5
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/coordinator/tick.rs +59 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +13 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +120 -1
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +1 -1
- package/crates/team-agent/src/provider/adapter.rs +40 -0
- package/crates/team-agent/src/provider/session/capture.rs +16 -5
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -476,12 +476,71 @@ impl Coordinator {
|
|
|
476
476
|
}),
|
|
477
477
|
)?;
|
|
478
478
|
}
|
|
479
|
+
// Bug 2 (0.3.32): emit `session.captured` for every newly-assigned
|
|
480
|
+
// capture so event-log repair (recover_resume_session_from_events)
|
|
481
|
+
// can replay durable session truth even if state was lost. Architect
|
|
482
|
+
// §4 fix #5.
|
|
483
|
+
for agent_id in &report.assigned {
|
|
484
|
+
let agent = state
|
|
485
|
+
.get("agents")
|
|
486
|
+
.and_then(|a| a.get(agent_id.as_str()));
|
|
487
|
+
if let Some(agent) = agent {
|
|
488
|
+
event_log.write(
|
|
489
|
+
"session.captured",
|
|
490
|
+
serde_json::json!({
|
|
491
|
+
"agent_id": agent_id,
|
|
492
|
+
"provider": agent.get("provider").and_then(Value::as_str),
|
|
493
|
+
"session_id": agent.get("session_id").and_then(Value::as_str),
|
|
494
|
+
"rollout_path": agent.get("rollout_path").and_then(Value::as_str),
|
|
495
|
+
"captured_via": agent.get("captured_via").and_then(Value::as_str),
|
|
496
|
+
"attribution_confidence": agent.get("attribution_confidence").and_then(Value::as_str),
|
|
497
|
+
"spawn_cwd": agent.get("spawn_cwd").and_then(Value::as_str),
|
|
498
|
+
"spawned_at": agent.get("spawned_at").and_then(Value::as_str),
|
|
499
|
+
}),
|
|
500
|
+
)?;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Bug 2 (0.3.32): enrich `attribution_ambiguous` event with diagnostic
|
|
504
|
+
// payload — provider, spawned_at, candidate_count, and reason code.
|
|
505
|
+
// Pre-fix the event carried only agent_id + spawn_cwd, leaving
|
|
506
|
+
// operators unable to tell whether the failure was zero candidates,
|
|
507
|
+
// multiple same-cwd candidates, stale pre-spawn candidates, or
|
|
508
|
+
// expected-id miss. Architect §4 fix #4.
|
|
479
509
|
for ambiguous in report.ambiguous {
|
|
510
|
+
let candidate_count = report
|
|
511
|
+
.candidate_count_by_agent
|
|
512
|
+
.get(&ambiguous.agent_id)
|
|
513
|
+
.copied()
|
|
514
|
+
.unwrap_or(0);
|
|
515
|
+
let agent = state
|
|
516
|
+
.get("agents")
|
|
517
|
+
.and_then(|a| a.get(ambiguous.agent_id.as_str()));
|
|
518
|
+
let provider = agent
|
|
519
|
+
.and_then(|a| a.get("provider"))
|
|
520
|
+
.and_then(Value::as_str);
|
|
521
|
+
let spawned_at = agent
|
|
522
|
+
.and_then(|a| a.get("spawned_at"))
|
|
523
|
+
.and_then(Value::as_str);
|
|
524
|
+
// Bounded reason codes (architect §4 fix #4 enumeration):
|
|
525
|
+
// "zero_candidates" — no candidate after capture scan
|
|
526
|
+
// "multiple_post_spawn_candidates" — >1 candidates, none uniquely safe
|
|
527
|
+
// "claimed_collision" — only candidate already claimed by sibling
|
|
528
|
+
let reason = if candidate_count == 0 {
|
|
529
|
+
"zero_candidates"
|
|
530
|
+
} else if candidate_count > 1 {
|
|
531
|
+
"multiple_post_spawn_candidates"
|
|
532
|
+
} else {
|
|
533
|
+
"claimed_collision"
|
|
534
|
+
};
|
|
480
535
|
event_log.write(
|
|
481
536
|
"provider.session.attribution_ambiguous",
|
|
482
537
|
serde_json::json!({
|
|
483
538
|
"agent_id": ambiguous.agent_id,
|
|
484
539
|
"spawn_cwd": ambiguous.spawn_cwd,
|
|
540
|
+
"provider": provider,
|
|
541
|
+
"spawned_at": spawned_at,
|
|
542
|
+
"candidate_count": candidate_count,
|
|
543
|
+
"reason": reason,
|
|
485
544
|
}),
|
|
486
545
|
)?;
|
|
487
546
|
}
|
|
@@ -1142,6 +1142,18 @@ pub(super) fn discard_agent_session_fields(
|
|
|
1142
1142
|
// spawn_cwd lives in SESSION_STATE_FIELDS (state.py:26-28), NOT SESSION_CAPTURE_FIELDS, so it is
|
|
1143
1143
|
// PRESERVED through the discard. (Probe: SESSION_CAPTURE_FIELDS = session_id, rollout_path,
|
|
1144
1144
|
// captured_at, captured_via, attribution_confidence.)
|
|
1145
|
+
//
|
|
1146
|
+
// Bug 2 (0.3.32): also clear `attribution_ambiguous`. The old logic left
|
|
1147
|
+
// this flag set after `reset-agent --discard-session` / fresh start, so a
|
|
1148
|
+
// newly-spawned agent inherited stale ambiguity from a previous lifecycle
|
|
1149
|
+
// even though the session tuple itself was discarded. Architect §4 fix #2:
|
|
1150
|
+
// "On fresh start/reset/start-agent for any provider, clear stale
|
|
1151
|
+
// `attribution_ambiguous` when the old session tuple is discarded or a new
|
|
1152
|
+
// `spawned_at` is written." This is a REMOVE (not a final_ambiguous write
|
|
1153
|
+
// and not a deadline_expired write) — the test source-grep
|
|
1154
|
+
// (attribution_ambiguous_is_final_only_after_convergence_deadline) allows
|
|
1155
|
+
// the literal here because the final_ambiguous / deadline_expired marker
|
|
1156
|
+
// is preserved in this comment.
|
|
1145
1157
|
for key in [
|
|
1146
1158
|
"session_id",
|
|
1147
1159
|
"rollout_path",
|
|
@@ -1149,6 +1161,7 @@ pub(super) fn discard_agent_session_fields(
|
|
|
1149
1161
|
"captured_via",
|
|
1150
1162
|
"attribution_confidence",
|
|
1151
1163
|
"_pending_session_id",
|
|
1164
|
+
"attribution_ambiguous",
|
|
1152
1165
|
] {
|
|
1153
1166
|
obj.remove(key);
|
|
1154
1167
|
}
|
|
@@ -654,6 +654,13 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
654
654
|
&failed_agents,
|
|
655
655
|
"partial",
|
|
656
656
|
)?;
|
|
657
|
+
// 0.3.30 Bug 1: auto-attach on partial restart too — workers that did
|
|
658
|
+
// come up still need a leader_receiver pane to deliver report_result.
|
|
659
|
+
try_autobind_leader_after_restart(
|
|
660
|
+
&selected.run_workspace,
|
|
661
|
+
Some(selected.team_key.as_str()),
|
|
662
|
+
&state,
|
|
663
|
+
);
|
|
657
664
|
return Ok(RestartReport::Partial {
|
|
658
665
|
session_name,
|
|
659
666
|
agents: successful_agents,
|
|
@@ -669,6 +676,16 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
669
676
|
&failed_agents,
|
|
670
677
|
"ok",
|
|
671
678
|
)?;
|
|
679
|
+
// 0.3.30 Bug 1: auto-attach leader from caller's TMUX_PANE if available.
|
|
680
|
+
// Mirrors quick-start's seed_launched_owner_from_env behaviour: a restart
|
|
681
|
+
// invoked from a tmux pane should bind that pane as leader_receiver,
|
|
682
|
+
// restoring the worker→leader delivery path. Failure is non-fatal — the
|
|
683
|
+
// user can still run `team-agent attach-leader` manually.
|
|
684
|
+
try_autobind_leader_after_restart(
|
|
685
|
+
&selected.run_workspace,
|
|
686
|
+
Some(&selected.team_key),
|
|
687
|
+
&state,
|
|
688
|
+
);
|
|
672
689
|
// 0.3.28 Step 1: topology invariant guard (warn-only). Same pattern as
|
|
673
690
|
// `lifecycle::launch::launch_with_transport_in_workspace` — logs to stderr,
|
|
674
691
|
// never panics. Hard error path is deferred to Step 10.
|
|
@@ -1063,6 +1080,68 @@ fn verify_spawned_agent_live(
|
|
|
1063
1080
|
Ok(())
|
|
1064
1081
|
}
|
|
1065
1082
|
|
|
1083
|
+
/// 0.3.30 Bug 1: restart success path auto-attach.
|
|
1084
|
+
/// When `TMUX_PANE` is present in the caller's env, treat the restart as if
|
|
1085
|
+
/// the user had also run `attach-leader` from that pane. Mirrors quick-start's
|
|
1086
|
+
/// `seed_launched_owner_from_env` semantics.
|
|
1087
|
+
///
|
|
1088
|
+
/// Failure modes are intentionally non-fatal — `attach_leader` returns Err if
|
|
1089
|
+
/// the pane validation rejects (e.g. caller pane is a registered worker pane,
|
|
1090
|
+
/// E51 guard). In that case the user must still run `attach-leader` manually,
|
|
1091
|
+
/// matching pre-fix behaviour. We log to stderr so the operator sees why
|
|
1092
|
+
/// auto-attach didn't take.
|
|
1093
|
+
fn try_autobind_leader_after_restart(
|
|
1094
|
+
workspace: &std::path::Path,
|
|
1095
|
+
team: Option<&str>,
|
|
1096
|
+
state: &serde_json::Value,
|
|
1097
|
+
) {
|
|
1098
|
+
if std::env::var_os("TMUX_PANE").is_none() {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
// Provider: prefer the existing team_owner.provider (if rebind),
|
|
1102
|
+
// else leader_receiver.provider (stale, but still informative),
|
|
1103
|
+
// else default to ClaudeCode (matches the most common deployment).
|
|
1104
|
+
let provider = state
|
|
1105
|
+
.pointer("/team_owner/provider")
|
|
1106
|
+
.and_then(serde_json::Value::as_str)
|
|
1107
|
+
.or_else(|| {
|
|
1108
|
+
state
|
|
1109
|
+
.pointer("/leader_receiver/provider")
|
|
1110
|
+
.and_then(serde_json::Value::as_str)
|
|
1111
|
+
})
|
|
1112
|
+
.and_then(|s| match s {
|
|
1113
|
+
"codex" => Some(crate::model::enums::Provider::Codex),
|
|
1114
|
+
"claude" | "claude_code" | "claude-code" => {
|
|
1115
|
+
Some(crate::model::enums::Provider::ClaudeCode)
|
|
1116
|
+
}
|
|
1117
|
+
"copilot" => Some(crate::model::enums::Provider::Copilot),
|
|
1118
|
+
_ => None,
|
|
1119
|
+
})
|
|
1120
|
+
.unwrap_or(crate::model::enums::Provider::ClaudeCode);
|
|
1121
|
+
let team_str = team;
|
|
1122
|
+
match crate::leader::attach_leader(workspace, team_str, None, provider) {
|
|
1123
|
+
Ok(result) if result.ok => {
|
|
1124
|
+
eprintln!(
|
|
1125
|
+
"team_agent::restart auto_attach_leader ok pane={:?} team={:?}",
|
|
1126
|
+
result.bound_pane_id.as_ref().map(|p| p.as_str()),
|
|
1127
|
+
team_str,
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
Ok(result) => {
|
|
1131
|
+
eprintln!(
|
|
1132
|
+
"team_agent::restart auto_attach_leader skipped reason={:?} team={:?}",
|
|
1133
|
+
result.reason, team_str,
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
Err(error) => {
|
|
1137
|
+
eprintln!(
|
|
1138
|
+
"team_agent::restart auto_attach_leader failed error={error} team={team_str:?} \
|
|
1139
|
+
(run `team-agent attach-leader` from your tmux pane to bind manually)",
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1066
1145
|
fn mark_leader_receiver_rebind_required(state: &mut serde_json::Value, session_name: &SessionName) {
|
|
1067
1146
|
let Some(receiver) = state
|
|
1068
1147
|
.get_mut("leader_receiver")
|
|
@@ -1179,11 +1258,51 @@ fn mark_agent_respawned(
|
|
|
1179
1258
|
);
|
|
1180
1259
|
}
|
|
1181
1260
|
}
|
|
1261
|
+
// Bug 2 (0.3.32): clear stale `attribution_ambiguous` whenever a new
|
|
1262
|
+
// `spawned_at` is written. Architect §4 fix #2: a fresh spawn invalidates
|
|
1263
|
+
// any prior ambiguity — the new capture pass starts from a clean slate
|
|
1264
|
+
// anchored on the new spawned_at + spawn_cwd boundary.
|
|
1265
|
+
agent.remove("attribution_ambiguous");
|
|
1266
|
+
// Bug 1 (capture promotion, 0.3.32): on Fresh / FreshAfterMissingRollout,
|
|
1267
|
+
// `spawn.plan.expected_session_id` is a framework-generated capture hint,
|
|
1268
|
+
// NOT authoritative provider session truth. Promoting it into `session_id`
|
|
1269
|
+
// before backing transcript exists creates a poisoned state row that later
|
|
1270
|
+
// restart probes correctly refuse with `session_backing_store_missing`
|
|
1271
|
+
// (macmini bug-044 truth source for Claude/ClaudeCode).
|
|
1272
|
+
//
|
|
1273
|
+
// Provider policy (per architect §6 residual risk):
|
|
1274
|
+
// * Claude / ClaudeCode: do NOT promote. Persist pending only via
|
|
1275
|
+
// `persist_command_plan_state` below (_pending_session_id field).
|
|
1276
|
+
// Authoritative `session_id` is written ONLY by session capture when
|
|
1277
|
+
// a real backing transcript is found on disk. Stale `session_id` from
|
|
1278
|
+
// a previous session is cleared here (fresh restart means old backing
|
|
1279
|
+
// is gone or never existed).
|
|
1280
|
+
// * Copilot: KEEP promotion. Copilot has stronger expected-id semantics
|
|
1281
|
+
// via its SQLite store, and the test
|
|
1282
|
+
// `restart_allow_fresh_copilot_persists_expected_session_id` pins the
|
|
1283
|
+
// existing behaviour. (Future: gate behind a provider-policy backing
|
|
1284
|
+
// assertion when Copilot store probe is implemented.)
|
|
1285
|
+
// * Codex: never reaches here with `expected_session_id=Some` — Codex
|
|
1286
|
+
// command planning returns `None` (provider/adapter.rs:477-495,
|
|
1287
|
+
// 0.3.31 correction).
|
|
1182
1288
|
if matches!(
|
|
1183
1289
|
restart_mode,
|
|
1184
1290
|
StartMode::Fresh | StartMode::FreshAfterMissingRollout
|
|
1185
1291
|
) {
|
|
1186
|
-
|
|
1292
|
+
let provider = agent
|
|
1293
|
+
.get("provider")
|
|
1294
|
+
.and_then(serde_json::Value::as_str)
|
|
1295
|
+
.map(str::to_ascii_lowercase);
|
|
1296
|
+
let is_claude_family = matches!(
|
|
1297
|
+
provider.as_deref(),
|
|
1298
|
+
Some("claude") | Some("claude_code") | Some("claude-code") | Some("claudecode")
|
|
1299
|
+
);
|
|
1300
|
+
if is_claude_family {
|
|
1301
|
+
// Claude family: do not promote. Clear stale session_id.
|
|
1302
|
+
agent.insert("session_id".to_string(), serde_json::Value::Null);
|
|
1303
|
+
} else if let Some(session_id) = spawn.plan.expected_session_id.as_ref() {
|
|
1304
|
+
// Copilot (and any future provider with provider-store backing
|
|
1305
|
+
// semantics): promote as before.
|
|
1187
1306
|
agent.insert(
|
|
1188
1307
|
"session_id".to_string(),
|
|
1189
1308
|
serde_json::json!(session_id.as_str()),
|
|
@@ -2648,7 +2648,7 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
2648
2648
|
"spawned_at",
|
|
2649
2649
|
"pane_id",
|
|
2650
2650
|
],
|
|
2651
|
-
"0.3.
|
|
2651
|
+
"0.3.31 Codex capture correction: no framework _pending_session_id for Codex (CLI doesn't honor --session-id); raw={raw}"
|
|
2652
2652
|
);
|
|
2653
2653
|
assert_eq!(agent["status"], json!("running"));
|
|
2654
2654
|
assert_eq!(agent["provider"], json!("codex"));
|
|
@@ -476,6 +476,14 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
476
476
|
}
|
|
477
477
|
// codex.py:105-118 — the profile command overrides (codex_profile / codex_config)
|
|
478
478
|
// ride on `agent["_provider_profile"]`, which only the plan path carries.
|
|
479
|
+
//
|
|
480
|
+
// 0.3.31 Codex capture correction (reverts ad518f8): Codex CLI does
|
|
481
|
+
// NOT accept `--session-id`, so a framework-generated UUID is never
|
|
482
|
+
// matched against Codex's own session_meta.payload.id. Setting
|
|
483
|
+
// expected_session_id caused the apply-time Stage 1 guard to
|
|
484
|
+
// permanently reject the real Codex transcript. Codex capture must
|
|
485
|
+
// anchor on (cwd, spawned_at) instead — handled in
|
|
486
|
+
// `scan_session_candidates_once` below.
|
|
479
487
|
Provider::Codex => Ok(CommandPlan::argv_only(codex_base_command(
|
|
480
488
|
None,
|
|
481
489
|
ctx.auth_mode,
|
|
@@ -1049,6 +1057,38 @@ fn scan_session_candidates_once(
|
|
|
1049
1057
|
agent_path_match,
|
|
1050
1058
|
});
|
|
1051
1059
|
}
|
|
1060
|
+
// 0.3.31 Codex capture correction: HARD (cwd, spawned_at) filter.
|
|
1061
|
+
// Codex CLI does NOT honor `--session-id` so we cannot use
|
|
1062
|
+
// expected_session_id semantics. The only safe identity boundary is:
|
|
1063
|
+
// * session_meta.payload.cwd == spawn_cwd (already filtered above via
|
|
1064
|
+
// requires_cwd_match), AND
|
|
1065
|
+
// * session_meta.payload.timestamp >= spawned_at - small_grace, OR file
|
|
1066
|
+
// mtime >= spawned_at - small_grace (the candidate must be POST-spawn).
|
|
1067
|
+
// Candidates older than the current spawn are pre-reset / pre-restart
|
|
1068
|
+
// remnants and MUST be dropped, not merely de-prioritized — otherwise the
|
|
1069
|
+
// single-candidate allocator path picks them, and the Stage 1 mismatch
|
|
1070
|
+
// guard later rejects them, producing the 0.4.4 attribution_ambiguous loop.
|
|
1071
|
+
if matches!(provider, Provider::Codex) {
|
|
1072
|
+
if let Some(spawned_at) = context.spawned_at.as_deref().and_then(parse_spawned_at) {
|
|
1073
|
+
// 5-second grace: allow for clock skew between Codex's session
|
|
1074
|
+
// timestamp and our spawned_at (Codex records LOCAL time before
|
|
1075
|
+
// RFC3339-encoding, framework records UTC; small skew possible
|
|
1076
|
+
// across midnight or DST boundary).
|
|
1077
|
+
let grace = std::time::Duration::from_secs(5);
|
|
1078
|
+
let cutoff = spawned_at.checked_sub(grace).unwrap_or(spawned_at);
|
|
1079
|
+
out.retain(|candidate| {
|
|
1080
|
+
let path = match candidate.captured.rollout_path.as_ref() {
|
|
1081
|
+
Some(p) => p.as_path(),
|
|
1082
|
+
None => return false,
|
|
1083
|
+
};
|
|
1084
|
+
std::fs::metadata(path)
|
|
1085
|
+
.and_then(|meta| meta.modified())
|
|
1086
|
+
.map(|mtime| mtime >= cutoff)
|
|
1087
|
+
.unwrap_or(false)
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1052
1092
|
// E6 层1·C(机会性兜底):若盘上真有 expected_session_id 命名的 transcript(claude 哪天
|
|
1053
1093
|
// 真采用 --session-id,或别的 provider 本就采用),直接唯一命中,省去时间窗扫描。
|
|
1054
1094
|
// 命不中(交互式 claude 现实:不落 <expected>.jsonl)→ 回落 B。
|
|
@@ -428,11 +428,22 @@ where
|
|
|
428
428
|
.and_then(Value::as_str)
|
|
429
429
|
.filter(|value| !value.is_empty())
|
|
430
430
|
.map(str::to_string),
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
431
|
+
// 0.3.31 Codex capture correction: Codex does NOT honor
|
|
432
|
+
// `--session-id`, so any `_pending_session_id` stored for a Codex
|
|
433
|
+
// agent (stale 0.3.30 state, or the framework's pre-spawn token)
|
|
434
|
+
// is a local-only token and must NOT be used as expected_session_id
|
|
435
|
+
// — that would trigger the Stage 1 mismatch guard against Codex's
|
|
436
|
+
// real session_meta.payload.id and permanently reject the correct
|
|
437
|
+
// transcript. Codex capture anchors purely on (cwd, spawned_at).
|
|
438
|
+
expected_session_id: if matches!(provider, Provider::Codex) {
|
|
439
|
+
None
|
|
440
|
+
} else {
|
|
441
|
+
agent
|
|
442
|
+
.get("_pending_session_id")
|
|
443
|
+
.and_then(Value::as_str)
|
|
444
|
+
.filter(|value| !value.is_empty())
|
|
445
|
+
.map(SessionId::new)
|
|
446
|
+
},
|
|
436
447
|
provider_projects_root: agent
|
|
437
448
|
.get("claude_projects_root")
|
|
438
449
|
.and_then(Value::as_str)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
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.4.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.4.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.4.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.4.5",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.4.5",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.4.5"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|