reallink-cli 0.1.13 → 0.1.14

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
@@ -75,6 +75,9 @@ reallink tool register --manifest ./spec/tools/trace-placeholder.tool.jsonc
75
75
  reallink tool publish --tool-id trace.placeholder --channel ga --visibility public
76
76
  reallink tool enable --tool-id trace.placeholder --project-id prj_xxx
77
77
  reallink tool enable --tool-id trace.placeholder --user-id usr_xxx
78
+ reallink tool context put --context-id aa-main --project-id prj_xxx --text "Build notes and constraints"
79
+ reallink tool context get --context-id aa-main --project-id prj_xxx
80
+ reallink tool prompt --tool-id agent.chat --project-id prj_xxx --prompt "Summarize latest trace findings" --session-key aa-main
78
81
  reallink tool run --tool-id trace.placeholder --project-id prj_xxx --input-file ./run-input.jsonc --idempotency-key run-001
79
82
  reallink tool runs --project-id prj_xxx
80
83
  reallink tool get-run --run-id trun_xxx
@@ -104,7 +107,8 @@ reallink link plugin install --project-id prj_xxx --name RealLinkUnreal --url ht
104
107
  - If the server rejects `tools:*` as invalid, CLI automatically retries with compatible scopes.
105
108
  - You can always pass explicit scopes via repeated `--scope`.
106
109
 
107
- `tool register`, `tool enable/disable` metadata, and `tool run --input-file` accept JSONC.
110
+ `tool register`, `tool enable/disable` metadata, `tool run --input-file`, and `tool prompt --input-file` accept JSONC.
111
+ `tool context put --text-file` accepts plain UTF-8 text files and stores scoped context via API gateway.
108
112
 
109
113
  ## Session Behavior
110
114
 
package/bin/reallink.cjs CHANGED
@@ -34,8 +34,15 @@ function runBinary(binaryPath) {
34
34
  });
35
35
  }
36
36
 
37
+ function resolvePreferredBinary() {
38
+ if (fs.existsSync(prebuiltBinaryPath)) {
39
+ return prebuiltBinaryPath;
40
+ }
41
+ return resolveExistingReleaseBinary();
42
+ }
43
+
37
44
  function ensureBinary() {
38
- if (resolveExistingReleaseBinary() || fs.existsSync(prebuiltBinaryPath)) {
45
+ if (resolvePreferredBinary()) {
39
46
  return true;
40
47
  }
41
48
 
@@ -56,14 +63,14 @@ function ensureBinary() {
56
63
  return false;
57
64
  }
58
65
 
59
- return build.status === 0 && !!resolveExistingReleaseBinary();
66
+ return build.status === 0 && !!resolvePreferredBinary();
60
67
  }
61
68
 
62
69
  if (!ensureBinary()) {
63
70
  process.exit(1);
64
71
  }
65
72
 
66
- const binaryPath = resolveExistingReleaseBinary() || prebuiltBinaryPath;
73
+ const binaryPath = resolvePreferredBinary();
67
74
  const result = runBinary(binaryPath);
68
75
  if (result.error) {
69
76
  console.error(`Failed to run reallink binary: ${result.error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reallink-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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
@@ -993,7 +993,7 @@ dependencies = [
993
993
 
994
994
  [[package]]
995
995
  name = "reallink-cli"
996
- version = "0.1.13"
996
+ version = "0.1.14"
997
997
  dependencies = [
998
998
  "anyhow",
999
999
  "clap",
package/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "reallink-cli"
3
- version = "0.1.13"
3
+ version = "0.1.14"
4
4
  edition = "2021"
5
5
  description = "CLI for Reallink auth and token workflows"
6
6
  license = "MIT"
package/rust/src/main.rs CHANGED
@@ -82,6 +82,10 @@ enum Commands {
82
82
  #[command(subcommand)]
83
83
  command: FileCommands,
84
84
  },
85
+ Skill {
86
+ #[command(subcommand)]
87
+ command: SkillCommands,
88
+ },
85
89
  Tool {
86
90
  #[command(subcommand)]
87
91
  command: ToolCommands,
@@ -192,10 +196,16 @@ enum ToolCommands {
192
196
  Publish(ToolPublishArgs),
193
197
  Enable(ToolEnableArgs),
194
198
  Disable(ToolDisableArgs),
199
+ Context {
200
+ #[command(subcommand)]
201
+ command: ToolContextCommands,
202
+ },
203
+ Prompt(ToolPromptArgs),
195
204
  Run(ToolRunArgs),
196
205
  Runs(ToolRunsArgs),
197
206
  GetRun(ToolGetRunArgs),
198
207
  RunEvents(ToolRunEventsArgs),
208
+ TraceStatus(ToolTraceStatusArgs),
199
209
  Local {
200
210
  #[command(subcommand)]
201
211
  command: ToolLocalCommands,
@@ -209,6 +219,35 @@ enum ToolLocalCommands {
209
219
  CompleteRun(ToolLocalCompleteRunArgs),
210
220
  }
211
221
 
222
+ #[derive(Subcommand)]
223
+ enum ToolContextCommands {
224
+ Put(ToolContextPutArgs),
225
+ Get(ToolContextGetArgs),
226
+ }
227
+
228
+ #[derive(Subcommand)]
229
+ enum SkillCommands {
230
+ List(ToolListArgs),
231
+ Register(ToolRegisterArgs),
232
+ Publish(ToolPublishArgs),
233
+ Enable(ToolEnableArgs),
234
+ Disable(ToolDisableArgs),
235
+ Context {
236
+ #[command(subcommand)]
237
+ command: ToolContextCommands,
238
+ },
239
+ Prompt(ToolPromptArgs),
240
+ Run(ToolRunArgs),
241
+ Runs(ToolRunsArgs),
242
+ GetRun(ToolGetRunArgs),
243
+ RunEvents(ToolRunEventsArgs),
244
+ TraceStatus(ToolTraceStatusArgs),
245
+ Local {
246
+ #[command(subcommand)]
247
+ command: ToolLocalCommands,
248
+ },
249
+ }
250
+
212
251
  #[derive(Subcommand)]
213
252
  enum LogsCommands {
214
253
  Status,
@@ -478,6 +517,46 @@ struct ToolRunArgs {
478
517
  metadata_file: Option<PathBuf>,
479
518
  #[arg(long, help = "Idempotency key for deduplicating retries")]
480
519
  idempotency_key: Option<String>,
520
+ #[arg(long, action = ArgAction::SetTrue, help = "Wait until the run reaches a terminal state")]
521
+ wait: bool,
522
+ #[arg(long, default_value_t = 300_000, help = "Wait timeout in milliseconds")]
523
+ timeout_ms: u64,
524
+ #[arg(long, default_value_t = 1_500, help = "Polling interval in milliseconds")]
525
+ poll_interval_ms: u64,
526
+ #[arg(long)]
527
+ base_url: Option<String>,
528
+ }
529
+
530
+ #[derive(Args)]
531
+ struct ToolPromptArgs {
532
+ #[arg(long)]
533
+ tool_id: String,
534
+ #[arg(long)]
535
+ prompt: String,
536
+ #[arg(long)]
537
+ org_id: Option<String>,
538
+ #[arg(long)]
539
+ project_id: Option<String>,
540
+ #[arg(long, help = "Optional system prompt")]
541
+ system_prompt: Option<String>,
542
+ #[arg(long, help = "Optional model hint passed to remote runtime")]
543
+ model: Option<String>,
544
+ #[arg(long, help = "Session key used by remote runtime for context restore")]
545
+ session_key: Option<String>,
546
+ #[arg(long, help = "Inline JSON object merged into tool input")]
547
+ input_json: Option<String>,
548
+ #[arg(long, help = "Path to JSON/JSONC file merged into tool input")]
549
+ input_file: Option<PathBuf>,
550
+ #[arg(long, help = "Path to JSON/JSONC metadata file")]
551
+ metadata_file: Option<PathBuf>,
552
+ #[arg(long, help = "Idempotency key for deduplicating retries")]
553
+ idempotency_key: Option<String>,
554
+ #[arg(long, action = ArgAction::SetTrue, help = "Wait until the run reaches a terminal state")]
555
+ wait: bool,
556
+ #[arg(long, default_value_t = 300_000, help = "Wait timeout in milliseconds")]
557
+ timeout_ms: u64,
558
+ #[arg(long, default_value_t = 1_500, help = "Polling interval in milliseconds")]
559
+ poll_interval_ms: u64,
481
560
  #[arg(long)]
482
561
  base_url: Option<String>,
483
562
  }
@@ -522,6 +601,44 @@ struct ToolRunEventsArgs {
522
601
  base_url: Option<String>,
523
602
  }
524
603
 
604
+ #[derive(Args)]
605
+ struct ToolTraceStatusArgs {
606
+ #[arg(long)]
607
+ project_id: String,
608
+ #[arg(long)]
609
+ limit: Option<u32>,
610
+ #[arg(long)]
611
+ base_url: Option<String>,
612
+ }
613
+
614
+ #[derive(Args)]
615
+ struct ToolContextPutArgs {
616
+ #[arg(long)]
617
+ context_id: String,
618
+ #[arg(long)]
619
+ text: Option<String>,
620
+ #[arg(long, help = "Path to UTF-8 text file for context payload")]
621
+ text_file: Option<PathBuf>,
622
+ #[arg(long)]
623
+ org_id: Option<String>,
624
+ #[arg(long)]
625
+ project_id: Option<String>,
626
+ #[arg(long)]
627
+ base_url: Option<String>,
628
+ }
629
+
630
+ #[derive(Args)]
631
+ struct ToolContextGetArgs {
632
+ #[arg(long)]
633
+ context_id: String,
634
+ #[arg(long)]
635
+ org_id: Option<String>,
636
+ #[arg(long)]
637
+ project_id: Option<String>,
638
+ #[arg(long)]
639
+ base_url: Option<String>,
640
+ }
641
+
525
642
  #[derive(Args)]
526
643
  struct ToolLocalCatalogArgs {
527
644
  #[arg(long)]
@@ -4943,6 +5060,141 @@ async fn tool_disable_command(client: &reqwest::Client, args: ToolDisableArgs) -
4943
5060
  .await
4944
5061
  }
4945
5062
 
5063
+ async fn tool_prompt_command(client: &reqwest::Client, args: ToolPromptArgs) -> Result<()> {
5064
+ let mut session = load_session()?;
5065
+ apply_base_url_override(&mut session, args.base_url);
5066
+
5067
+ if args.input_json.is_some() && args.input_file.is_some() {
5068
+ return Err(anyhow!(
5069
+ "Provide either --input-json or --input-file, not both"
5070
+ ));
5071
+ }
5072
+
5073
+ let prompt = args.prompt.trim();
5074
+ if prompt.is_empty() {
5075
+ return Err(anyhow!("--prompt is required"));
5076
+ }
5077
+
5078
+ let merged_input = if let Some(path) = args.input_file {
5079
+ load_jsonc_file(&path, "tool prompt input")?
5080
+ } else if let Some(input_json) = args.input_json {
5081
+ parse_jsonc_str(&input_json, "tool prompt input")?
5082
+ } else {
5083
+ serde_json::Value::Object(serde_json::Map::new())
5084
+ };
5085
+ let mut input_map = parse_object_from_value(merged_input, "tool prompt input")?;
5086
+ input_map.insert(
5087
+ "prompt".to_string(),
5088
+ serde_json::Value::String(prompt.to_string()),
5089
+ );
5090
+ if let Some(system_prompt) = args.system_prompt {
5091
+ let normalized = system_prompt.trim();
5092
+ if !normalized.is_empty() {
5093
+ input_map.insert(
5094
+ "systemPrompt".to_string(),
5095
+ serde_json::Value::String(normalized.to_string()),
5096
+ );
5097
+ }
5098
+ }
5099
+ if let Some(model) = args.model {
5100
+ let normalized = model.trim();
5101
+ if !normalized.is_empty() {
5102
+ input_map.insert(
5103
+ "model".to_string(),
5104
+ serde_json::Value::String(normalized.to_string()),
5105
+ );
5106
+ }
5107
+ }
5108
+ if let Some(session_key) = args.session_key {
5109
+ let normalized = session_key.trim();
5110
+ if !normalized.is_empty() {
5111
+ input_map.insert(
5112
+ "sessionKey".to_string(),
5113
+ serde_json::Value::String(normalized.to_string()),
5114
+ );
5115
+ }
5116
+ }
5117
+
5118
+ let mut body = serde_json::Map::new();
5119
+ body.insert(
5120
+ "toolId".to_string(),
5121
+ serde_json::Value::String(args.tool_id),
5122
+ );
5123
+ body.insert("input".to_string(), serde_json::Value::Object(input_map));
5124
+ if let Some(org_id) = args.org_id {
5125
+ body.insert("orgId".to_string(), serde_json::Value::String(org_id));
5126
+ }
5127
+ if let Some(project_id) = args.project_id {
5128
+ body.insert(
5129
+ "projectId".to_string(),
5130
+ serde_json::Value::String(project_id),
5131
+ );
5132
+ }
5133
+
5134
+ let mut metadata_map = if let Some(path) = args.metadata_file {
5135
+ let metadata = load_jsonc_file(&path, "tool prompt metadata")?;
5136
+ parse_object_from_value(metadata, "tool prompt metadata")?
5137
+ } else {
5138
+ serde_json::Map::new()
5139
+ };
5140
+ if let Some(idempotency_key) = args.idempotency_key {
5141
+ let normalized = idempotency_key.trim();
5142
+ if !normalized.is_empty() {
5143
+ metadata_map.insert(
5144
+ "idempotencyKey".to_string(),
5145
+ serde_json::Value::String(normalized.to_string()),
5146
+ );
5147
+ }
5148
+ }
5149
+ metadata_map.insert(
5150
+ "clientCommand".to_string(),
5151
+ serde_json::Value::String("tool.prompt".to_string()),
5152
+ );
5153
+ if !metadata_map.is_empty() {
5154
+ body.insert(
5155
+ "metadata".to_string(),
5156
+ serde_json::Value::Object(metadata_map),
5157
+ );
5158
+ }
5159
+
5160
+ let response = authed_request(
5161
+ client,
5162
+ &mut session,
5163
+ Method::POST,
5164
+ "/tools/runs",
5165
+ Some(serde_json::Value::Object(body)),
5166
+ )
5167
+ .await?;
5168
+ if !response.status().is_success() {
5169
+ let body = read_error_body(response).await;
5170
+ return Err(anyhow!("tool prompt failed: {}", body));
5171
+ }
5172
+ let payload: serde_json::Value = response.json().await?;
5173
+ let payload = if args.wait {
5174
+ let run_id = payload
5175
+ .get("run")
5176
+ .and_then(|run| run.get("id"))
5177
+ .and_then(serde_json::Value::as_str)
5178
+ .ok_or_else(|| anyhow!("tool prompt response missing run.id"))?;
5179
+ wait_for_tool_run_completion(
5180
+ client,
5181
+ &mut session,
5182
+ run_id,
5183
+ args.timeout_ms,
5184
+ args.poll_interval_ms,
5185
+ )
5186
+ .await?
5187
+ } else {
5188
+ payload
5189
+ };
5190
+ println!(
5191
+ "{}",
5192
+ serde_json::to_string_pretty(payload.get("run").unwrap_or(&payload))?
5193
+ );
5194
+ save_session(&session)?;
5195
+ Ok(())
5196
+ }
5197
+
4946
5198
  async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result<()> {
4947
5199
  let mut session = load_session()?;
4948
5200
  apply_base_url_override(&mut session, args.base_url);
@@ -5014,6 +5266,23 @@ async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result
5014
5266
  return Err(anyhow!("tool run failed: {}", body));
5015
5267
  }
5016
5268
  let payload: serde_json::Value = response.json().await?;
5269
+ let payload = if args.wait {
5270
+ let run_id = payload
5271
+ .get("run")
5272
+ .and_then(|run| run.get("id"))
5273
+ .and_then(serde_json::Value::as_str)
5274
+ .ok_or_else(|| anyhow!("tool run response missing run.id"))?;
5275
+ wait_for_tool_run_completion(
5276
+ client,
5277
+ &mut session,
5278
+ run_id,
5279
+ args.timeout_ms,
5280
+ args.poll_interval_ms,
5281
+ )
5282
+ .await?
5283
+ } else {
5284
+ payload
5285
+ };
5017
5286
  println!(
5018
5287
  "{}",
5019
5288
  serde_json::to_string_pretty(payload.get("run").unwrap_or(&payload))?
@@ -5022,6 +5291,42 @@ async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result
5022
5291
  Ok(())
5023
5292
  }
5024
5293
 
5294
+ async fn wait_for_tool_run_completion(
5295
+ client: &reqwest::Client,
5296
+ session: &mut SessionConfig,
5297
+ run_id: &str,
5298
+ timeout_ms: u64,
5299
+ poll_interval_ms: u64,
5300
+ ) -> Result<serde_json::Value> {
5301
+ let poll_interval = Duration::from_millis(poll_interval_ms.max(250));
5302
+ let deadline = std::time::Instant::now() + Duration::from_millis(timeout_ms.max(1_000));
5303
+
5304
+ loop {
5305
+ let path = format!("/tools/runs/{}", run_id);
5306
+ let response = authed_request(client, session, Method::GET, &path, None).await?;
5307
+ if !response.status().is_success() {
5308
+ let body = read_error_body(response).await;
5309
+ return Err(anyhow!("tool wait failed: {}", body));
5310
+ }
5311
+ let payload: serde_json::Value = response.json().await?;
5312
+ let status = payload
5313
+ .get("run")
5314
+ .and_then(|run| run.get("status"))
5315
+ .and_then(serde_json::Value::as_str)
5316
+ .unwrap_or_default();
5317
+ if matches!(status, "succeeded" | "failed" | "cancelled") {
5318
+ return Ok(payload);
5319
+ }
5320
+ if std::time::Instant::now() >= deadline {
5321
+ return Err(anyhow!(
5322
+ "Timed out waiting for tool run {} to finish",
5323
+ run_id
5324
+ ));
5325
+ }
5326
+ sleep(poll_interval).await;
5327
+ }
5328
+ }
5329
+
5025
5330
  async fn tool_runs_command(client: &reqwest::Client, args: ToolRunsArgs) -> Result<()> {
5026
5331
  let mut session = load_session()?;
5027
5332
  apply_base_url_override(&mut session, args.base_url);
@@ -5117,6 +5422,232 @@ async fn tool_run_events_command(client: &reqwest::Client, args: ToolRunEventsAr
5117
5422
  Ok(())
5118
5423
  }
5119
5424
 
5425
+ fn resolve_report_url(base_url: &str, report_download_path: Option<&str>) -> Option<String> {
5426
+ let path = report_download_path?.trim();
5427
+ if path.is_empty() {
5428
+ return None;
5429
+ }
5430
+ if path.starts_with("http://") || path.starts_with("https://") {
5431
+ return Some(path.to_string());
5432
+ }
5433
+ Some(format!("{}{}", normalize_base_url(base_url), path))
5434
+ }
5435
+
5436
+ fn extract_tool_run_report_paths(
5437
+ run: &serde_json::Map<String, serde_json::Value>,
5438
+ base_url: &str,
5439
+ ) -> serde_json::Value {
5440
+ let output = run
5441
+ .get("output")
5442
+ .and_then(|value| value.as_object())
5443
+ .cloned()
5444
+ .unwrap_or_default();
5445
+
5446
+ let report_asset_id = output
5447
+ .get("reportHtmlAssetId")
5448
+ .and_then(|value| value.as_str())
5449
+ .or_else(|| output.get("reportAssetId").and_then(|value| value.as_str()));
5450
+ let report_download_path = output
5451
+ .get("reportHtmlDownloadPath")
5452
+ .and_then(|value| value.as_str())
5453
+ .or_else(|| output.get("reportDownloadPath").and_then(|value| value.as_str()));
5454
+ let report_url = resolve_report_url(base_url, report_download_path);
5455
+ let runtime = output
5456
+ .get("runtime")
5457
+ .and_then(|value| value.as_str())
5458
+ .unwrap_or_default();
5459
+ let workflow_mode = output
5460
+ .get("workflowMode")
5461
+ .and_then(|value| value.as_str())
5462
+ .unwrap_or_default();
5463
+ let summary = output
5464
+ .get("summary")
5465
+ .and_then(|value| value.as_str())
5466
+ .unwrap_or_default();
5467
+
5468
+ serde_json::json!({
5469
+ "reportAssetId": report_asset_id,
5470
+ "reportDownloadPath": report_download_path,
5471
+ "reportUrl": report_url,
5472
+ "runtime": runtime,
5473
+ "workflowMode": workflow_mode,
5474
+ "summary": summary
5475
+ })
5476
+ }
5477
+
5478
+ async fn tool_trace_status_command(client: &reqwest::Client, args: ToolTraceStatusArgs) -> Result<()> {
5479
+ let mut session = load_session()?;
5480
+ apply_base_url_override(&mut session, args.base_url);
5481
+
5482
+ let limit = args.limit.unwrap_or(80).max(1);
5483
+ let path = format!("/tools/runs?projectId={}&limit={}", args.project_id, limit);
5484
+ let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
5485
+ if !response.status().is_success() {
5486
+ let body = read_error_body(response).await;
5487
+ return Err(anyhow!("tool trace-status failed: {}", body));
5488
+ }
5489
+ let payload: serde_json::Value = response.json().await?;
5490
+ let runs = payload
5491
+ .get("runs")
5492
+ .and_then(|value| value.as_array())
5493
+ .cloned()
5494
+ .unwrap_or_default();
5495
+
5496
+ let mut items: Vec<serde_json::Value> = Vec::new();
5497
+ for run in &runs {
5498
+ let Some(run_obj) = run.as_object() else {
5499
+ continue;
5500
+ };
5501
+ let tool_id = run_obj
5502
+ .get("toolId")
5503
+ .and_then(|value| value.as_str())
5504
+ .unwrap_or_default()
5505
+ .to_string();
5506
+ let normalized_tool_id = tool_id.to_ascii_lowercase();
5507
+ if !normalized_tool_id.starts_with("trace") && !normalized_tool_id.starts_with("crash") {
5508
+ continue;
5509
+ }
5510
+
5511
+ let run_id = run_obj
5512
+ .get("id")
5513
+ .and_then(|value| value.as_str())
5514
+ .unwrap_or_default();
5515
+ let status = run_obj
5516
+ .get("status")
5517
+ .and_then(|value| value.as_str())
5518
+ .unwrap_or_default();
5519
+ let created_at = run_obj
5520
+ .get("createdAt")
5521
+ .and_then(|value| value.as_str())
5522
+ .unwrap_or_default();
5523
+ let completed_at = run_obj
5524
+ .get("completedAt")
5525
+ .and_then(|value| value.as_str())
5526
+ .unwrap_or_default();
5527
+ let error_message = run_obj
5528
+ .get("errorMessage")
5529
+ .and_then(|value| value.as_str())
5530
+ .unwrap_or_default();
5531
+ let report = extract_tool_run_report_paths(run_obj, &session.base_url);
5532
+
5533
+ items.push(serde_json::json!({
5534
+ "runId": run_id,
5535
+ "toolId": tool_id,
5536
+ "status": status,
5537
+ "createdAt": created_at,
5538
+ "completedAt": if completed_at.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(completed_at.to_string()) },
5539
+ "errorMessage": if error_message.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(error_message.to_string()) },
5540
+ "report": report
5541
+ }));
5542
+ }
5543
+
5544
+ println!(
5545
+ "{}",
5546
+ serde_json::to_string_pretty(&serde_json::json!({
5547
+ "projectId": args.project_id,
5548
+ "count": items.len(),
5549
+ "items": items
5550
+ }))?
5551
+ );
5552
+ save_session(&session)?;
5553
+ Ok(())
5554
+ }
5555
+
5556
+ fn build_tool_context_path(
5557
+ context_id: &str,
5558
+ org_id: Option<&str>,
5559
+ project_id: Option<&str>,
5560
+ ) -> Result<String> {
5561
+ if org_id.is_some() && project_id.is_some() {
5562
+ return Err(anyhow!("Only one of --org-id or --project-id can be set"));
5563
+ }
5564
+ let normalized_context_id = context_id.trim();
5565
+ if normalized_context_id.is_empty() {
5566
+ return Err(anyhow!("--context-id is required"));
5567
+ }
5568
+
5569
+ let mut path = format!("/tools/runtime/contexts/{}", normalized_context_id);
5570
+ let mut query_parts: Vec<String> = Vec::new();
5571
+ if let Some(org_id) = org_id {
5572
+ let normalized = org_id.trim();
5573
+ if !normalized.is_empty() {
5574
+ query_parts.push(format!("orgId={}", normalized));
5575
+ }
5576
+ }
5577
+ if let Some(project_id) = project_id {
5578
+ let normalized = project_id.trim();
5579
+ if !normalized.is_empty() {
5580
+ query_parts.push(format!("projectId={}", normalized));
5581
+ }
5582
+ }
5583
+ if !query_parts.is_empty() {
5584
+ path.push_str(&format!("?{}", query_parts.join("&")));
5585
+ }
5586
+ Ok(path)
5587
+ }
5588
+
5589
+ async fn tool_context_put_command(client: &reqwest::Client, args: ToolContextPutArgs) -> Result<()> {
5590
+ let mut session = load_session()?;
5591
+ apply_base_url_override(&mut session, args.base_url);
5592
+
5593
+ if args.text.is_some() && args.text_file.is_some() {
5594
+ return Err(anyhow!("Provide either --text or --text-file, not both"));
5595
+ }
5596
+ let context_text = if let Some(text_file) = args.text_file {
5597
+ fs::read_to_string(&text_file)
5598
+ .with_context(|| format!("Failed to read context text file {}", text_file.display()))?
5599
+ } else {
5600
+ args.text.unwrap_or_default()
5601
+ };
5602
+ if context_text.trim().is_empty() {
5603
+ return Err(anyhow!("Context text is required (--text or --text-file)"));
5604
+ }
5605
+
5606
+ let path = build_tool_context_path(
5607
+ &args.context_id,
5608
+ args.org_id.as_deref(),
5609
+ args.project_id.as_deref(),
5610
+ )?;
5611
+ let response = authed_request(
5612
+ client,
5613
+ &mut session,
5614
+ Method::PUT,
5615
+ &path,
5616
+ Some(serde_json::json!({
5617
+ "text": context_text
5618
+ })),
5619
+ )
5620
+ .await?;
5621
+ if !response.status().is_success() {
5622
+ let body = read_error_body(response).await;
5623
+ return Err(anyhow!("tool context put failed: {}", body));
5624
+ }
5625
+ let payload: serde_json::Value = response.json().await?;
5626
+ println!("{}", serde_json::to_string_pretty(&payload)?);
5627
+ save_session(&session)?;
5628
+ Ok(())
5629
+ }
5630
+
5631
+ async fn tool_context_get_command(client: &reqwest::Client, args: ToolContextGetArgs) -> Result<()> {
5632
+ let mut session = load_session()?;
5633
+ apply_base_url_override(&mut session, args.base_url);
5634
+
5635
+ let path = build_tool_context_path(
5636
+ &args.context_id,
5637
+ args.org_id.as_deref(),
5638
+ args.project_id.as_deref(),
5639
+ )?;
5640
+ let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
5641
+ if !response.status().is_success() {
5642
+ let body = read_error_body(response).await;
5643
+ return Err(anyhow!("tool context get failed: {}", body));
5644
+ }
5645
+ let payload: serde_json::Value = response.json().await?;
5646
+ println!("{}", serde_json::to_string_pretty(&payload)?);
5647
+ save_session(&session)?;
5648
+ Ok(())
5649
+ }
5650
+
5120
5651
  async fn tool_local_catalog_command(client: &reqwest::Client, args: ToolLocalCatalogArgs) -> Result<()> {
5121
5652
  let mut session = load_session()?;
5122
5653
  apply_base_url_override(&mut session, args.base_url);
@@ -5741,16 +6272,46 @@ async fn run_cli(cli: Cli) -> Result<()> {
5741
6272
  FileCommands::Rmdir(args) => file_rmdir_command(&client, args).await?,
5742
6273
  FileCommands::Remove(args) => file_remove_command(&client, args).await?,
5743
6274
  },
6275
+ Commands::Skill { command } => match command {
6276
+ SkillCommands::List(args) => tool_list_command(&client, args).await?,
6277
+ SkillCommands::Register(args) => tool_register_command(&client, args).await?,
6278
+ SkillCommands::Publish(args) => tool_publish_command(&client, args).await?,
6279
+ SkillCommands::Enable(args) => tool_enable_command(&client, args).await?,
6280
+ SkillCommands::Disable(args) => tool_disable_command(&client, args).await?,
6281
+ SkillCommands::Context { command } => match command {
6282
+ ToolContextCommands::Put(args) => tool_context_put_command(&client, args).await?,
6283
+ ToolContextCommands::Get(args) => tool_context_get_command(&client, args).await?,
6284
+ },
6285
+ SkillCommands::Prompt(args) => tool_prompt_command(&client, args).await?,
6286
+ SkillCommands::Run(args) => tool_run_command(&client, args).await?,
6287
+ SkillCommands::Runs(args) => tool_runs_command(&client, args).await?,
6288
+ SkillCommands::GetRun(args) => tool_get_run_command(&client, args).await?,
6289
+ SkillCommands::RunEvents(args) => tool_run_events_command(&client, args).await?,
6290
+ SkillCommands::TraceStatus(args) => tool_trace_status_command(&client, args).await?,
6291
+ SkillCommands::Local { command } => match command {
6292
+ ToolLocalCommands::Catalog(args) => tool_local_catalog_command(&client, args).await?,
6293
+ ToolLocalCommands::Install(args) => tool_local_install_command(&client, args).await?,
6294
+ ToolLocalCommands::CompleteRun(args) => {
6295
+ tool_local_complete_run_command(&client, args).await?
6296
+ }
6297
+ },
6298
+ },
5744
6299
  Commands::Tool { command } => match command {
5745
6300
  ToolCommands::List(args) => tool_list_command(&client, args).await?,
5746
6301
  ToolCommands::Register(args) => tool_register_command(&client, args).await?,
5747
6302
  ToolCommands::Publish(args) => tool_publish_command(&client, args).await?,
5748
6303
  ToolCommands::Enable(args) => tool_enable_command(&client, args).await?,
5749
6304
  ToolCommands::Disable(args) => tool_disable_command(&client, args).await?,
6305
+ ToolCommands::Context { command } => match command {
6306
+ ToolContextCommands::Put(args) => tool_context_put_command(&client, args).await?,
6307
+ ToolContextCommands::Get(args) => tool_context_get_command(&client, args).await?,
6308
+ },
6309
+ ToolCommands::Prompt(args) => tool_prompt_command(&client, args).await?,
5750
6310
  ToolCommands::Run(args) => tool_run_command(&client, args).await?,
5751
6311
  ToolCommands::Runs(args) => tool_runs_command(&client, args).await?,
5752
6312
  ToolCommands::GetRun(args) => tool_get_run_command(&client, args).await?,
5753
6313
  ToolCommands::RunEvents(args) => tool_run_events_command(&client, args).await?,
6314
+ ToolCommands::TraceStatus(args) => tool_trace_status_command(&client, args).await?,
5754
6315
  ToolCommands::Local { command } => match command {
5755
6316
  ToolLocalCommands::Catalog(args) => tool_local_catalog_command(&client, args).await?,
5756
6317
  ToolLocalCommands::Install(args) => tool_local_install_command(&client, args).await?,