reallink-cli 0.1.10 → 0.1.11

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/README.md CHANGED
@@ -36,6 +36,9 @@ reallink token revoke --token-id tok_xxx
36
36
 
37
37
  reallink file list --project-id prj_xxx
38
38
  reallink file get --asset-id ast_xxx
39
+ reallink file stat --asset-id ast_xxx
40
+ reallink file download --asset-id ast_xxx --output ./downloads/report.json
41
+ reallink file download --asset-id ast_xxx --output ./downloads/report.json --resume
39
42
  reallink file upload --project-id prj_xxx --source ./local/report.json --path reports/daily
40
43
  reallink file mkdir --project-id prj_xxx --path reports/archive
41
44
  reallink file move --asset-id ast_xxx --file-name reports/archive/report.json
@@ -62,4 +65,5 @@ reallink tool get-run --run-id trun_xxx
62
65
 
63
66
  ## Requirements
64
67
 
65
- - Rust toolchain (`cargo`) must be installed.
68
+ - Prebuilt platforms: no Rust toolchain required.
69
+ - Fallback build path (when no prebuilt binary exists): Rust toolchain (`cargo`) required.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reallink-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Rust-based CLI for Reallink auth and API operations",
5
5
  "bin": {
6
6
  "reallink": "bin/reallink.cjs"
Binary file
package/rust/Cargo.lock CHANGED
@@ -910,7 +910,7 @@ dependencies = [
910
910
 
911
911
  [[package]]
912
912
  name = "reallink-cli"
913
- version = "0.1.10"
913
+ version = "0.1.11"
914
914
  dependencies = [
915
915
  "anyhow",
916
916
  "clap",
package/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "reallink-cli"
3
- version = "0.1.10"
3
+ version = "0.1.11"
4
4
  edition = "2021"
5
5
  description = "CLI for Reallink auth and token workflows"
6
6
  license = "MIT"
@@ -13,5 +13,5 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus
13
13
  serde = { version = "1.0", features = ["derive"] }
14
14
  serde_json = "1.0"
15
15
  json5 = "0.4"
16
- tokio = { version = "1.42", features = ["macros", "rt-multi-thread", "time"] }
16
+ tokio = { version = "1.42", features = ["macros", "rt-multi-thread", "time", "fs", "io-util"] }
17
17
  webbrowser = "1.0"
package/rust/src/main.rs CHANGED
@@ -7,6 +7,8 @@ use std::io::{self, Write};
7
7
  use std::path::{Path, PathBuf};
8
8
  use std::process::Command;
9
9
  use std::time::{Duration, SystemTime, UNIX_EPOCH};
10
+ use tokio::fs as tokio_fs;
11
+ use tokio::io::AsyncWriteExt;
10
12
  use tokio::time::sleep;
11
13
 
12
14
  const DEFAULT_BASE_URL: &str = "https://api.real-agent.link";
@@ -18,7 +20,12 @@ const VERSION_CHECK_INTERVAL_MS: u128 = 24 * 60 * 60 * 1000;
18
20
  const DEFAULT_VERSION_FETCH_TIMEOUT_MS: u64 = 800;
19
21
 
20
22
  #[derive(Parser)]
21
- #[command(name = "reallink", bin_name = "reallink", version, about = "Reallink CLI")]
23
+ #[command(
24
+ name = "reallink",
25
+ bin_name = "reallink",
26
+ version,
27
+ about = "Reallink CLI"
28
+ )]
22
29
  struct Cli {
23
30
  #[command(subcommand)]
24
31
  command: Commands,
@@ -92,6 +99,8 @@ enum ProjectCommands {
92
99
  enum FileCommands {
93
100
  List(FileListArgs),
94
101
  Get(FileGetArgs),
102
+ Stat(FileStatArgs),
103
+ Download(FileDownloadArgs),
95
104
  Upload(FileUploadArgs),
96
105
  Mkdir(FileMkdirArgs),
97
106
  Move(FileMoveArgs),
@@ -151,6 +160,29 @@ struct FileGetArgs {
151
160
  base_url: Option<String>,
152
161
  }
153
162
 
163
+ #[derive(Args)]
164
+ struct FileStatArgs {
165
+ #[arg(long)]
166
+ asset_id: String,
167
+ #[arg(long)]
168
+ base_url: Option<String>,
169
+ }
170
+
171
+ #[derive(Args)]
172
+ struct FileDownloadArgs {
173
+ #[arg(long)]
174
+ asset_id: String,
175
+ #[arg(long)]
176
+ output: Option<PathBuf>,
177
+ #[arg(
178
+ long,
179
+ help = "Resume download from existing output file size using HTTP Range"
180
+ )]
181
+ resume: bool,
182
+ #[arg(long)]
183
+ base_url: Option<String>,
184
+ }
185
+
154
186
  #[derive(Args)]
155
187
  struct FileUploadArgs {
156
188
  #[arg(long)]
@@ -508,6 +540,20 @@ struct AssetResponse {
508
540
  asset: AssetRecord,
509
541
  }
510
542
 
543
+ #[derive(Debug, Serialize, Deserialize)]
544
+ #[serde(rename_all = "camelCase")]
545
+ struct AssetContentMetadata {
546
+ part_count: i64,
547
+ content_length: i64,
548
+ accept_ranges: String,
549
+ }
550
+
551
+ #[derive(Debug, Serialize, Deserialize)]
552
+ struct AssetMetadataResponse {
553
+ asset: AssetRecord,
554
+ content: AssetContentMetadata,
555
+ }
556
+
511
557
  #[derive(Debug, Serialize)]
512
558
  #[serde(rename_all = "camelCase")]
513
559
  struct UploadIntentRequest {
@@ -598,7 +644,10 @@ fn load_jsonc_file(path: &Path, label: &str) -> Result<serde_json::Value> {
598
644
  parse_jsonc_str(&raw, &format!("{} file {}", label, path.display()))
599
645
  }
600
646
 
601
- fn parse_object_from_value(value: serde_json::Value, context: &str) -> Result<serde_json::Map<String, serde_json::Value>> {
647
+ fn parse_object_from_value(
648
+ value: serde_json::Value,
649
+ context: &str,
650
+ ) -> Result<serde_json::Map<String, serde_json::Value>> {
602
651
  match value {
603
652
  serde_json::Value::Object(map) => Ok(map),
604
653
  _ => Err(anyhow!("{} must be a JSON object", context)),
@@ -659,7 +708,8 @@ fn write_atomic(path: &Path, payload: &[u8]) -> Result<()> {
659
708
  fn save_session(session: &SessionConfig) -> Result<()> {
660
709
  let path = config_path()?;
661
710
  if let Some(parent) = path.parent() {
662
- fs::create_dir_all(parent).with_context(|| format!("Failed to create {}", parent.display()))?;
711
+ fs::create_dir_all(parent)
712
+ .with_context(|| format!("Failed to create {}", parent.display()))?;
663
713
  }
664
714
  let payload = serde_json::to_vec_pretty(session)?;
665
715
  write_atomic(&path, &payload)?;
@@ -697,7 +747,8 @@ fn load_update_cache() -> Option<UpdateCheckCache> {
697
747
  fn save_update_cache(cache: &UpdateCheckCache) -> Result<()> {
698
748
  let path = update_cache_path()?;
699
749
  if let Some(parent) = path.parent() {
700
- fs::create_dir_all(parent).with_context(|| format!("Failed to create {}", parent.display()))?;
750
+ fs::create_dir_all(parent)
751
+ .with_context(|| format!("Failed to create {}", parent.display()))?;
701
752
  }
702
753
  let payload = serde_json::to_vec_pretty(cache)?;
703
754
  write_atomic(&path, &payload)?;
@@ -755,7 +806,11 @@ async fn fetch_latest_cli_version(client: &reqwest::Client) -> Option<String> {
755
806
  .filter(|value| !value.is_empty())
756
807
  }
757
808
 
758
- async fn maybe_notify_update(client: &reqwest::Client, force_refresh: bool, allow_network_fetch: bool) {
809
+ async fn maybe_notify_update(
810
+ client: &reqwest::Client,
811
+ force_refresh: bool,
812
+ allow_network_fetch: bool,
813
+ ) {
759
814
  if std::env::var("REALLINK_DISABLE_AUTO_UPDATE_CHECK")
760
815
  .map(|value| value == "1")
761
816
  .unwrap_or(false)
@@ -824,16 +879,43 @@ fn join_remote_path(prefix: Option<&str>, file_name: &str) -> String {
824
879
  format!("{}/{}", prefix_clean, file_clean)
825
880
  }
826
881
 
882
+ fn base_name_from_virtual_path(path: &str) -> String {
883
+ let normalized = clean_virtual_path(path);
884
+ normalized
885
+ .split('/')
886
+ .filter(|segment| !segment.is_empty())
887
+ .next_back()
888
+ .unwrap_or("download.bin")
889
+ .to_string()
890
+ }
891
+
827
892
  async fn authed_request(
828
893
  client: &reqwest::Client,
829
894
  session: &mut SessionConfig,
830
895
  method: Method,
831
896
  path: &str,
832
897
  body: Option<serde_json::Value>,
898
+ ) -> Result<reqwest::Response> {
899
+ authed_request_with_headers(client, session, method, path, body, &[]).await
900
+ }
901
+
902
+ async fn authed_request_with_headers(
903
+ client: &reqwest::Client,
904
+ session: &mut SessionConfig,
905
+ method: Method,
906
+ path: &str,
907
+ body: Option<serde_json::Value>,
908
+ extra_headers: &[(String, String)],
833
909
  ) -> Result<reqwest::Response> {
834
910
  let url = format!("{}{}", normalize_base_url(&session.base_url), path);
835
- let mut request =
836
- with_cli_headers(client.request(method.clone(), &url).bearer_auth(&session.access_token));
911
+ let mut request = with_cli_headers(
912
+ client
913
+ .request(method.clone(), &url)
914
+ .bearer_auth(&session.access_token),
915
+ );
916
+ for (key, value) in extra_headers {
917
+ request = request.header(key, value);
918
+ }
837
919
  if let Some(ref body_value) = body {
838
920
  request = request.json(body_value);
839
921
  }
@@ -844,7 +926,14 @@ async fn authed_request(
844
926
 
845
927
  refresh_session(client, session).await?;
846
928
 
847
- let mut retry = with_cli_headers(client.request(method, &url).bearer_auth(&session.access_token));
929
+ let mut retry = with_cli_headers(
930
+ client
931
+ .request(method, &url)
932
+ .bearer_auth(&session.access_token),
933
+ );
934
+ for (key, value) in extra_headers {
935
+ retry = retry.header(key, value);
936
+ }
848
937
  if let Some(ref body_value) = body {
849
938
  retry = retry.json(body_value);
850
939
  }
@@ -857,7 +946,9 @@ async fn refresh_session(client: &reqwest::Client, session: &mut SessionConfig)
857
946
  refresh_token: session.refresh_token.clone(),
858
947
  session_id: session.session_id.clone(),
859
948
  };
860
- let response = with_cli_headers(client.post(url).json(&payload)).send().await?;
949
+ let response = with_cli_headers(client.post(url).json(&payload))
950
+ .send()
951
+ .await?;
861
952
  if !response.status().is_success() {
862
953
  let body = read_error_body(response).await;
863
954
  return Err(anyhow!("Refresh failed: {}", body));
@@ -924,13 +1015,14 @@ async fn login_command(client: &reqwest::Client, args: LoginArgs) -> Result<()>
924
1015
  args.scope
925
1016
  };
926
1017
 
927
- let device_code_response = with_cli_headers(client.post(format!("{}/auth/device/code", base_url)))
928
- .json(&DeviceCodeRequest {
929
- client_id: args.client_id.clone(),
930
- scope,
931
- })
932
- .send()
933
- .await?;
1018
+ let device_code_response =
1019
+ with_cli_headers(client.post(format!("{}/auth/device/code", base_url)))
1020
+ .json(&DeviceCodeRequest {
1021
+ client_id: args.client_id.clone(),
1022
+ scope,
1023
+ })
1024
+ .send()
1025
+ .await?;
934
1026
 
935
1027
  if !device_code_response.status().is_success() {
936
1028
  let body = read_error_body(device_code_response).await;
@@ -961,14 +1053,15 @@ async fn login_command(client: &reqwest::Client, args: LoginArgs) -> Result<()>
961
1053
 
962
1054
  sleep(poll_interval).await;
963
1055
 
964
- let token_response = with_cli_headers(client.post(format!("{}/auth/device/token", base_url)))
965
- .json(&DeviceTokenRequest {
966
- grant_type: "urn:ietf:params:oauth:grant-type:device_code".to_string(),
967
- device_code: device_code.device_code.clone(),
968
- client_id: args.client_id.clone(),
969
- })
970
- .send()
971
- .await?;
1056
+ let token_response =
1057
+ with_cli_headers(client.post(format!("{}/auth/device/token", base_url)))
1058
+ .json(&DeviceTokenRequest {
1059
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code".to_string(),
1060
+ device_code: device_code.device_code.clone(),
1061
+ client_id: args.client_id.clone(),
1062
+ })
1063
+ .send()
1064
+ .await?;
972
1065
 
973
1066
  if token_response.status().is_success() {
974
1067
  let tokens: DeviceTokenSuccess = token_response.json().await?;
@@ -988,10 +1081,8 @@ async fn login_command(client: &reqwest::Client, args: LoginArgs) -> Result<()>
988
1081
  return Ok(());
989
1082
  }
990
1083
 
991
- let error_payload: DeviceTokenError = token_response
992
- .json()
993
- .await
994
- .unwrap_or(DeviceTokenError {
1084
+ let error_payload: DeviceTokenError =
1085
+ token_response.json().await.unwrap_or(DeviceTokenError {
995
1086
  error: "unknown_error".to_string(),
996
1087
  error_description: Some("Could not parse auth error".to_string()),
997
1088
  });
@@ -1018,7 +1109,7 @@ async fn login_command(client: &reqwest::Client, args: LoginArgs) -> Result<()>
1018
1109
  "Device login failed: {} ({})",
1019
1110
  error_payload.error,
1020
1111
  error_payload.error_description.unwrap_or_default()
1021
- ))
1112
+ ));
1022
1113
  }
1023
1114
  }
1024
1115
  }
@@ -1060,7 +1151,10 @@ async fn logout_command(client: &reqwest::Client) -> Result<()> {
1060
1151
  }
1061
1152
 
1062
1153
  if remote_revoked {
1063
- println!("Logged out. Server session revoked and local session removed from {}.", path_display);
1154
+ println!(
1155
+ "Logged out. Server session revoked and local session removed from {}.",
1156
+ path_display
1157
+ );
1064
1158
  } else if remote_unavailable {
1065
1159
  println!("Logged out locally. Removed session from {}.", path_display);
1066
1160
  println!("Server logout endpoint is not available on this API deployment yet.");
@@ -1119,7 +1213,14 @@ async fn token_create_command(client: &reqwest::Client, args: TokenCreateArgs) -
1119
1213
  expires_in_days: args.expires_in_days,
1120
1214
  })?;
1121
1215
 
1122
- let response = authed_request(client, &mut session, Method::POST, "/auth/tokens", Some(body)).await?;
1216
+ let response = authed_request(
1217
+ client,
1218
+ &mut session,
1219
+ Method::POST,
1220
+ "/auth/tokens",
1221
+ Some(body),
1222
+ )
1223
+ .await?;
1123
1224
  if !response.status().is_success() {
1124
1225
  let body_text = read_error_body(response).await;
1125
1226
  return Err(anyhow!("token create failed: {}", body_text));
@@ -1151,7 +1252,9 @@ async fn project_list_command(client: &reqwest::Client, args: ProjectListArgs) -
1151
1252
  apply_base_url_override(&mut session, args.base_url);
1152
1253
 
1153
1254
  let path = match args.org_id {
1154
- Some(org_id) if !org_id.trim().is_empty() => format!("/core/projects?orgId={}", org_id.trim()),
1255
+ Some(org_id) if !org_id.trim().is_empty() => {
1256
+ format!("/core/projects?orgId={}", org_id.trim())
1257
+ }
1155
1258
  _ => "/core/projects".to_string(),
1156
1259
  };
1157
1260
 
@@ -1303,11 +1406,23 @@ async fn upload_asset_via_intent(
1303
1406
  let complete_payload = if strategy == "multipart" {
1304
1407
  let part_size = intent
1305
1408
  .part_size_bytes
1306
- .and_then(|value| if value > 0 { Some(value as usize) } else { None })
1409
+ .and_then(|value| {
1410
+ if value > 0 {
1411
+ Some(value as usize)
1412
+ } else {
1413
+ None
1414
+ }
1415
+ })
1307
1416
  .ok_or_else(|| anyhow!("multipart upload intent is missing partSizeBytes"))?;
1308
1417
  let part_count = intent
1309
1418
  .part_count
1310
- .and_then(|value| if value > 0 { Some(value as usize) } else { None })
1419
+ .and_then(|value| {
1420
+ if value > 0 {
1421
+ Some(value as usize)
1422
+ } else {
1423
+ None
1424
+ }
1425
+ })
1311
1426
  .ok_or_else(|| anyhow!("multipart upload intent is missing partCount"))?;
1312
1427
  let session_id = intent
1313
1428
  .session_id
@@ -1459,6 +1574,144 @@ async fn file_get_command(client: &reqwest::Client, args: FileGetArgs) -> Result
1459
1574
  Ok(())
1460
1575
  }
1461
1576
 
1577
+ async fn file_stat_command(client: &reqwest::Client, args: FileStatArgs) -> Result<()> {
1578
+ let mut session = load_session()?;
1579
+ apply_base_url_override(&mut session, args.base_url);
1580
+
1581
+ let path = format!("/assets/{}/metadata", args.asset_id);
1582
+ let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
1583
+ if !response.status().is_success() {
1584
+ let body = read_error_body(response).await;
1585
+ return Err(anyhow!("file stat failed: {}", body));
1586
+ }
1587
+ let payload: AssetMetadataResponse = response.json().await?;
1588
+ println!("{}", serde_json::to_string_pretty(&payload)?);
1589
+ save_session(&session)?;
1590
+ Ok(())
1591
+ }
1592
+
1593
+ async fn file_download_command(client: &reqwest::Client, args: FileDownloadArgs) -> Result<()> {
1594
+ let mut session = load_session()?;
1595
+ apply_base_url_override(&mut session, args.base_url);
1596
+
1597
+ let metadata_path = format!("/assets/{}/metadata", args.asset_id);
1598
+ let metadata_response =
1599
+ authed_request(client, &mut session, Method::GET, &metadata_path, None).await?;
1600
+ if !metadata_response.status().is_success() {
1601
+ let body = read_error_body(metadata_response).await;
1602
+ return Err(anyhow!("file metadata fetch failed: {}", body));
1603
+ }
1604
+ let metadata_payload: AssetMetadataResponse = metadata_response.json().await?;
1605
+ let fallback_name = base_name_from_virtual_path(&metadata_payload.asset.file_name);
1606
+
1607
+ let mut output_path = args
1608
+ .output
1609
+ .unwrap_or_else(|| PathBuf::from(fallback_name.as_str()));
1610
+ if output_path.exists() && output_path.is_dir() {
1611
+ output_path = output_path.join(fallback_name.as_str());
1612
+ }
1613
+
1614
+ if let Some(parent) = output_path.parent() {
1615
+ if !parent.as_os_str().is_empty() {
1616
+ tokio_fs::create_dir_all(parent).await.with_context(|| {
1617
+ format!("Failed to create output directory {}", parent.display())
1618
+ })?;
1619
+ }
1620
+ }
1621
+
1622
+ let mut resume_from: Option<u64> = None;
1623
+ if args.resume && output_path.exists() && output_path.is_file() {
1624
+ let existing_size = tokio_fs::metadata(&output_path)
1625
+ .await
1626
+ .with_context(|| {
1627
+ format!(
1628
+ "Failed to read output file metadata {}",
1629
+ output_path.display()
1630
+ )
1631
+ })?
1632
+ .len();
1633
+ if existing_size > 0 {
1634
+ let remote_size = metadata_payload.content.content_length.max(0) as u64;
1635
+ if existing_size >= remote_size && remote_size > 0 {
1636
+ println!(
1637
+ "{}",
1638
+ serde_json::to_string_pretty(&serde_json::json!({
1639
+ "assetId": metadata_payload.asset.id,
1640
+ "fileName": metadata_payload.asset.file_name,
1641
+ "output": output_path.display().to_string(),
1642
+ "bytesWritten": 0,
1643
+ "resumedFrom": existing_size,
1644
+ "alreadyComplete": true
1645
+ }))?
1646
+ );
1647
+ save_session(&session)?;
1648
+ return Ok(());
1649
+ }
1650
+ resume_from = Some(existing_size);
1651
+ }
1652
+ }
1653
+
1654
+ let download_path = format!("/assets/{}/download", args.asset_id);
1655
+ let mut headers = Vec::new();
1656
+ if let Some(offset) = resume_from {
1657
+ headers.push(("range".to_string(), format!("bytes={}-", offset)));
1658
+ }
1659
+ let mut response = authed_request_with_headers(
1660
+ client,
1661
+ &mut session,
1662
+ Method::GET,
1663
+ &download_path,
1664
+ None,
1665
+ &headers,
1666
+ )
1667
+ .await?;
1668
+ if !(response.status().is_success() || response.status() == StatusCode::PARTIAL_CONTENT) {
1669
+ let body = read_error_body(response).await;
1670
+ return Err(anyhow!("file download failed: {}", body));
1671
+ }
1672
+
1673
+ let append_mode = resume_from.is_some() && response.status() == StatusCode::PARTIAL_CONTENT;
1674
+ let mut output_file = if append_mode {
1675
+ tokio_fs::OpenOptions::new()
1676
+ .append(true)
1677
+ .open(&output_path)
1678
+ .await
1679
+ .with_context(|| {
1680
+ format!(
1681
+ "Failed to open output file for append {}",
1682
+ output_path.display()
1683
+ )
1684
+ })?
1685
+ } else {
1686
+ tokio_fs::File::create(&output_path)
1687
+ .await
1688
+ .with_context(|| format!("Failed to create output file {}", output_path.display()))?
1689
+ };
1690
+ let mut bytes_written: u64 = 0;
1691
+ while let Some(chunk) = response.chunk().await? {
1692
+ output_file
1693
+ .write_all(&chunk)
1694
+ .await
1695
+ .with_context(|| format!("Failed to write to {}", output_path.display()))?;
1696
+ bytes_written = bytes_written.saturating_add(chunk.len() as u64);
1697
+ }
1698
+ output_file.flush().await?;
1699
+
1700
+ println!(
1701
+ "{}",
1702
+ serde_json::to_string_pretty(&serde_json::json!({
1703
+ "assetId": metadata_payload.asset.id,
1704
+ "fileName": metadata_payload.asset.file_name,
1705
+ "output": output_path.display().to_string(),
1706
+ "bytesWritten": bytes_written,
1707
+ "resumedFrom": resume_from,
1708
+ "partialContent": response.status() == StatusCode::PARTIAL_CONTENT
1709
+ }))?
1710
+ );
1711
+ save_session(&session)?;
1712
+ Ok(())
1713
+ }
1714
+
1462
1715
  async fn file_upload_command(client: &reqwest::Client, args: FileUploadArgs) -> Result<()> {
1463
1716
  let mut session = load_session()?;
1464
1717
  apply_base_url_override(&mut session, args.base_url);
@@ -1589,12 +1842,16 @@ async fn tool_register_command(client: &reqwest::Client, args: ToolRegisterArgs)
1589
1842
  apply_base_url_override(&mut session, args.base_url);
1590
1843
 
1591
1844
  let manifest = load_jsonc_file(&args.manifest, "tool manifest")?;
1592
- let body = serde_json::Value::Object(parse_object_from_value(
1593
- manifest,
1594
- "tool manifest payload",
1595
- )?);
1596
- let response =
1597
- authed_request(client, &mut session, Method::POST, "/tools/definitions", Some(body)).await?;
1845
+ let body =
1846
+ serde_json::Value::Object(parse_object_from_value(manifest, "tool manifest payload")?);
1847
+ let response = authed_request(
1848
+ client,
1849
+ &mut session,
1850
+ Method::POST,
1851
+ "/tools/definitions",
1852
+ Some(body),
1853
+ )
1854
+ .await?;
1598
1855
  if !response.status().is_success() {
1599
1856
  let body = read_error_body(response).await;
1600
1857
  return Err(anyhow!("tool register failed: {}", body));
@@ -1637,13 +1894,19 @@ async fn tool_set_entitlement_command(
1637
1894
  body.insert("orgId".to_string(), serde_json::Value::String(org_id));
1638
1895
  }
1639
1896
  if let Some(project_id) = project_id {
1640
- body.insert("projectId".to_string(), serde_json::Value::String(project_id));
1897
+ body.insert(
1898
+ "projectId".to_string(),
1899
+ serde_json::Value::String(project_id),
1900
+ );
1641
1901
  }
1642
1902
  if let Some(user_id) = user_id {
1643
1903
  body.insert("userId".to_string(), serde_json::Value::String(user_id));
1644
1904
  }
1645
1905
  if let Some(expires_at) = expires_at {
1646
- body.insert("expiresAt".to_string(), serde_json::Value::String(expires_at));
1906
+ body.insert(
1907
+ "expiresAt".to_string(),
1908
+ serde_json::Value::String(expires_at),
1909
+ );
1647
1910
  }
1648
1911
  if let Some(path) = metadata_file {
1649
1912
  let metadata = load_jsonc_file(&path, "tool entitlement metadata")?;
@@ -1731,19 +1994,23 @@ async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result
1731
1994
  serde_json::Value::Object(serde_json::Map::new())
1732
1995
  };
1733
1996
 
1734
- let input_object = serde_json::Value::Object(parse_object_from_value(
1735
- input_value,
1736
- "tool run input",
1737
- )?);
1997
+ let input_object =
1998
+ serde_json::Value::Object(parse_object_from_value(input_value, "tool run input")?);
1738
1999
 
1739
2000
  let mut body = serde_json::Map::new();
1740
- body.insert("toolId".to_string(), serde_json::Value::String(args.tool_id));
2001
+ body.insert(
2002
+ "toolId".to_string(),
2003
+ serde_json::Value::String(args.tool_id),
2004
+ );
1741
2005
  body.insert("input".to_string(), input_object);
1742
2006
  if let Some(org_id) = args.org_id {
1743
2007
  body.insert("orgId".to_string(), serde_json::Value::String(org_id));
1744
2008
  }
1745
2009
  if let Some(project_id) = args.project_id {
1746
- body.insert("projectId".to_string(), serde_json::Value::String(project_id));
2010
+ body.insert(
2011
+ "projectId".to_string(),
2012
+ serde_json::Value::String(project_id),
2013
+ );
1747
2014
  }
1748
2015
  let mut metadata_map = if let Some(path) = args.metadata_file {
1749
2016
  let metadata = load_jsonc_file(&path, "tool run metadata")?;
@@ -1761,7 +2028,10 @@ async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result
1761
2028
  }
1762
2029
  }
1763
2030
  if !metadata_map.is_empty() {
1764
- body.insert("metadata".to_string(), serde_json::Value::Object(metadata_map));
2031
+ body.insert(
2032
+ "metadata".to_string(),
2033
+ serde_json::Value::Object(metadata_map),
2034
+ );
1765
2035
  }
1766
2036
 
1767
2037
  let response = authed_request(
@@ -1886,7 +2156,9 @@ async fn self_update_command(client: &reqwest::Client, args: SelfUpdateArgs) ->
1886
2156
  },
1887
2157
  "npm self-update",
1888
2158
  )?;
1889
- println!("Updated via npm. Restart your shell if `reallink --version` still shows old version.");
2159
+ println!(
2160
+ "Updated via npm. Restart your shell if `reallink --version` still shows old version."
2161
+ );
1890
2162
  return Ok(());
1891
2163
  }
1892
2164
 
@@ -1952,6 +2224,8 @@ async fn main() -> Result<()> {
1952
2224
  Commands::File { command } => match command {
1953
2225
  FileCommands::List(args) => file_list_command(&client, args).await?,
1954
2226
  FileCommands::Get(args) => file_get_command(&client, args).await?,
2227
+ FileCommands::Stat(args) => file_stat_command(&client, args).await?,
2228
+ FileCommands::Download(args) => file_download_command(&client, args).await?,
1955
2229
  FileCommands::Upload(args) => file_upload_command(&client, args).await?,
1956
2230
  FileCommands::Mkdir(args) => file_mkdir_command(&client, args).await?,
1957
2231
  FileCommands::Move(args) => file_move_command(&client, args).await?,