reallink-cli 0.1.12 → 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 +31 -1
- package/bin/reallink.cjs +10 -3
- package/package.json +1 -1
- package/prebuilt/win32-x64/reallink-cli.exe +0 -0
- package/rust/Cargo.lock +1 -1
- package/rust/Cargo.toml +1 -1
- package/rust/src/generated/contract.rs +1 -1
- package/rust/src/logs.rs +277 -0
- package/rust/src/main.rs +1951 -182
package/rust/src/main.rs
CHANGED
|
@@ -14,6 +14,7 @@ use tokio::time::sleep;
|
|
|
14
14
|
|
|
15
15
|
mod unreal;
|
|
16
16
|
mod generated;
|
|
17
|
+
mod logs;
|
|
17
18
|
use unreal::{
|
|
18
19
|
LinkDoctorArgs, LinkOpenArgs, LinkPathsArgs, LinkPluginInstallArgs, LinkPluginListArgs,
|
|
19
20
|
LinkRemoveArgs, LinkRunArgs, LinkUnrealArgs, LinkUseArgs, PluginIndexFile, UnrealLinkRecord,
|
|
@@ -81,10 +82,18 @@ enum Commands {
|
|
|
81
82
|
#[command(subcommand)]
|
|
82
83
|
command: FileCommands,
|
|
83
84
|
},
|
|
85
|
+
Skill {
|
|
86
|
+
#[command(subcommand)]
|
|
87
|
+
command: SkillCommands,
|
|
88
|
+
},
|
|
84
89
|
Tool {
|
|
85
90
|
#[command(subcommand)]
|
|
86
91
|
command: ToolCommands,
|
|
87
92
|
},
|
|
93
|
+
Logs {
|
|
94
|
+
#[command(subcommand)]
|
|
95
|
+
command: LogsCommands,
|
|
96
|
+
},
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
#[derive(Args)]
|
|
@@ -149,24 +158,102 @@ enum OrgCommands {
|
|
|
149
158
|
#[derive(Subcommand)]
|
|
150
159
|
enum FileCommands {
|
|
151
160
|
List(FileListArgs),
|
|
161
|
+
Tree(FileTreeArgs),
|
|
152
162
|
Get(FileGetArgs),
|
|
153
163
|
Stat(FileStatArgs),
|
|
164
|
+
Thumbnail(FileThumbnailArgs),
|
|
154
165
|
Download(FileDownloadArgs),
|
|
155
166
|
Upload(FileUploadArgs),
|
|
156
167
|
Mkdir(FileMkdirArgs),
|
|
157
168
|
Move(FileMoveArgs),
|
|
169
|
+
Set(FileSetArgs),
|
|
170
|
+
MoveFolder(FileMoveFolderArgs),
|
|
171
|
+
Rmdir(FileRemoveFolderArgs),
|
|
158
172
|
Remove(FileRemoveArgs),
|
|
159
173
|
}
|
|
160
174
|
|
|
175
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
|
|
176
|
+
enum FileThumbnailSize {
|
|
177
|
+
Small,
|
|
178
|
+
Medium,
|
|
179
|
+
Large,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
impl FileThumbnailSize {
|
|
183
|
+
fn as_str(self) -> &'static str {
|
|
184
|
+
match self {
|
|
185
|
+
FileThumbnailSize::Small => "small",
|
|
186
|
+
FileThumbnailSize::Medium => "medium",
|
|
187
|
+
FileThumbnailSize::Large => "large",
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
#[derive(Subcommand)]
|
|
162
193
|
enum ToolCommands {
|
|
163
194
|
List(ToolListArgs),
|
|
164
195
|
Register(ToolRegisterArgs),
|
|
196
|
+
Publish(ToolPublishArgs),
|
|
197
|
+
Enable(ToolEnableArgs),
|
|
198
|
+
Disable(ToolDisableArgs),
|
|
199
|
+
Context {
|
|
200
|
+
#[command(subcommand)]
|
|
201
|
+
command: ToolContextCommands,
|
|
202
|
+
},
|
|
203
|
+
Prompt(ToolPromptArgs),
|
|
204
|
+
Run(ToolRunArgs),
|
|
205
|
+
Runs(ToolRunsArgs),
|
|
206
|
+
GetRun(ToolGetRunArgs),
|
|
207
|
+
RunEvents(ToolRunEventsArgs),
|
|
208
|
+
TraceStatus(ToolTraceStatusArgs),
|
|
209
|
+
Local {
|
|
210
|
+
#[command(subcommand)]
|
|
211
|
+
command: ToolLocalCommands,
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[derive(Subcommand)]
|
|
216
|
+
enum ToolLocalCommands {
|
|
217
|
+
Catalog(ToolLocalCatalogArgs),
|
|
218
|
+
Install(ToolLocalInstallArgs),
|
|
219
|
+
CompleteRun(ToolLocalCompleteRunArgs),
|
|
220
|
+
}
|
|
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),
|
|
165
233
|
Enable(ToolEnableArgs),
|
|
166
234
|
Disable(ToolDisableArgs),
|
|
235
|
+
Context {
|
|
236
|
+
#[command(subcommand)]
|
|
237
|
+
command: ToolContextCommands,
|
|
238
|
+
},
|
|
239
|
+
Prompt(ToolPromptArgs),
|
|
167
240
|
Run(ToolRunArgs),
|
|
168
241
|
Runs(ToolRunsArgs),
|
|
169
242
|
GetRun(ToolGetRunArgs),
|
|
243
|
+
RunEvents(ToolRunEventsArgs),
|
|
244
|
+
TraceStatus(ToolTraceStatusArgs),
|
|
245
|
+
Local {
|
|
246
|
+
#[command(subcommand)]
|
|
247
|
+
command: ToolLocalCommands,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[derive(Subcommand)]
|
|
252
|
+
enum LogsCommands {
|
|
253
|
+
Status,
|
|
254
|
+
Consent(LogsConsentArgs),
|
|
255
|
+
Tail(LogsTailArgs),
|
|
256
|
+
Upload(LogsUploadArgs),
|
|
170
257
|
}
|
|
171
258
|
|
|
172
259
|
#[derive(Args)]
|
|
@@ -200,6 +287,12 @@ struct FileListArgs {
|
|
|
200
287
|
#[arg(long)]
|
|
201
288
|
path: Option<String>,
|
|
202
289
|
#[arg(long)]
|
|
290
|
+
offset: Option<u32>,
|
|
291
|
+
#[arg(long)]
|
|
292
|
+
limit: Option<u32>,
|
|
293
|
+
#[arg(long)]
|
|
294
|
+
include_folder_markers: Option<bool>,
|
|
295
|
+
#[arg(long)]
|
|
203
296
|
base_url: Option<String>,
|
|
204
297
|
}
|
|
205
298
|
|
|
@@ -211,6 +304,14 @@ struct FileGetArgs {
|
|
|
211
304
|
base_url: Option<String>,
|
|
212
305
|
}
|
|
213
306
|
|
|
307
|
+
#[derive(Args)]
|
|
308
|
+
struct FileTreeArgs {
|
|
309
|
+
#[arg(long)]
|
|
310
|
+
project_id: String,
|
|
311
|
+
#[arg(long)]
|
|
312
|
+
base_url: Option<String>,
|
|
313
|
+
}
|
|
314
|
+
|
|
214
315
|
#[derive(Args)]
|
|
215
316
|
struct FileStatArgs {
|
|
216
317
|
#[arg(long)]
|
|
@@ -219,6 +320,18 @@ struct FileStatArgs {
|
|
|
219
320
|
base_url: Option<String>,
|
|
220
321
|
}
|
|
221
322
|
|
|
323
|
+
#[derive(Args)]
|
|
324
|
+
struct FileThumbnailArgs {
|
|
325
|
+
#[arg(long)]
|
|
326
|
+
asset_id: String,
|
|
327
|
+
#[arg(long, value_enum, default_value_t = FileThumbnailSize::Medium)]
|
|
328
|
+
size: FileThumbnailSize,
|
|
329
|
+
#[arg(long = "output")]
|
|
330
|
+
output_path: Option<PathBuf>,
|
|
331
|
+
#[arg(long)]
|
|
332
|
+
base_url: Option<String>,
|
|
333
|
+
}
|
|
334
|
+
|
|
222
335
|
#[derive(Args)]
|
|
223
336
|
struct FileDownloadArgs {
|
|
224
337
|
#[arg(long)]
|
|
@@ -256,6 +369,8 @@ struct FileMkdirArgs {
|
|
|
256
369
|
project_id: String,
|
|
257
370
|
#[arg(long)]
|
|
258
371
|
path: String,
|
|
372
|
+
#[arg(long, default_value = "private")]
|
|
373
|
+
visibility: String,
|
|
259
374
|
#[arg(long)]
|
|
260
375
|
base_url: Option<String>,
|
|
261
376
|
}
|
|
@@ -270,6 +385,48 @@ struct FileMoveArgs {
|
|
|
270
385
|
base_url: Option<String>,
|
|
271
386
|
}
|
|
272
387
|
|
|
388
|
+
#[derive(Args)]
|
|
389
|
+
struct FileSetArgs {
|
|
390
|
+
#[arg(long)]
|
|
391
|
+
asset_id: String,
|
|
392
|
+
#[arg(long)]
|
|
393
|
+
file_name: Option<String>,
|
|
394
|
+
#[arg(long)]
|
|
395
|
+
asset_type: Option<String>,
|
|
396
|
+
#[arg(long)]
|
|
397
|
+
visibility: Option<String>,
|
|
398
|
+
#[arg(long)]
|
|
399
|
+
base_url: Option<String>,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#[derive(Args)]
|
|
403
|
+
struct FileMoveFolderArgs {
|
|
404
|
+
#[arg(long)]
|
|
405
|
+
project_id: String,
|
|
406
|
+
#[arg(long)]
|
|
407
|
+
source_path: String,
|
|
408
|
+
#[arg(long, default_value = "")]
|
|
409
|
+
target_path: String,
|
|
410
|
+
#[arg(long)]
|
|
411
|
+
base_url: Option<String>,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[derive(Args)]
|
|
415
|
+
struct FileRemoveFolderArgs {
|
|
416
|
+
#[arg(long)]
|
|
417
|
+
project_id: String,
|
|
418
|
+
#[arg(long)]
|
|
419
|
+
path: String,
|
|
420
|
+
#[arg(long, default_value_t = false)]
|
|
421
|
+
recursive: bool,
|
|
422
|
+
#[arg(long, default_value_t = false)]
|
|
423
|
+
dry_run: bool,
|
|
424
|
+
#[arg(long, default_value_t = true)]
|
|
425
|
+
include_folder_markers: bool,
|
|
426
|
+
#[arg(long)]
|
|
427
|
+
base_url: Option<String>,
|
|
428
|
+
}
|
|
429
|
+
|
|
273
430
|
#[derive(Args)]
|
|
274
431
|
struct FileRemoveArgs {
|
|
275
432
|
#[arg(long)]
|
|
@@ -314,6 +471,20 @@ struct ToolEnableArgs {
|
|
|
314
471
|
base_url: Option<String>,
|
|
315
472
|
}
|
|
316
473
|
|
|
474
|
+
#[derive(Args)]
|
|
475
|
+
struct ToolPublishArgs {
|
|
476
|
+
#[arg(long)]
|
|
477
|
+
tool_id: String,
|
|
478
|
+
#[arg(long)]
|
|
479
|
+
channel: Option<String>,
|
|
480
|
+
#[arg(long)]
|
|
481
|
+
visibility: Option<String>,
|
|
482
|
+
#[arg(long)]
|
|
483
|
+
notes: Option<String>,
|
|
484
|
+
#[arg(long)]
|
|
485
|
+
base_url: Option<String>,
|
|
486
|
+
}
|
|
487
|
+
|
|
317
488
|
#[derive(Args)]
|
|
318
489
|
struct ToolDisableArgs {
|
|
319
490
|
#[arg(long)]
|
|
@@ -346,6 +517,46 @@ struct ToolRunArgs {
|
|
|
346
517
|
metadata_file: Option<PathBuf>,
|
|
347
518
|
#[arg(long, help = "Idempotency key for deduplicating retries")]
|
|
348
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,
|
|
349
560
|
#[arg(long)]
|
|
350
561
|
base_url: Option<String>,
|
|
351
562
|
}
|
|
@@ -372,6 +583,152 @@ struct ToolGetRunArgs {
|
|
|
372
583
|
base_url: Option<String>,
|
|
373
584
|
}
|
|
374
585
|
|
|
586
|
+
#[derive(Args)]
|
|
587
|
+
struct ToolRunEventsArgs {
|
|
588
|
+
#[arg(long)]
|
|
589
|
+
run_id: String,
|
|
590
|
+
#[arg(long)]
|
|
591
|
+
limit: Option<u32>,
|
|
592
|
+
#[arg(long)]
|
|
593
|
+
status: Option<String>,
|
|
594
|
+
#[arg(long)]
|
|
595
|
+
stage_prefix: Option<String>,
|
|
596
|
+
#[arg(long, help = "Only include events created after this ISO-8601 timestamp")]
|
|
597
|
+
since: Option<String>,
|
|
598
|
+
#[arg(long, help = "Only include events created at/before this ISO-8601 timestamp")]
|
|
599
|
+
until: Option<String>,
|
|
600
|
+
#[arg(long)]
|
|
601
|
+
base_url: Option<String>,
|
|
602
|
+
}
|
|
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
|
+
|
|
642
|
+
#[derive(Args)]
|
|
643
|
+
struct ToolLocalCatalogArgs {
|
|
644
|
+
#[arg(long)]
|
|
645
|
+
org_id: Option<String>,
|
|
646
|
+
#[arg(long)]
|
|
647
|
+
project_id: Option<String>,
|
|
648
|
+
#[arg(long)]
|
|
649
|
+
platform: Option<String>,
|
|
650
|
+
#[arg(long)]
|
|
651
|
+
arch: Option<String>,
|
|
652
|
+
#[arg(long)]
|
|
653
|
+
base_url: Option<String>,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#[derive(Args)]
|
|
657
|
+
struct ToolLocalInstallArgs {
|
|
658
|
+
#[arg(long)]
|
|
659
|
+
tool_id: String,
|
|
660
|
+
#[arg(long)]
|
|
661
|
+
org_id: Option<String>,
|
|
662
|
+
#[arg(long)]
|
|
663
|
+
project_id: Option<String>,
|
|
664
|
+
#[arg(long)]
|
|
665
|
+
platform: Option<String>,
|
|
666
|
+
#[arg(long)]
|
|
667
|
+
arch: Option<String>,
|
|
668
|
+
#[arg(long)]
|
|
669
|
+
version: Option<String>,
|
|
670
|
+
#[arg(long = "output")]
|
|
671
|
+
output_path: Option<PathBuf>,
|
|
672
|
+
#[arg(long, help = "Resume download from existing output file size using HTTP Range")]
|
|
673
|
+
resume: bool,
|
|
674
|
+
#[arg(long, help = "Only print install intent, do not download")]
|
|
675
|
+
no_download: bool,
|
|
676
|
+
#[arg(long)]
|
|
677
|
+
base_url: Option<String>,
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
#[derive(Args)]
|
|
681
|
+
struct ToolLocalCompleteRunArgs {
|
|
682
|
+
#[arg(long)]
|
|
683
|
+
run_id: String,
|
|
684
|
+
#[arg(long, default_value = "succeeded")]
|
|
685
|
+
status: String,
|
|
686
|
+
#[arg(long, help = "Path to JSON/JSONC file for run output")]
|
|
687
|
+
output_file: Option<PathBuf>,
|
|
688
|
+
#[arg(long, help = "Inline JSON object for run output")]
|
|
689
|
+
output_json: Option<String>,
|
|
690
|
+
#[arg(long)]
|
|
691
|
+
error_message: Option<String>,
|
|
692
|
+
#[arg(long, help = "Path to JSON/JSONC metadata file")]
|
|
693
|
+
metadata_file: Option<PathBuf>,
|
|
694
|
+
#[arg(long)]
|
|
695
|
+
base_url: Option<String>,
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#[derive(Args)]
|
|
699
|
+
struct LogsConsentArgs {
|
|
700
|
+
#[arg(long, default_value_t = false, conflicts_with = "disable")]
|
|
701
|
+
enable: bool,
|
|
702
|
+
#[arg(long, default_value_t = false, conflicts_with = "enable")]
|
|
703
|
+
disable: bool,
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
#[derive(Args)]
|
|
707
|
+
struct LogsTailArgs {
|
|
708
|
+
#[arg(long, default_value_t = 80)]
|
|
709
|
+
lines: usize,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#[derive(Args)]
|
|
713
|
+
struct LogsUploadArgs {
|
|
714
|
+
#[arg(long)]
|
|
715
|
+
project_id: String,
|
|
716
|
+
#[arg(long, action = ArgAction::Set, default_value_t = true)]
|
|
717
|
+
include_runtime: bool,
|
|
718
|
+
#[arg(long, action = ArgAction::Set, default_value_t = true)]
|
|
719
|
+
include_crash: bool,
|
|
720
|
+
#[arg(long)]
|
|
721
|
+
clear_on_success: bool,
|
|
722
|
+
#[arg(long, default_value = "other")]
|
|
723
|
+
asset_type: String,
|
|
724
|
+
#[arg(long, default_value = "private")]
|
|
725
|
+
visibility: String,
|
|
726
|
+
#[arg(long)]
|
|
727
|
+
dry_run: bool,
|
|
728
|
+
#[arg(long)]
|
|
729
|
+
base_url: Option<String>,
|
|
730
|
+
}
|
|
731
|
+
|
|
375
732
|
#[derive(Args)]
|
|
376
733
|
struct ProjectListArgs {
|
|
377
734
|
#[arg(long)]
|
|
@@ -813,6 +1170,12 @@ struct AssetRecord {
|
|
|
813
1170
|
#[derive(Debug, Serialize, Deserialize)]
|
|
814
1171
|
struct ListAssetsResponse {
|
|
815
1172
|
assets: Vec<AssetRecord>,
|
|
1173
|
+
#[serde(default)]
|
|
1174
|
+
total: Option<usize>,
|
|
1175
|
+
#[serde(default)]
|
|
1176
|
+
offset: Option<usize>,
|
|
1177
|
+
#[serde(default)]
|
|
1178
|
+
limit: Option<usize>,
|
|
816
1179
|
}
|
|
817
1180
|
|
|
818
1181
|
#[derive(Debug, Serialize, Deserialize)]
|
|
@@ -966,18 +1329,20 @@ fn resolve_config_root() -> Result<PathBuf> {
|
|
|
966
1329
|
}
|
|
967
1330
|
|
|
968
1331
|
fn config_path() -> Result<PathBuf> {
|
|
969
|
-
|
|
970
|
-
Ok(base.join(SESSION_DIR_NAME).join(SESSION_FILE_NAME))
|
|
1332
|
+
Ok(state_root_path()?.join(SESSION_FILE_NAME))
|
|
971
1333
|
}
|
|
972
1334
|
|
|
973
1335
|
fn update_cache_path() -> Result<PathBuf> {
|
|
974
|
-
|
|
975
|
-
Ok(base.join(SESSION_DIR_NAME).join(UPDATE_CACHE_FILE_NAME))
|
|
1336
|
+
Ok(state_root_path()?.join(UPDATE_CACHE_FILE_NAME))
|
|
976
1337
|
}
|
|
977
1338
|
|
|
978
1339
|
fn unreal_links_path() -> Result<PathBuf> {
|
|
1340
|
+
Ok(state_root_path()?.join(UNREAL_LINKS_FILE_NAME))
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
fn state_root_path() -> Result<PathBuf> {
|
|
979
1344
|
let base = resolve_config_root()?;
|
|
980
|
-
Ok(base.join(SESSION_DIR_NAME)
|
|
1345
|
+
Ok(base.join(SESSION_DIR_NAME))
|
|
981
1346
|
}
|
|
982
1347
|
|
|
983
1348
|
fn session_path_display() -> String {
|
|
@@ -1315,6 +1680,16 @@ async fn read_error_body(response: reqwest::Response) -> String {
|
|
|
1315
1680
|
}
|
|
1316
1681
|
}
|
|
1317
1682
|
|
|
1683
|
+
#[derive(Deserialize)]
|
|
1684
|
+
struct ApiErrorEnvelope {
|
|
1685
|
+
code: Option<String>,
|
|
1686
|
+
message: Option<String>,
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
fn parse_api_error(body: &str) -> Option<ApiErrorEnvelope> {
|
|
1690
|
+
serde_json::from_str::<ApiErrorEnvelope>(body).ok()
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1318
1693
|
fn with_cli_headers(request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
|
1319
1694
|
request
|
|
1320
1695
|
.header("x-reallink-client", "cli")
|
|
@@ -1831,42 +2206,282 @@ async fn logout_command(client: &reqwest::Client, output: OutputFormat) -> Resul
|
|
|
1831
2206
|
Ok(())
|
|
1832
2207
|
}
|
|
1833
2208
|
|
|
1834
|
-
|
|
1835
|
-
let
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2209
|
+
fn sanitize_cli_args(raw: &[String]) -> Vec<String> {
|
|
2210
|
+
let sensitive_flags = [
|
|
2211
|
+
"--input-json",
|
|
2212
|
+
"--input-file",
|
|
2213
|
+
"--metadata-file",
|
|
2214
|
+
"--token",
|
|
2215
|
+
"--refresh-token",
|
|
2216
|
+
"--password",
|
|
2217
|
+
"--secret",
|
|
2218
|
+
];
|
|
2219
|
+
let mut sanitized = Vec::with_capacity(raw.len());
|
|
2220
|
+
let mut redact_next = false;
|
|
2221
|
+
for value in raw {
|
|
2222
|
+
if redact_next {
|
|
2223
|
+
sanitized.push("<redacted>".to_string());
|
|
2224
|
+
redact_next = false;
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
if let Some((flag, _rest)) = value.split_once('=') {
|
|
2228
|
+
if sensitive_flags.iter().any(|candidate| candidate == &flag) {
|
|
2229
|
+
sanitized.push(format!("{}=<redacted>", flag));
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if sensitive_flags.iter().any(|candidate| candidate == value) {
|
|
2234
|
+
sanitized.push(value.clone());
|
|
2235
|
+
redact_next = true;
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
sanitized.push(value.clone());
|
|
1842
2239
|
}
|
|
1843
|
-
|
|
1844
|
-
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
1845
|
-
save_session(&session)?;
|
|
1846
|
-
Ok(())
|
|
2240
|
+
sanitized
|
|
1847
2241
|
}
|
|
1848
2242
|
|
|
1849
|
-
|
|
1850
|
-
let
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
return Err(anyhow!("token list failed: {}", body));
|
|
2243
|
+
fn command_summary_for_logs() -> String {
|
|
2244
|
+
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
2245
|
+
let sanitized = sanitize_cli_args(&args);
|
|
2246
|
+
if sanitized.is_empty() {
|
|
2247
|
+
"reallink".to_string()
|
|
2248
|
+
} else {
|
|
2249
|
+
format!("reallink {}", sanitized.join(" "))
|
|
1857
2250
|
}
|
|
1858
|
-
let payload: ListApiTokensResponse = response.json().await?;
|
|
1859
|
-
println!("{}", serde_json::to_string_pretty(&payload.tokens)?);
|
|
1860
|
-
save_session(&session)?;
|
|
1861
|
-
Ok(())
|
|
1862
2251
|
}
|
|
1863
2252
|
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2253
|
+
fn append_runtime_log_event(
|
|
2254
|
+
command: &str,
|
|
2255
|
+
message: &str,
|
|
2256
|
+
level: &str,
|
|
2257
|
+
duration_ms: Option<u128>,
|
|
2258
|
+
exit_code: Option<i32>,
|
|
2259
|
+
) {
|
|
2260
|
+
let state_root = match state_root_path() {
|
|
2261
|
+
Ok(path) => path,
|
|
2262
|
+
Err(_) => return,
|
|
2263
|
+
};
|
|
2264
|
+
let event = logs::RuntimeLogEvent {
|
|
2265
|
+
ts_epoch_ms: now_epoch_ms(),
|
|
2266
|
+
level: level.to_string(),
|
|
2267
|
+
command: command.to_string(),
|
|
2268
|
+
message: message.to_string(),
|
|
2269
|
+
duration_ms,
|
|
2270
|
+
exit_code,
|
|
2271
|
+
};
|
|
2272
|
+
let _ = logs::append_runtime_event(&state_root, &event);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
fn record_cli_crash_report(command: &str, error: &anyhow::Error) {
|
|
2276
|
+
let state_root = match state_root_path() {
|
|
2277
|
+
Ok(path) => path,
|
|
2278
|
+
Err(_) => return,
|
|
2279
|
+
};
|
|
2280
|
+
let report = logs::CrashReport {
|
|
2281
|
+
ts_epoch_ms: now_epoch_ms(),
|
|
2282
|
+
command: command.to_string(),
|
|
2283
|
+
message: error.to_string(),
|
|
2284
|
+
detail: Some(format!("{:#}", error)),
|
|
2285
|
+
};
|
|
2286
|
+
let _ = logs::write_crash_report(&state_root, &report);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
async fn logs_status_command(output: OutputFormat) -> Result<()> {
|
|
2290
|
+
let status = logs::status(&state_root_path()?)?;
|
|
2291
|
+
let payload = serde_json::to_value(&status)?;
|
|
2292
|
+
if output == OutputFormat::Text {
|
|
2293
|
+
println!(
|
|
2294
|
+
"Logs root: {}\nRuntime log: {}\nCrash dir: {}\nUpload consent: {}\nCrash reports: {}",
|
|
2295
|
+
status.logs_root,
|
|
2296
|
+
status.runtime_log_path,
|
|
2297
|
+
status.crash_dir,
|
|
2298
|
+
if status.consent.upload_enabled {
|
|
2299
|
+
"enabled"
|
|
2300
|
+
} else {
|
|
2301
|
+
"disabled"
|
|
2302
|
+
},
|
|
2303
|
+
status.crash_report_count
|
|
2304
|
+
);
|
|
2305
|
+
} else {
|
|
2306
|
+
print_json(&payload)?;
|
|
2307
|
+
}
|
|
2308
|
+
Ok(())
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
async fn logs_consent_command(args: LogsConsentArgs, output: OutputFormat) -> Result<()> {
|
|
2312
|
+
let state_root = state_root_path()?;
|
|
2313
|
+
let consent = if args.enable {
|
|
2314
|
+
logs::set_upload_consent(&state_root, true)?
|
|
2315
|
+
} else if args.disable {
|
|
2316
|
+
logs::set_upload_consent(&state_root, false)?
|
|
2317
|
+
} else {
|
|
2318
|
+
logs::load_consent(&state_root)?
|
|
2319
|
+
};
|
|
2320
|
+
let payload = serde_json::json!({
|
|
2321
|
+
"ok": true,
|
|
2322
|
+
"consent": consent
|
|
2323
|
+
});
|
|
2324
|
+
if output == OutputFormat::Text {
|
|
2325
|
+
println!(
|
|
2326
|
+
"Log upload consent is {}.",
|
|
2327
|
+
if consent.upload_enabled {
|
|
2328
|
+
"enabled"
|
|
2329
|
+
} else {
|
|
2330
|
+
"disabled"
|
|
2331
|
+
}
|
|
2332
|
+
);
|
|
2333
|
+
} else {
|
|
2334
|
+
print_json(&payload)?;
|
|
2335
|
+
}
|
|
2336
|
+
Ok(())
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
async fn logs_tail_command(args: LogsTailArgs, output: OutputFormat) -> Result<()> {
|
|
2340
|
+
let lines = logs::read_runtime_tail(&state_root_path()?, args.lines)?;
|
|
2341
|
+
if output == OutputFormat::Text {
|
|
2342
|
+
for line in lines {
|
|
2343
|
+
println!("{}", line);
|
|
2344
|
+
}
|
|
2345
|
+
} else {
|
|
2346
|
+
print_json(&serde_json::json!({
|
|
2347
|
+
"ok": true,
|
|
2348
|
+
"lines": lines
|
|
2349
|
+
}))?;
|
|
2350
|
+
}
|
|
2351
|
+
Ok(())
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
async fn logs_upload_command(
|
|
2355
|
+
client: &reqwest::Client,
|
|
2356
|
+
args: LogsUploadArgs,
|
|
2357
|
+
output: OutputFormat,
|
|
2358
|
+
) -> Result<()> {
|
|
2359
|
+
let state_root = state_root_path()?;
|
|
2360
|
+
let consent = logs::load_consent(&state_root)?;
|
|
2361
|
+
if !consent.upload_enabled {
|
|
2362
|
+
return Err(anyhow!(
|
|
2363
|
+
"Log upload consent is disabled. Run `reallink logs consent --enable` first."
|
|
2364
|
+
));
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
let candidates =
|
|
2368
|
+
logs::list_upload_candidates(&state_root, args.include_runtime, args.include_crash)?;
|
|
2369
|
+
if args.dry_run {
|
|
2370
|
+
let payload = serde_json::json!({
|
|
2371
|
+
"ok": true,
|
|
2372
|
+
"dryRun": true,
|
|
2373
|
+
"count": candidates.len(),
|
|
2374
|
+
"candidates": candidates
|
|
2375
|
+
});
|
|
2376
|
+
emit_text_or_json(
|
|
2377
|
+
output,
|
|
2378
|
+
&format!("Dry run complete. {} log files are ready to upload.", payload["count"]),
|
|
2379
|
+
payload,
|
|
2380
|
+
)?;
|
|
2381
|
+
return Ok(());
|
|
2382
|
+
}
|
|
2383
|
+
if candidates.is_empty() {
|
|
2384
|
+
let payload = serde_json::json!({
|
|
2385
|
+
"ok": true,
|
|
2386
|
+
"uploaded": [],
|
|
2387
|
+
"count": 0
|
|
2388
|
+
});
|
|
2389
|
+
emit_text_or_json(output, "No local log files to upload.", payload)?;
|
|
2390
|
+
return Ok(());
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
let mut session = load_session()?;
|
|
2394
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
2395
|
+
let mut uploaded = Vec::new();
|
|
2396
|
+
|
|
2397
|
+
for candidate in candidates {
|
|
2398
|
+
let bytes = fs::read(&candidate.local_path).with_context(|| {
|
|
2399
|
+
format!("Failed to read log file {}", candidate.local_path.display())
|
|
2400
|
+
})?;
|
|
2401
|
+
let asset = upload_asset_via_intent(
|
|
2402
|
+
client,
|
|
2403
|
+
&mut session,
|
|
2404
|
+
&args.project_id,
|
|
2405
|
+
&candidate.remote_path,
|
|
2406
|
+
bytes,
|
|
2407
|
+
&candidate.content_type,
|
|
2408
|
+
&args.asset_type,
|
|
2409
|
+
&args.visibility,
|
|
2410
|
+
)
|
|
2411
|
+
.await?;
|
|
2412
|
+
|
|
2413
|
+
if args.clear_on_success {
|
|
2414
|
+
let file_name = candidate
|
|
2415
|
+
.local_path
|
|
2416
|
+
.file_name()
|
|
2417
|
+
.and_then(|value| value.to_str())
|
|
2418
|
+
.unwrap_or_default();
|
|
2419
|
+
if file_name.eq_ignore_ascii_case("runtime.jsonl") {
|
|
2420
|
+
fs::write(&candidate.local_path, b"").with_context(|| {
|
|
2421
|
+
format!("Failed to clear runtime log {}", candidate.local_path.display())
|
|
2422
|
+
})?;
|
|
2423
|
+
} else {
|
|
2424
|
+
let _ = fs::remove_file(&candidate.local_path);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
uploaded.push(serde_json::json!({
|
|
2429
|
+
"localPath": candidate.local_path.display().to_string(),
|
|
2430
|
+
"remotePath": candidate.remote_path,
|
|
2431
|
+
"asset": asset
|
|
2432
|
+
}));
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
save_session(&session)?;
|
|
2436
|
+
let payload = serde_json::json!({
|
|
2437
|
+
"ok": true,
|
|
2438
|
+
"count": uploaded.len(),
|
|
2439
|
+
"uploaded": uploaded
|
|
2440
|
+
});
|
|
2441
|
+
emit_text_or_json(
|
|
2442
|
+
output,
|
|
2443
|
+
&format!("Uploaded {} log files.", payload["count"]),
|
|
2444
|
+
payload,
|
|
2445
|
+
)?;
|
|
2446
|
+
Ok(())
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
async fn whoami_command(client: &reqwest::Client, args: BaseArgs) -> Result<()> {
|
|
2450
|
+
let mut session = load_session()?;
|
|
2451
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
2452
|
+
|
|
2453
|
+
let response = authed_request(client, &mut session, Method::GET, "/auth/me", None).await?;
|
|
2454
|
+
if !response.status().is_success() {
|
|
2455
|
+
let body = read_error_body(response).await;
|
|
2456
|
+
return Err(anyhow!("whoami failed: {}", body));
|
|
2457
|
+
}
|
|
2458
|
+
let payload: serde_json::Value = response.json().await?;
|
|
2459
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
2460
|
+
save_session(&session)?;
|
|
2461
|
+
Ok(())
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
async fn token_list_command(client: &reqwest::Client, args: BaseArgs) -> Result<()> {
|
|
2465
|
+
let mut session = load_session()?;
|
|
2466
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
2467
|
+
|
|
2468
|
+
let response = authed_request(client, &mut session, Method::GET, "/auth/tokens", None).await?;
|
|
2469
|
+
if !response.status().is_success() {
|
|
2470
|
+
let body = read_error_body(response).await;
|
|
2471
|
+
return Err(anyhow!("token list failed: {}", body));
|
|
2472
|
+
}
|
|
2473
|
+
let payload: ListApiTokensResponse = response.json().await?;
|
|
2474
|
+
println!("{}", serde_json::to_string_pretty(&payload.tokens)?);
|
|
2475
|
+
save_session(&session)?;
|
|
2476
|
+
Ok(())
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
async fn token_create_command(client: &reqwest::Client, args: TokenCreateArgs) -> Result<()> {
|
|
2480
|
+
let mut session = load_session()?;
|
|
2481
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
2482
|
+
|
|
2483
|
+
let scopes = if args.scope.is_empty() {
|
|
2484
|
+
return Err(anyhow!("At least one --scope must be provided"));
|
|
1870
2485
|
} else {
|
|
1871
2486
|
args.scope
|
|
1872
2487
|
};
|
|
@@ -2017,6 +2632,27 @@ async fn org_invites_command(client: &reqwest::Client, args: OrgInvitesArgs) ->
|
|
|
2017
2632
|
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
2018
2633
|
if !response.status().is_success() {
|
|
2019
2634
|
let body = read_error_body(response).await;
|
|
2635
|
+
if let Some(api_error) = parse_api_error(&body) {
|
|
2636
|
+
match api_error.code.as_deref() {
|
|
2637
|
+
Some("CLERK_ORG_NOT_LINKED") => {
|
|
2638
|
+
return Err(anyhow!(
|
|
2639
|
+
"org invites unavailable: {}. Action: create this organization from a Clerk-authenticated session, then retry.",
|
|
2640
|
+
api_error
|
|
2641
|
+
.message
|
|
2642
|
+
.unwrap_or_else(|| "organization is not linked to Clerk".to_string())
|
|
2643
|
+
));
|
|
2644
|
+
}
|
|
2645
|
+
Some("CLERK_NOT_CONFIGURED") => {
|
|
2646
|
+
return Err(anyhow!(
|
|
2647
|
+
"org invites unavailable: {}. Action: configure CLERK_SECRET_KEY on the API deployment.",
|
|
2648
|
+
api_error
|
|
2649
|
+
.message
|
|
2650
|
+
.unwrap_or_else(|| "Clerk is not configured".to_string())
|
|
2651
|
+
));
|
|
2652
|
+
}
|
|
2653
|
+
_ => {}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2020
2656
|
return Err(anyhow!("org invites failed: {}", body));
|
|
2021
2657
|
}
|
|
2022
2658
|
let payload: ListOrgInvitesResponse = response.json().await?;
|
|
@@ -2052,6 +2688,35 @@ async fn org_invite_command(client: &reqwest::Client, args: OrgInviteArgs) -> Re
|
|
|
2052
2688
|
.await?;
|
|
2053
2689
|
if !response.status().is_success() {
|
|
2054
2690
|
let body = read_error_body(response).await;
|
|
2691
|
+
if let Some(api_error) = parse_api_error(&body) {
|
|
2692
|
+
match api_error.code.as_deref() {
|
|
2693
|
+
Some("CLERK_USER_NOT_LINKED") => {
|
|
2694
|
+
return Err(anyhow!(
|
|
2695
|
+
"org invite unavailable: {}. Action: login via Clerk on the web once, then rerun this command.",
|
|
2696
|
+
api_error
|
|
2697
|
+
.message
|
|
2698
|
+
.unwrap_or_else(|| "current user is not linked to Clerk".to_string())
|
|
2699
|
+
));
|
|
2700
|
+
}
|
|
2701
|
+
Some("CLERK_ORG_NOT_LINKED") => {
|
|
2702
|
+
return Err(anyhow!(
|
|
2703
|
+
"org invite unavailable: {}. Action: create this organization from a Clerk-authenticated session, then retry.",
|
|
2704
|
+
api_error
|
|
2705
|
+
.message
|
|
2706
|
+
.unwrap_or_else(|| "organization is not linked to Clerk".to_string())
|
|
2707
|
+
));
|
|
2708
|
+
}
|
|
2709
|
+
Some("CLERK_NOT_CONFIGURED") => {
|
|
2710
|
+
return Err(anyhow!(
|
|
2711
|
+
"org invite unavailable: {}. Action: configure CLERK_SECRET_KEY on the API deployment.",
|
|
2712
|
+
api_error
|
|
2713
|
+
.message
|
|
2714
|
+
.unwrap_or_else(|| "Clerk is not configured".to_string())
|
|
2715
|
+
));
|
|
2716
|
+
}
|
|
2717
|
+
_ => {}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2055
2720
|
return Err(anyhow!("org invite failed: {}", body));
|
|
2056
2721
|
}
|
|
2057
2722
|
let payload: OrgInviteResponse = response.json().await?;
|
|
@@ -2169,16 +2834,22 @@ async fn project_create_command(client: &reqwest::Client, args: ProjectCreateArg
|
|
|
2169
2834
|
let mut session = load_session()?;
|
|
2170
2835
|
apply_base_url_override(&mut session, args.base_url);
|
|
2171
2836
|
|
|
2837
|
+
let mut body = serde_json::Map::new();
|
|
2838
|
+
body.insert("orgId".to_string(), serde_json::Value::String(args.org_id));
|
|
2839
|
+
body.insert("name".to_string(), serde_json::Value::String(args.name));
|
|
2840
|
+
if let Some(description) = args.description {
|
|
2841
|
+
body.insert(
|
|
2842
|
+
"description".to_string(),
|
|
2843
|
+
serde_json::Value::String(description),
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2172
2847
|
let response = authed_request(
|
|
2173
2848
|
client,
|
|
2174
2849
|
&mut session,
|
|
2175
2850
|
Method::POST,
|
|
2176
2851
|
"/core/projects",
|
|
2177
|
-
Some(serde_json::
|
|
2178
|
-
"orgId": args.org_id,
|
|
2179
|
-
"name": args.name,
|
|
2180
|
-
"description": args.description
|
|
2181
|
-
})),
|
|
2852
|
+
Some(serde_json::Value::Object(body)),
|
|
2182
2853
|
)
|
|
2183
2854
|
.await?;
|
|
2184
2855
|
if !response.status().is_success() {
|
|
@@ -2380,6 +3051,23 @@ fn file_name_component(path: &str) -> String {
|
|
|
2380
3051
|
.to_string()
|
|
2381
3052
|
}
|
|
2382
3053
|
|
|
3054
|
+
fn detect_local_runtime_platform_arch() -> (String, String) {
|
|
3055
|
+
let platform = match std::env::consts::OS {
|
|
3056
|
+
"windows" => "win32",
|
|
3057
|
+
"macos" => "darwin",
|
|
3058
|
+
"linux" => "linux",
|
|
3059
|
+
other => other,
|
|
3060
|
+
}
|
|
3061
|
+
.to_string();
|
|
3062
|
+
let arch = match std::env::consts::ARCH {
|
|
3063
|
+
"x86_64" => "x64",
|
|
3064
|
+
"aarch64" => "arm64",
|
|
3065
|
+
other => other,
|
|
3066
|
+
}
|
|
3067
|
+
.to_string();
|
|
3068
|
+
(platform, arch)
|
|
3069
|
+
}
|
|
3070
|
+
|
|
2383
3071
|
fn build_unreal_link_manifest_payload(
|
|
2384
3072
|
link: &UnrealLinkRecord,
|
|
2385
3073
|
include_local_paths: bool,
|
|
@@ -3685,29 +4373,51 @@ async fn file_list_command(client: &reqwest::Client, args: FileListArgs) -> Resu
|
|
|
3685
4373
|
let mut session = load_session()?;
|
|
3686
4374
|
apply_base_url_override(&mut session, args.base_url);
|
|
3687
4375
|
|
|
3688
|
-
let
|
|
3689
|
-
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
3690
|
-
if !response.status().is_success() {
|
|
3691
|
-
let body = read_error_body(response).await;
|
|
3692
|
-
return Err(anyhow!("file list failed: {}", body));
|
|
3693
|
-
}
|
|
3694
|
-
let mut payload: ListAssetsResponse = response.json().await?;
|
|
4376
|
+
let mut query_parts = vec![format!("projectId={}", args.project_id)];
|
|
3695
4377
|
if let Some(prefix) = args.path {
|
|
3696
4378
|
let cleaned = clean_virtual_path(&prefix);
|
|
3697
4379
|
if !cleaned.is_empty() {
|
|
3698
|
-
|
|
3699
|
-
payload.assets = payload
|
|
3700
|
-
.assets
|
|
3701
|
-
.into_iter()
|
|
3702
|
-
.filter(|asset| asset.file_name == cleaned || asset.file_name.starts_with(&strict))
|
|
3703
|
-
.collect();
|
|
4380
|
+
query_parts.push(format!("path={}", cleaned));
|
|
3704
4381
|
}
|
|
3705
4382
|
}
|
|
4383
|
+
if let Some(offset) = args.offset {
|
|
4384
|
+
query_parts.push(format!("offset={}", offset));
|
|
4385
|
+
}
|
|
4386
|
+
if let Some(limit) = args.limit {
|
|
4387
|
+
query_parts.push(format!("limit={}", limit));
|
|
4388
|
+
}
|
|
4389
|
+
if let Some(include_folder_markers) = args.include_folder_markers {
|
|
4390
|
+
query_parts.push(format!("includeFolderMarkers={}", include_folder_markers));
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
let path = format!("/assets?{}", query_parts.join("&"));
|
|
4394
|
+
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
4395
|
+
if !response.status().is_success() {
|
|
4396
|
+
let body = read_error_body(response).await;
|
|
4397
|
+
return Err(anyhow!("file list failed: {}", body));
|
|
4398
|
+
}
|
|
4399
|
+
let payload: ListAssetsResponse = response.json().await?;
|
|
3706
4400
|
println!("{}", serde_json::to_string_pretty(&payload.assets)?);
|
|
3707
4401
|
save_session(&session)?;
|
|
3708
4402
|
Ok(())
|
|
3709
4403
|
}
|
|
3710
4404
|
|
|
4405
|
+
async fn file_tree_command(client: &reqwest::Client, args: FileTreeArgs) -> Result<()> {
|
|
4406
|
+
let mut session = load_session()?;
|
|
4407
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
4408
|
+
|
|
4409
|
+
let path = format!("/assets/tree?projectId={}", args.project_id);
|
|
4410
|
+
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
4411
|
+
if !response.status().is_success() {
|
|
4412
|
+
let body = read_error_body(response).await;
|
|
4413
|
+
return Err(anyhow!("file tree failed: {}", body));
|
|
4414
|
+
}
|
|
4415
|
+
let payload: serde_json::Value = response.json().await?;
|
|
4416
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
4417
|
+
save_session(&session)?;
|
|
4418
|
+
Ok(())
|
|
4419
|
+
}
|
|
4420
|
+
|
|
3711
4421
|
async fn file_get_command(client: &reqwest::Client, args: FileGetArgs) -> Result<()> {
|
|
3712
4422
|
let mut session = load_session()?;
|
|
3713
4423
|
apply_base_url_override(&mut session, args.base_url);
|
|
@@ -3740,6 +4450,77 @@ async fn file_stat_command(client: &reqwest::Client, args: FileStatArgs) -> Resu
|
|
|
3740
4450
|
Ok(())
|
|
3741
4451
|
}
|
|
3742
4452
|
|
|
4453
|
+
fn infer_thumbnail_extension(content_type: Option<&str>) -> &'static str {
|
|
4454
|
+
let normalized = content_type.unwrap_or("").to_ascii_lowercase();
|
|
4455
|
+
if normalized.contains("image/png") {
|
|
4456
|
+
"png"
|
|
4457
|
+
} else if normalized.contains("image/jpeg") || normalized.contains("image/jpg") {
|
|
4458
|
+
"jpg"
|
|
4459
|
+
} else if normalized.contains("image/webp") {
|
|
4460
|
+
"webp"
|
|
4461
|
+
} else if normalized.contains("image/svg+xml") {
|
|
4462
|
+
"svg"
|
|
4463
|
+
} else {
|
|
4464
|
+
"bin"
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
async fn file_thumbnail_command(client: &reqwest::Client, args: FileThumbnailArgs) -> Result<()> {
|
|
4469
|
+
let mut session = load_session()?;
|
|
4470
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
4471
|
+
|
|
4472
|
+
let size = args.size.as_str();
|
|
4473
|
+
let path = format!("/assets/{}/thumbnail/{}", args.asset_id, size);
|
|
4474
|
+
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
4475
|
+
if !response.status().is_success() {
|
|
4476
|
+
let body = read_error_body(response).await;
|
|
4477
|
+
return Err(anyhow!("file thumbnail failed: {}", body));
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
let content_type = response
|
|
4481
|
+
.headers()
|
|
4482
|
+
.get("content-type")
|
|
4483
|
+
.and_then(|value| value.to_str().ok())
|
|
4484
|
+
.map(|value| value.to_string());
|
|
4485
|
+
let bytes = response.bytes().await?;
|
|
4486
|
+
let output_path = args.output_path.unwrap_or_else(|| {
|
|
4487
|
+
PathBuf::from(format!(
|
|
4488
|
+
"{}-{}.{}",
|
|
4489
|
+
args.asset_id,
|
|
4490
|
+
size,
|
|
4491
|
+
infer_thumbnail_extension(content_type.as_deref())
|
|
4492
|
+
))
|
|
4493
|
+
});
|
|
4494
|
+
if let Some(parent) = output_path.parent() {
|
|
4495
|
+
if !parent.as_os_str().is_empty() {
|
|
4496
|
+
tokio_fs::create_dir_all(parent).await.with_context(|| {
|
|
4497
|
+
format!(
|
|
4498
|
+
"Failed to create output directory {}",
|
|
4499
|
+
parent.display()
|
|
4500
|
+
)
|
|
4501
|
+
})?;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
tokio_fs::write(&output_path, &bytes)
|
|
4505
|
+
.await
|
|
4506
|
+
.with_context(|| format!("Failed to write thumbnail {}", output_path.display()))?;
|
|
4507
|
+
|
|
4508
|
+
println!(
|
|
4509
|
+
"{}",
|
|
4510
|
+
serde_json::to_string_pretty(&serde_json::json!({
|
|
4511
|
+
"ok": true,
|
|
4512
|
+
"assetId": args.asset_id,
|
|
4513
|
+
"size": size,
|
|
4514
|
+
"output": output_path.display().to_string(),
|
|
4515
|
+
"bytesWritten": bytes.len(),
|
|
4516
|
+
"contentType": content_type
|
|
4517
|
+
}))?
|
|
4518
|
+
);
|
|
4519
|
+
|
|
4520
|
+
save_session(&session)?;
|
|
4521
|
+
Ok(())
|
|
4522
|
+
}
|
|
4523
|
+
|
|
3743
4524
|
async fn file_download_command(client: &reqwest::Client, args: FileDownloadArgs) -> Result<()> {
|
|
3744
4525
|
let mut session = load_session()?;
|
|
3745
4526
|
apply_base_url_override(&mut session, args.base_url);
|
|
@@ -3902,20 +4683,24 @@ async fn file_mkdir_command(client: &reqwest::Client, args: FileMkdirArgs) -> Re
|
|
|
3902
4683
|
if folder.is_empty() {
|
|
3903
4684
|
return Err(anyhow!("folder path is empty"));
|
|
3904
4685
|
}
|
|
3905
|
-
let
|
|
3906
|
-
let marker_bytes = format!("folder marker {}\n", now_epoch_ms()).into_bytes();
|
|
3907
|
-
let asset = upload_asset_via_intent(
|
|
4686
|
+
let response = authed_request(
|
|
3908
4687
|
client,
|
|
3909
4688
|
&mut session,
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4689
|
+
Method::POST,
|
|
4690
|
+
"/assets/folder",
|
|
4691
|
+
Some(serde_json::json!({
|
|
4692
|
+
"projectId": args.project_id,
|
|
4693
|
+
"path": folder,
|
|
4694
|
+
"visibility": args.visibility
|
|
4695
|
+
})),
|
|
3916
4696
|
)
|
|
3917
4697
|
.await?;
|
|
3918
|
-
|
|
4698
|
+
if !response.status().is_success() {
|
|
4699
|
+
let body = read_error_body(response).await;
|
|
4700
|
+
return Err(anyhow!("file mkdir failed: {}", body));
|
|
4701
|
+
}
|
|
4702
|
+
let payload: serde_json::Value = response.json().await?;
|
|
4703
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
3919
4704
|
save_session(&session)?;
|
|
3920
4705
|
Ok(())
|
|
3921
4706
|
}
|
|
@@ -3949,27 +4734,139 @@ async fn file_move_command(client: &reqwest::Client, args: FileMoveArgs) -> Resu
|
|
|
3949
4734
|
Ok(())
|
|
3950
4735
|
}
|
|
3951
4736
|
|
|
3952
|
-
async fn
|
|
4737
|
+
async fn file_set_command(client: &reqwest::Client, args: FileSetArgs) -> Result<()> {
|
|
3953
4738
|
let mut session = load_session()?;
|
|
3954
4739
|
apply_base_url_override(&mut session, args.base_url);
|
|
3955
4740
|
|
|
4741
|
+
if args.file_name.is_none() && args.asset_type.is_none() && args.visibility.is_none() {
|
|
4742
|
+
return Err(anyhow!(
|
|
4743
|
+
"At least one of --file-name, --asset-type, or --visibility is required"
|
|
4744
|
+
));
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
let mut body = serde_json::Map::new();
|
|
4748
|
+
if let Some(file_name) = args.file_name {
|
|
4749
|
+
let normalized = clean_virtual_path(&file_name);
|
|
4750
|
+
if normalized.is_empty() {
|
|
4751
|
+
return Err(anyhow!("--file-name resolved to an empty path"));
|
|
4752
|
+
}
|
|
4753
|
+
body.insert(
|
|
4754
|
+
"fileName".to_string(),
|
|
4755
|
+
serde_json::Value::String(normalized),
|
|
4756
|
+
);
|
|
4757
|
+
}
|
|
4758
|
+
if let Some(asset_type) = args.asset_type {
|
|
4759
|
+
body.insert("assetType".to_string(), serde_json::Value::String(asset_type));
|
|
4760
|
+
}
|
|
4761
|
+
if let Some(visibility) = args.visibility {
|
|
4762
|
+
body.insert("visibility".to_string(), serde_json::Value::String(visibility));
|
|
4763
|
+
}
|
|
4764
|
+
|
|
3956
4765
|
let path = format!("/assets/{}", args.asset_id);
|
|
3957
|
-
let response = authed_request(
|
|
4766
|
+
let response = authed_request(
|
|
4767
|
+
client,
|
|
4768
|
+
&mut session,
|
|
4769
|
+
Method::PATCH,
|
|
4770
|
+
&path,
|
|
4771
|
+
Some(serde_json::Value::Object(body)),
|
|
4772
|
+
)
|
|
4773
|
+
.await?;
|
|
3958
4774
|
if !response.status().is_success() {
|
|
3959
4775
|
let body = read_error_body(response).await;
|
|
3960
|
-
return Err(anyhow!("file
|
|
4776
|
+
return Err(anyhow!("file set failed: {}", body));
|
|
3961
4777
|
}
|
|
3962
|
-
let payload:
|
|
3963
|
-
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
4778
|
+
let payload: AssetResponse = response.json().await?;
|
|
4779
|
+
println!("{}", serde_json::to_string_pretty(&payload.asset)?);
|
|
3964
4780
|
save_session(&session)?;
|
|
3965
4781
|
Ok(())
|
|
3966
4782
|
}
|
|
3967
4783
|
|
|
3968
|
-
async fn
|
|
4784
|
+
async fn file_move_folder_command(client: &reqwest::Client, args: FileMoveFolderArgs) -> Result<()> {
|
|
3969
4785
|
let mut session = load_session()?;
|
|
3970
4786
|
apply_base_url_override(&mut session, args.base_url);
|
|
3971
4787
|
|
|
3972
|
-
let
|
|
4788
|
+
let source_path = clean_virtual_path(&args.source_path);
|
|
4789
|
+
if source_path.is_empty() {
|
|
4790
|
+
return Err(anyhow!("source_path is empty"));
|
|
4791
|
+
}
|
|
4792
|
+
let target_path = clean_virtual_path(&args.target_path);
|
|
4793
|
+
|
|
4794
|
+
let response = authed_request(
|
|
4795
|
+
client,
|
|
4796
|
+
&mut session,
|
|
4797
|
+
Method::POST,
|
|
4798
|
+
"/assets/folder/move",
|
|
4799
|
+
Some(serde_json::json!({
|
|
4800
|
+
"projectId": args.project_id,
|
|
4801
|
+
"sourcePath": source_path,
|
|
4802
|
+
"targetPath": target_path
|
|
4803
|
+
})),
|
|
4804
|
+
)
|
|
4805
|
+
.await?;
|
|
4806
|
+
if !response.status().is_success() {
|
|
4807
|
+
let body = read_error_body(response).await;
|
|
4808
|
+
return Err(anyhow!("file move-folder failed: {}", body));
|
|
4809
|
+
}
|
|
4810
|
+
let payload: serde_json::Value = response.json().await?;
|
|
4811
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
4812
|
+
save_session(&session)?;
|
|
4813
|
+
Ok(())
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4816
|
+
async fn file_rmdir_command(client: &reqwest::Client, args: FileRemoveFolderArgs) -> Result<()> {
|
|
4817
|
+
let mut session = load_session()?;
|
|
4818
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
4819
|
+
|
|
4820
|
+
let folder_path = clean_virtual_path(&args.path);
|
|
4821
|
+
if folder_path.is_empty() {
|
|
4822
|
+
return Err(anyhow!("path is empty"));
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
let response = authed_request(
|
|
4826
|
+
client,
|
|
4827
|
+
&mut session,
|
|
4828
|
+
Method::POST,
|
|
4829
|
+
"/assets/folder/delete",
|
|
4830
|
+
Some(serde_json::json!({
|
|
4831
|
+
"projectId": args.project_id,
|
|
4832
|
+
"path": folder_path,
|
|
4833
|
+
"recursive": args.recursive,
|
|
4834
|
+
"dryRun": args.dry_run,
|
|
4835
|
+
"includeFolderMarkers": args.include_folder_markers
|
|
4836
|
+
})),
|
|
4837
|
+
)
|
|
4838
|
+
.await?;
|
|
4839
|
+
if !response.status().is_success() {
|
|
4840
|
+
let body = read_error_body(response).await;
|
|
4841
|
+
return Err(anyhow!("file rmdir failed: {}", body));
|
|
4842
|
+
}
|
|
4843
|
+
let payload: serde_json::Value = response.json().await?;
|
|
4844
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
4845
|
+
save_session(&session)?;
|
|
4846
|
+
Ok(())
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
async fn file_remove_command(client: &reqwest::Client, args: FileRemoveArgs) -> Result<()> {
|
|
4850
|
+
let mut session = load_session()?;
|
|
4851
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
4852
|
+
|
|
4853
|
+
let path = format!("/assets/{}", args.asset_id);
|
|
4854
|
+
let response = authed_request(client, &mut session, Method::DELETE, &path, None).await?;
|
|
4855
|
+
if !response.status().is_success() {
|
|
4856
|
+
let body = read_error_body(response).await;
|
|
4857
|
+
return Err(anyhow!("file remove failed: {}", body));
|
|
4858
|
+
}
|
|
4859
|
+
let payload: serde_json::Value = response.json().await?;
|
|
4860
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
4861
|
+
save_session(&session)?;
|
|
4862
|
+
Ok(())
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
async fn tool_list_command(client: &reqwest::Client, args: ToolListArgs) -> Result<()> {
|
|
4866
|
+
let mut session = load_session()?;
|
|
4867
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
4868
|
+
|
|
4869
|
+
let path = format!(
|
|
3973
4870
|
"/tools/definitions?includeInactive={}&includeDisabledChannel={}",
|
|
3974
4871
|
args.include_inactive, args.include_disabled_channel
|
|
3975
4872
|
);
|
|
@@ -4015,6 +4912,43 @@ async fn tool_register_command(client: &reqwest::Client, args: ToolRegisterArgs)
|
|
|
4015
4912
|
Ok(())
|
|
4016
4913
|
}
|
|
4017
4914
|
|
|
4915
|
+
async fn tool_publish_command(client: &reqwest::Client, args: ToolPublishArgs) -> Result<()> {
|
|
4916
|
+
let mut session = load_session()?;
|
|
4917
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
4918
|
+
|
|
4919
|
+
let mut body = serde_json::Map::new();
|
|
4920
|
+
if let Some(channel) = args.channel {
|
|
4921
|
+
body.insert("channel".to_string(), serde_json::Value::String(channel));
|
|
4922
|
+
}
|
|
4923
|
+
if let Some(visibility) = args.visibility {
|
|
4924
|
+
body.insert("visibility".to_string(), serde_json::Value::String(visibility));
|
|
4925
|
+
}
|
|
4926
|
+
if let Some(notes) = args.notes {
|
|
4927
|
+
body.insert("notes".to_string(), serde_json::Value::String(notes));
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
let path = format!("/tools/definitions/{}/publish", args.tool_id);
|
|
4931
|
+
let response = authed_request(
|
|
4932
|
+
client,
|
|
4933
|
+
&mut session,
|
|
4934
|
+
Method::POST,
|
|
4935
|
+
&path,
|
|
4936
|
+
Some(serde_json::Value::Object(body)),
|
|
4937
|
+
)
|
|
4938
|
+
.await?;
|
|
4939
|
+
if !response.status().is_success() {
|
|
4940
|
+
let body = read_error_body(response).await;
|
|
4941
|
+
return Err(anyhow!("tool publish failed: {}", body));
|
|
4942
|
+
}
|
|
4943
|
+
let payload: serde_json::Value = response.json().await?;
|
|
4944
|
+
println!(
|
|
4945
|
+
"{}",
|
|
4946
|
+
serde_json::to_string_pretty(payload.get("definition").unwrap_or(&payload))?
|
|
4947
|
+
);
|
|
4948
|
+
save_session(&session)?;
|
|
4949
|
+
Ok(())
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4018
4952
|
async fn tool_set_entitlement_command(
|
|
4019
4953
|
client: &reqwest::Client,
|
|
4020
4954
|
mut session: SessionConfig,
|
|
@@ -4079,80 +5013,693 @@ async fn tool_set_entitlement_command(
|
|
|
4079
5013
|
.await?;
|
|
4080
5014
|
if !response.status().is_success() {
|
|
4081
5015
|
let body = read_error_body(response).await;
|
|
4082
|
-
return Err(anyhow!("tool entitlement update failed: {}", body));
|
|
5016
|
+
return Err(anyhow!("tool entitlement update failed: {}", body));
|
|
5017
|
+
}
|
|
5018
|
+
let payload: serde_json::Value = response.json().await?;
|
|
5019
|
+
println!(
|
|
5020
|
+
"{}",
|
|
5021
|
+
serde_json::to_string_pretty(payload.get("entitlement").unwrap_or(&payload))?
|
|
5022
|
+
);
|
|
5023
|
+
save_session(&session)?;
|
|
5024
|
+
Ok(())
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
async fn tool_enable_command(client: &reqwest::Client, args: ToolEnableArgs) -> Result<()> {
|
|
5028
|
+
let mut session = load_session()?;
|
|
5029
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
5030
|
+
|
|
5031
|
+
tool_set_entitlement_command(
|
|
5032
|
+
client,
|
|
5033
|
+
session,
|
|
5034
|
+
args.tool_id,
|
|
5035
|
+
args.org_id,
|
|
5036
|
+
args.project_id,
|
|
5037
|
+
args.user_id,
|
|
5038
|
+
"enabled",
|
|
5039
|
+
args.expires_at,
|
|
5040
|
+
args.metadata_file,
|
|
5041
|
+
)
|
|
5042
|
+
.await
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
async fn tool_disable_command(client: &reqwest::Client, args: ToolDisableArgs) -> Result<()> {
|
|
5046
|
+
let mut session = load_session()?;
|
|
5047
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
5048
|
+
|
|
5049
|
+
tool_set_entitlement_command(
|
|
5050
|
+
client,
|
|
5051
|
+
session,
|
|
5052
|
+
args.tool_id,
|
|
5053
|
+
args.org_id,
|
|
5054
|
+
args.project_id,
|
|
5055
|
+
args.user_id,
|
|
5056
|
+
"disabled",
|
|
5057
|
+
None,
|
|
5058
|
+
args.metadata_file,
|
|
5059
|
+
)
|
|
5060
|
+
.await
|
|
5061
|
+
}
|
|
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
|
+
|
|
5198
|
+
async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result<()> {
|
|
5199
|
+
let mut session = load_session()?;
|
|
5200
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
5201
|
+
|
|
5202
|
+
if args.input_json.is_some() && args.input_file.is_some() {
|
|
5203
|
+
return Err(anyhow!(
|
|
5204
|
+
"Provide either --input-json or --input-file, not both"
|
|
5205
|
+
));
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5208
|
+
let input_value = if let Some(path) = args.input_file {
|
|
5209
|
+
load_jsonc_file(&path, "tool run input")?
|
|
5210
|
+
} else if let Some(input_json) = args.input_json {
|
|
5211
|
+
parse_jsonc_str(&input_json, "tool run input")?
|
|
5212
|
+
} else {
|
|
5213
|
+
serde_json::Value::Object(serde_json::Map::new())
|
|
5214
|
+
};
|
|
5215
|
+
|
|
5216
|
+
let input_object =
|
|
5217
|
+
serde_json::Value::Object(parse_object_from_value(input_value, "tool run input")?);
|
|
5218
|
+
|
|
5219
|
+
let mut body = serde_json::Map::new();
|
|
5220
|
+
body.insert(
|
|
5221
|
+
"toolId".to_string(),
|
|
5222
|
+
serde_json::Value::String(args.tool_id),
|
|
5223
|
+
);
|
|
5224
|
+
body.insert("input".to_string(), input_object);
|
|
5225
|
+
if let Some(org_id) = args.org_id {
|
|
5226
|
+
body.insert("orgId".to_string(), serde_json::Value::String(org_id));
|
|
5227
|
+
}
|
|
5228
|
+
if let Some(project_id) = args.project_id {
|
|
5229
|
+
body.insert(
|
|
5230
|
+
"projectId".to_string(),
|
|
5231
|
+
serde_json::Value::String(project_id),
|
|
5232
|
+
);
|
|
5233
|
+
}
|
|
5234
|
+
let mut metadata_map = if let Some(path) = args.metadata_file {
|
|
5235
|
+
let metadata = load_jsonc_file(&path, "tool run metadata")?;
|
|
5236
|
+
parse_object_from_value(metadata, "tool run metadata")?
|
|
5237
|
+
} else {
|
|
5238
|
+
serde_json::Map::new()
|
|
5239
|
+
};
|
|
5240
|
+
if let Some(idempotency_key) = args.idempotency_key {
|
|
5241
|
+
let normalized = idempotency_key.trim();
|
|
5242
|
+
if !normalized.is_empty() {
|
|
5243
|
+
metadata_map.insert(
|
|
5244
|
+
"idempotencyKey".to_string(),
|
|
5245
|
+
serde_json::Value::String(normalized.to_string()),
|
|
5246
|
+
);
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
if !metadata_map.is_empty() {
|
|
5250
|
+
body.insert(
|
|
5251
|
+
"metadata".to_string(),
|
|
5252
|
+
serde_json::Value::Object(metadata_map),
|
|
5253
|
+
);
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
let response = authed_request(
|
|
5257
|
+
client,
|
|
5258
|
+
&mut session,
|
|
5259
|
+
Method::POST,
|
|
5260
|
+
"/tools/runs",
|
|
5261
|
+
Some(serde_json::Value::Object(body)),
|
|
5262
|
+
)
|
|
5263
|
+
.await?;
|
|
5264
|
+
if !response.status().is_success() {
|
|
5265
|
+
let body = read_error_body(response).await;
|
|
5266
|
+
return Err(anyhow!("tool run failed: {}", body));
|
|
5267
|
+
}
|
|
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
|
+
};
|
|
5286
|
+
println!(
|
|
5287
|
+
"{}",
|
|
5288
|
+
serde_json::to_string_pretty(payload.get("run").unwrap_or(&payload))?
|
|
5289
|
+
);
|
|
5290
|
+
save_session(&session)?;
|
|
5291
|
+
Ok(())
|
|
5292
|
+
}
|
|
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
|
+
|
|
5330
|
+
async fn tool_runs_command(client: &reqwest::Client, args: ToolRunsArgs) -> Result<()> {
|
|
5331
|
+
let mut session = load_session()?;
|
|
5332
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
5333
|
+
|
|
5334
|
+
let mut query_parts: Vec<String> = Vec::new();
|
|
5335
|
+
if let Some(tool_id) = args.tool_id {
|
|
5336
|
+
query_parts.push(format!("toolId={}", tool_id));
|
|
5337
|
+
}
|
|
5338
|
+
if let Some(project_id) = args.project_id {
|
|
5339
|
+
query_parts.push(format!("projectId={}", project_id));
|
|
5340
|
+
}
|
|
5341
|
+
if let Some(requested_by_user_id) = args.requested_by_user_id {
|
|
5342
|
+
query_parts.push(format!("requestedByUserId={}", requested_by_user_id));
|
|
5343
|
+
}
|
|
5344
|
+
if let Some(status) = args.status {
|
|
5345
|
+
query_parts.push(format!("status={}", status));
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5348
|
+
let path = if query_parts.is_empty() {
|
|
5349
|
+
"/tools/runs".to_string()
|
|
5350
|
+
} else {
|
|
5351
|
+
format!("/tools/runs?{}", query_parts.join("&"))
|
|
5352
|
+
};
|
|
5353
|
+
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
5354
|
+
if !response.status().is_success() {
|
|
5355
|
+
let body = read_error_body(response).await;
|
|
5356
|
+
return Err(anyhow!("tool runs failed: {}", body));
|
|
5357
|
+
}
|
|
5358
|
+
let payload: serde_json::Value = response.json().await?;
|
|
5359
|
+
println!(
|
|
5360
|
+
"{}",
|
|
5361
|
+
serde_json::to_string_pretty(payload.get("runs").unwrap_or(&payload))?
|
|
5362
|
+
);
|
|
5363
|
+
save_session(&session)?;
|
|
5364
|
+
Ok(())
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5367
|
+
async fn tool_get_run_command(client: &reqwest::Client, args: ToolGetRunArgs) -> Result<()> {
|
|
5368
|
+
let mut session = load_session()?;
|
|
5369
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
5370
|
+
|
|
5371
|
+
let path = format!("/tools/runs/{}", args.run_id);
|
|
5372
|
+
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
5373
|
+
if !response.status().is_success() {
|
|
5374
|
+
let body = read_error_body(response).await;
|
|
5375
|
+
return Err(anyhow!("tool get-run failed: {}", body));
|
|
5376
|
+
}
|
|
5377
|
+
let payload: serde_json::Value = response.json().await?;
|
|
5378
|
+
println!(
|
|
5379
|
+
"{}",
|
|
5380
|
+
serde_json::to_string_pretty(payload.get("run").unwrap_or(&payload))?
|
|
5381
|
+
);
|
|
5382
|
+
save_session(&session)?;
|
|
5383
|
+
Ok(())
|
|
5384
|
+
}
|
|
5385
|
+
|
|
5386
|
+
async fn tool_run_events_command(client: &reqwest::Client, args: ToolRunEventsArgs) -> Result<()> {
|
|
5387
|
+
let mut session = load_session()?;
|
|
5388
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
5389
|
+
|
|
5390
|
+
let mut path = format!("/tools/runs/{}/events", args.run_id);
|
|
5391
|
+
let mut query_parts: Vec<String> = Vec::new();
|
|
5392
|
+
if let Some(limit) = args.limit {
|
|
5393
|
+
query_parts.push(format!("limit={}", limit));
|
|
5394
|
+
}
|
|
5395
|
+
if let Some(status) = args.status {
|
|
5396
|
+
query_parts.push(format!("status={}", status));
|
|
5397
|
+
}
|
|
5398
|
+
if let Some(stage_prefix) = args.stage_prefix {
|
|
5399
|
+
query_parts.push(format!("stagePrefix={}", stage_prefix));
|
|
5400
|
+
}
|
|
5401
|
+
if let Some(since) = args.since {
|
|
5402
|
+
query_parts.push(format!("since={}", since));
|
|
5403
|
+
}
|
|
5404
|
+
if let Some(until) = args.until {
|
|
5405
|
+
query_parts.push(format!("until={}", until));
|
|
5406
|
+
}
|
|
5407
|
+
if !query_parts.is_empty() {
|
|
5408
|
+
path.push_str(&format!("?{}", query_parts.join("&")));
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
5412
|
+
if !response.status().is_success() {
|
|
5413
|
+
let body = read_error_body(response).await;
|
|
5414
|
+
return Err(anyhow!("tool run-events failed: {}", body));
|
|
5415
|
+
}
|
|
5416
|
+
let payload: serde_json::Value = response.json().await?;
|
|
5417
|
+
println!(
|
|
5418
|
+
"{}",
|
|
5419
|
+
serde_json::to_string_pretty(payload.get("events").unwrap_or(&payload))?
|
|
5420
|
+
);
|
|
5421
|
+
save_session(&session)?;
|
|
5422
|
+
Ok(())
|
|
5423
|
+
}
|
|
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
|
+
|
|
5651
|
+
async fn tool_local_catalog_command(client: &reqwest::Client, args: ToolLocalCatalogArgs) -> Result<()> {
|
|
5652
|
+
let mut session = load_session()?;
|
|
5653
|
+
apply_base_url_override(&mut session, args.base_url);
|
|
5654
|
+
|
|
5655
|
+
let (default_platform, default_arch) = detect_local_runtime_platform_arch();
|
|
5656
|
+
let platform = args.platform.unwrap_or(default_platform);
|
|
5657
|
+
let arch = args.arch.unwrap_or(default_arch);
|
|
5658
|
+
|
|
5659
|
+
let mut query_parts = vec![
|
|
5660
|
+
format!("platform={}", platform),
|
|
5661
|
+
format!("arch={}", arch),
|
|
5662
|
+
];
|
|
5663
|
+
if let Some(org_id) = args.org_id {
|
|
5664
|
+
query_parts.push(format!("orgId={}", org_id));
|
|
5665
|
+
}
|
|
5666
|
+
if let Some(project_id) = args.project_id {
|
|
5667
|
+
query_parts.push(format!("projectId={}", project_id));
|
|
5668
|
+
}
|
|
5669
|
+
let path = format!("/tools/local/catalog?{}", query_parts.join("&"));
|
|
5670
|
+
|
|
5671
|
+
let response = authed_request(client, &mut session, Method::GET, &path, None).await?;
|
|
5672
|
+
if !response.status().is_success() {
|
|
5673
|
+
let body = read_error_body(response).await;
|
|
5674
|
+
return Err(anyhow!("tool local catalog failed: {}", body));
|
|
4083
5675
|
}
|
|
4084
5676
|
let payload: serde_json::Value = response.json().await?;
|
|
4085
5677
|
println!(
|
|
4086
5678
|
"{}",
|
|
4087
|
-
serde_json::to_string_pretty(payload.get("
|
|
5679
|
+
serde_json::to_string_pretty(payload.get("tools").unwrap_or(&payload))?
|
|
4088
5680
|
);
|
|
4089
5681
|
save_session(&session)?;
|
|
4090
5682
|
Ok(())
|
|
4091
5683
|
}
|
|
4092
5684
|
|
|
4093
|
-
async fn
|
|
4094
|
-
let mut session = load_session()?;
|
|
4095
|
-
apply_base_url_override(&mut session, args.base_url);
|
|
4096
|
-
|
|
4097
|
-
tool_set_entitlement_command(
|
|
4098
|
-
client,
|
|
4099
|
-
session,
|
|
4100
|
-
args.tool_id,
|
|
4101
|
-
args.org_id,
|
|
4102
|
-
args.project_id,
|
|
4103
|
-
args.user_id,
|
|
4104
|
-
"enabled",
|
|
4105
|
-
args.expires_at,
|
|
4106
|
-
args.metadata_file,
|
|
4107
|
-
)
|
|
4108
|
-
.await
|
|
4109
|
-
}
|
|
4110
|
-
|
|
4111
|
-
async fn tool_disable_command(client: &reqwest::Client, args: ToolDisableArgs) -> Result<()> {
|
|
4112
|
-
let mut session = load_session()?;
|
|
4113
|
-
apply_base_url_override(&mut session, args.base_url);
|
|
4114
|
-
|
|
4115
|
-
tool_set_entitlement_command(
|
|
4116
|
-
client,
|
|
4117
|
-
session,
|
|
4118
|
-
args.tool_id,
|
|
4119
|
-
args.org_id,
|
|
4120
|
-
args.project_id,
|
|
4121
|
-
args.user_id,
|
|
4122
|
-
"disabled",
|
|
4123
|
-
None,
|
|
4124
|
-
args.metadata_file,
|
|
4125
|
-
)
|
|
4126
|
-
.await
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
|
-
async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result<()> {
|
|
5685
|
+
async fn tool_local_install_command(client: &reqwest::Client, args: ToolLocalInstallArgs) -> Result<()> {
|
|
4130
5686
|
let mut session = load_session()?;
|
|
4131
5687
|
apply_base_url_override(&mut session, args.base_url);
|
|
4132
5688
|
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
));
|
|
4137
|
-
}
|
|
4138
|
-
|
|
4139
|
-
let input_value = if let Some(path) = args.input_file {
|
|
4140
|
-
load_jsonc_file(&path, "tool run input")?
|
|
4141
|
-
} else if let Some(input_json) = args.input_json {
|
|
4142
|
-
parse_jsonc_str(&input_json, "tool run input")?
|
|
4143
|
-
} else {
|
|
4144
|
-
serde_json::Value::Object(serde_json::Map::new())
|
|
4145
|
-
};
|
|
4146
|
-
|
|
4147
|
-
let input_object =
|
|
4148
|
-
serde_json::Value::Object(parse_object_from_value(input_value, "tool run input")?);
|
|
5689
|
+
let (default_platform, default_arch) = detect_local_runtime_platform_arch();
|
|
5690
|
+
let platform = args.platform.unwrap_or(default_platform);
|
|
5691
|
+
let arch = args.arch.unwrap_or(default_arch);
|
|
4149
5692
|
|
|
4150
5693
|
let mut body = serde_json::Map::new();
|
|
4151
5694
|
body.insert(
|
|
4152
5695
|
"toolId".to_string(),
|
|
4153
|
-
serde_json::Value::String(args.tool_id),
|
|
5696
|
+
serde_json::Value::String(args.tool_id.clone()),
|
|
4154
5697
|
);
|
|
4155
|
-
body.insert(
|
|
5698
|
+
body.insert(
|
|
5699
|
+
"platform".to_string(),
|
|
5700
|
+
serde_json::Value::String(platform.clone()),
|
|
5701
|
+
);
|
|
5702
|
+
body.insert("arch".to_string(), serde_json::Value::String(arch.clone()));
|
|
4156
5703
|
if let Some(org_id) = args.org_id {
|
|
4157
5704
|
body.insert("orgId".to_string(), serde_json::Value::String(org_id));
|
|
4158
5705
|
}
|
|
@@ -4162,95 +5709,231 @@ async fn tool_run_command(client: &reqwest::Client, args: ToolRunArgs) -> Result
|
|
|
4162
5709
|
serde_json::Value::String(project_id),
|
|
4163
5710
|
);
|
|
4164
5711
|
}
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
parse_object_from_value(metadata, "tool run metadata")?
|
|
4168
|
-
} else {
|
|
4169
|
-
serde_json::Map::new()
|
|
4170
|
-
};
|
|
4171
|
-
if let Some(idempotency_key) = args.idempotency_key {
|
|
4172
|
-
let normalized = idempotency_key.trim();
|
|
4173
|
-
if !normalized.is_empty() {
|
|
4174
|
-
metadata_map.insert(
|
|
4175
|
-
"idempotencyKey".to_string(),
|
|
4176
|
-
serde_json::Value::String(normalized.to_string()),
|
|
4177
|
-
);
|
|
4178
|
-
}
|
|
4179
|
-
}
|
|
4180
|
-
if !metadata_map.is_empty() {
|
|
4181
|
-
body.insert(
|
|
4182
|
-
"metadata".to_string(),
|
|
4183
|
-
serde_json::Value::Object(metadata_map),
|
|
4184
|
-
);
|
|
5712
|
+
if let Some(version) = args.version {
|
|
5713
|
+
body.insert("version".to_string(), serde_json::Value::String(version));
|
|
4185
5714
|
}
|
|
4186
5715
|
|
|
4187
5716
|
let response = authed_request(
|
|
4188
5717
|
client,
|
|
4189
5718
|
&mut session,
|
|
4190
5719
|
Method::POST,
|
|
4191
|
-
"/tools/
|
|
5720
|
+
"/tools/local/install-intent",
|
|
4192
5721
|
Some(serde_json::Value::Object(body)),
|
|
4193
5722
|
)
|
|
4194
5723
|
.await?;
|
|
4195
5724
|
if !response.status().is_success() {
|
|
4196
5725
|
let body = read_error_body(response).await;
|
|
4197
|
-
return Err(anyhow!("tool
|
|
5726
|
+
return Err(anyhow!("tool local install-intent failed: {}", body));
|
|
4198
5727
|
}
|
|
5728
|
+
|
|
4199
5729
|
let payload: serde_json::Value = response.json().await?;
|
|
4200
|
-
|
|
4201
|
-
"{}",
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
Ok(())
|
|
4206
|
-
}
|
|
5730
|
+
if args.no_download {
|
|
5731
|
+
println!("{}", serde_json::to_string_pretty(&payload)?);
|
|
5732
|
+
save_session(&session)?;
|
|
5733
|
+
return Ok(());
|
|
5734
|
+
}
|
|
4207
5735
|
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
5736
|
+
let download_path = payload
|
|
5737
|
+
.pointer("/install/downloadPath")
|
|
5738
|
+
.and_then(|value| value.as_str())
|
|
5739
|
+
.ok_or_else(|| anyhow!("install intent is missing install.downloadPath"))?;
|
|
5740
|
+
let asset_id = payload
|
|
5741
|
+
.pointer("/asset/assetId")
|
|
5742
|
+
.and_then(|value| value.as_str())
|
|
5743
|
+
.unwrap_or("unknown");
|
|
5744
|
+
let fallback_file_name = payload
|
|
5745
|
+
.pointer("/asset/fileName")
|
|
5746
|
+
.and_then(|value| value.as_str())
|
|
5747
|
+
.unwrap_or("tool-bundle.bin");
|
|
4211
5748
|
|
|
4212
|
-
let mut
|
|
4213
|
-
|
|
4214
|
-
|
|
5749
|
+
let mut output_path = args
|
|
5750
|
+
.output_path
|
|
5751
|
+
.unwrap_or_else(|| PathBuf::from(base_name_from_virtual_path(fallback_file_name)));
|
|
5752
|
+
if output_path.exists() && output_path.is_dir() {
|
|
5753
|
+
output_path = output_path.join(base_name_from_virtual_path(fallback_file_name));
|
|
4215
5754
|
}
|
|
4216
|
-
if let Some(
|
|
4217
|
-
|
|
5755
|
+
if let Some(parent) = output_path.parent() {
|
|
5756
|
+
if !parent.as_os_str().is_empty() {
|
|
5757
|
+
tokio_fs::create_dir_all(parent)
|
|
5758
|
+
.await
|
|
5759
|
+
.with_context(|| format!("Failed to create output directory {}", parent.display()))?;
|
|
5760
|
+
}
|
|
4218
5761
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
5762
|
+
|
|
5763
|
+
let mut resume_from: Option<u64> = None;
|
|
5764
|
+
if args.resume && output_path.exists() && output_path.is_file() {
|
|
5765
|
+
let existing_size = tokio_fs::metadata(&output_path)
|
|
5766
|
+
.await
|
|
5767
|
+
.with_context(|| {
|
|
5768
|
+
format!(
|
|
5769
|
+
"Failed to read output file metadata {}",
|
|
5770
|
+
output_path.display()
|
|
5771
|
+
)
|
|
5772
|
+
})?
|
|
5773
|
+
.len();
|
|
5774
|
+
if existing_size > 0 {
|
|
5775
|
+
let remote_size = payload
|
|
5776
|
+
.pointer("/asset/sizeBytes")
|
|
5777
|
+
.and_then(|value| value.as_u64())
|
|
5778
|
+
.unwrap_or(0);
|
|
5779
|
+
if remote_size > 0 && existing_size >= remote_size {
|
|
5780
|
+
println!(
|
|
5781
|
+
"{}",
|
|
5782
|
+
serde_json::to_string_pretty(&serde_json::json!({
|
|
5783
|
+
"toolId": args.tool_id,
|
|
5784
|
+
"assetId": asset_id,
|
|
5785
|
+
"output": output_path.display().to_string(),
|
|
5786
|
+
"bytesWritten": 0,
|
|
5787
|
+
"resumedFrom": existing_size,
|
|
5788
|
+
"alreadyComplete": true
|
|
5789
|
+
}))?
|
|
5790
|
+
);
|
|
5791
|
+
save_session(&session)?;
|
|
5792
|
+
return Ok(());
|
|
5793
|
+
}
|
|
5794
|
+
resume_from = Some(existing_size);
|
|
5795
|
+
}
|
|
4221
5796
|
}
|
|
4222
|
-
|
|
4223
|
-
|
|
5797
|
+
|
|
5798
|
+
let mut headers = Vec::new();
|
|
5799
|
+
if let Some(offset) = resume_from {
|
|
5800
|
+
headers.push(("range".to_string(), format!("bytes={}-", offset)));
|
|
5801
|
+
}
|
|
5802
|
+
let mut download_response = authed_request_with_headers(
|
|
5803
|
+
client,
|
|
5804
|
+
&mut session,
|
|
5805
|
+
Method::GET,
|
|
5806
|
+
download_path,
|
|
5807
|
+
None,
|
|
5808
|
+
&headers,
|
|
5809
|
+
)
|
|
5810
|
+
.await?;
|
|
5811
|
+
if !(download_response.status().is_success()
|
|
5812
|
+
|| download_response.status() == StatusCode::PARTIAL_CONTENT)
|
|
5813
|
+
{
|
|
5814
|
+
let body = read_error_body(download_response).await;
|
|
5815
|
+
return Err(anyhow!("tool local install download failed: {}", body));
|
|
4224
5816
|
}
|
|
4225
5817
|
|
|
4226
|
-
let
|
|
4227
|
-
|
|
5818
|
+
let append_mode = resume_from.is_some() && download_response.status() == StatusCode::PARTIAL_CONTENT;
|
|
5819
|
+
let mut output_file = if append_mode {
|
|
5820
|
+
tokio_fs::OpenOptions::new()
|
|
5821
|
+
.append(true)
|
|
5822
|
+
.open(&output_path)
|
|
5823
|
+
.await
|
|
5824
|
+
.with_context(|| {
|
|
5825
|
+
format!(
|
|
5826
|
+
"Failed to open output file for append {}",
|
|
5827
|
+
output_path.display()
|
|
5828
|
+
)
|
|
5829
|
+
})?
|
|
4228
5830
|
} else {
|
|
4229
|
-
|
|
5831
|
+
tokio_fs::File::create(&output_path)
|
|
5832
|
+
.await
|
|
5833
|
+
.with_context(|| format!("Failed to create output file {}", output_path.display()))?
|
|
4230
5834
|
};
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
5835
|
+
|
|
5836
|
+
let mut bytes_written: u64 = 0;
|
|
5837
|
+
while let Some(chunk) = download_response.chunk().await? {
|
|
5838
|
+
output_file
|
|
5839
|
+
.write_all(&chunk)
|
|
5840
|
+
.await
|
|
5841
|
+
.with_context(|| format!("Failed to write to {}", output_path.display()))?;
|
|
5842
|
+
bytes_written = bytes_written.saturating_add(chunk.len() as u64);
|
|
4235
5843
|
}
|
|
4236
|
-
|
|
5844
|
+
output_file.flush().await?;
|
|
5845
|
+
|
|
4237
5846
|
println!(
|
|
4238
5847
|
"{}",
|
|
4239
|
-
serde_json::to_string_pretty(
|
|
5848
|
+
serde_json::to_string_pretty(&serde_json::json!({
|
|
5849
|
+
"toolId": args.tool_id,
|
|
5850
|
+
"platform": platform,
|
|
5851
|
+
"arch": arch,
|
|
5852
|
+
"assetId": asset_id,
|
|
5853
|
+
"output": output_path.display().to_string(),
|
|
5854
|
+
"bytesWritten": bytes_written,
|
|
5855
|
+
"resumedFrom": resume_from,
|
|
5856
|
+
"partialContent": download_response.status() == StatusCode::PARTIAL_CONTENT
|
|
5857
|
+
}))?
|
|
4240
5858
|
);
|
|
4241
5859
|
save_session(&session)?;
|
|
4242
5860
|
Ok(())
|
|
4243
5861
|
}
|
|
4244
5862
|
|
|
4245
|
-
async fn
|
|
5863
|
+
async fn tool_local_complete_run_command(
|
|
5864
|
+
client: &reqwest::Client,
|
|
5865
|
+
args: ToolLocalCompleteRunArgs,
|
|
5866
|
+
) -> Result<()> {
|
|
4246
5867
|
let mut session = load_session()?;
|
|
4247
5868
|
apply_base_url_override(&mut session, args.base_url);
|
|
4248
5869
|
|
|
4249
|
-
let
|
|
4250
|
-
|
|
5870
|
+
let normalized_status = args.status.trim().to_lowercase();
|
|
5871
|
+
if normalized_status != "succeeded"
|
|
5872
|
+
&& normalized_status != "failed"
|
|
5873
|
+
&& normalized_status != "cancelled"
|
|
5874
|
+
{
|
|
5875
|
+
return Err(anyhow!(
|
|
5876
|
+
"status must be one of: succeeded, failed, cancelled"
|
|
5877
|
+
));
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
if args.output_file.is_some() && args.output_json.is_some() {
|
|
5881
|
+
return Err(anyhow!(
|
|
5882
|
+
"Provide either --output-file or --output-json, not both"
|
|
5883
|
+
));
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
let output_value = if let Some(path) = args.output_file {
|
|
5887
|
+
Some(load_jsonc_file(&path, "tool local complete output")?)
|
|
5888
|
+
} else if let Some(raw) = args.output_json {
|
|
5889
|
+
Some(parse_jsonc_str(&raw, "tool local complete output")?)
|
|
5890
|
+
} else {
|
|
5891
|
+
None
|
|
5892
|
+
};
|
|
5893
|
+
|
|
5894
|
+
let mut body = serde_json::Map::new();
|
|
5895
|
+
body.insert(
|
|
5896
|
+
"status".to_string(),
|
|
5897
|
+
serde_json::Value::String(normalized_status),
|
|
5898
|
+
);
|
|
5899
|
+
if let Some(output) = output_value {
|
|
5900
|
+
body.insert(
|
|
5901
|
+
"output".to_string(),
|
|
5902
|
+
serde_json::Value::Object(parse_object_from_value(
|
|
5903
|
+
output,
|
|
5904
|
+
"tool local complete output",
|
|
5905
|
+
)?),
|
|
5906
|
+
);
|
|
5907
|
+
}
|
|
5908
|
+
if let Some(error_message) = args.error_message {
|
|
5909
|
+
body.insert(
|
|
5910
|
+
"errorMessage".to_string(),
|
|
5911
|
+
serde_json::Value::String(error_message),
|
|
5912
|
+
);
|
|
5913
|
+
}
|
|
5914
|
+
if let Some(path) = args.metadata_file {
|
|
5915
|
+
let metadata = load_jsonc_file(&path, "tool local complete metadata")?;
|
|
5916
|
+
body.insert(
|
|
5917
|
+
"metadata".to_string(),
|
|
5918
|
+
serde_json::Value::Object(parse_object_from_value(
|
|
5919
|
+
metadata,
|
|
5920
|
+
"tool local complete metadata",
|
|
5921
|
+
)?),
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
|
|
5925
|
+
let path = format!("/tools/runs/{}/complete", args.run_id);
|
|
5926
|
+
let response = authed_request(
|
|
5927
|
+
client,
|
|
5928
|
+
&mut session,
|
|
5929
|
+
Method::POST,
|
|
5930
|
+
&path,
|
|
5931
|
+
Some(serde_json::Value::Object(body)),
|
|
5932
|
+
)
|
|
5933
|
+
.await?;
|
|
4251
5934
|
if !response.status().is_success() {
|
|
4252
5935
|
let body = read_error_body(response).await;
|
|
4253
|
-
return Err(anyhow!("tool
|
|
5936
|
+
return Err(anyhow!("tool local complete-run failed: {}", body));
|
|
4254
5937
|
}
|
|
4255
5938
|
let payload: serde_json::Value = response.json().await?;
|
|
4256
5939
|
println!(
|
|
@@ -4403,7 +6086,8 @@ async fn self_update_command(
|
|
|
4403
6086
|
mod tests {
|
|
4404
6087
|
use super::{
|
|
4405
6088
|
compose_plugin_archive_url, default_login_scopes, default_login_scopes_with_tools,
|
|
4406
|
-
file_name_component, normalize_public_bucket_base, normalize_sha256_hex,
|
|
6089
|
+
file_name_component, normalize_public_bucket_base, normalize_sha256_hex, parse_api_error,
|
|
6090
|
+
resolve_plugin_from_index,
|
|
4407
6091
|
};
|
|
4408
6092
|
use super::unreal::{PluginIndexFile, PluginIndexPlugin, PluginIndexVersion};
|
|
4409
6093
|
|
|
@@ -4508,6 +6192,20 @@ mod tests {
|
|
|
4508
6192
|
assert!(scopes.contains(&"tools:write".to_string()));
|
|
4509
6193
|
assert!(scopes.contains(&"tools:run".to_string()));
|
|
4510
6194
|
}
|
|
6195
|
+
|
|
6196
|
+
#[test]
|
|
6197
|
+
fn parses_api_error_envelope() {
|
|
6198
|
+
let parsed = parse_api_error(r#"{"code":"CLERK_ORG_NOT_LINKED","message":"Not linked"}"#)
|
|
6199
|
+
.expect("api error should parse");
|
|
6200
|
+
assert_eq!(parsed.code.as_deref(), Some("CLERK_ORG_NOT_LINKED"));
|
|
6201
|
+
assert_eq!(parsed.message.as_deref(), Some("Not linked"));
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
#[test]
|
|
6205
|
+
fn ignores_non_json_api_error_body() {
|
|
6206
|
+
let parsed = parse_api_error("plain text error");
|
|
6207
|
+
assert!(parsed.is_none());
|
|
6208
|
+
}
|
|
4511
6209
|
}
|
|
4512
6210
|
|
|
4513
6211
|
async fn run_cli(cli: Cli) -> Result<()> {
|
|
@@ -4561,22 +6259,72 @@ async fn run_cli(cli: Cli) -> Result<()> {
|
|
|
4561
6259
|
},
|
|
4562
6260
|
Commands::File { command } => match command {
|
|
4563
6261
|
FileCommands::List(args) => file_list_command(&client, args).await?,
|
|
6262
|
+
FileCommands::Tree(args) => file_tree_command(&client, args).await?,
|
|
4564
6263
|
FileCommands::Get(args) => file_get_command(&client, args).await?,
|
|
4565
6264
|
FileCommands::Stat(args) => file_stat_command(&client, args).await?,
|
|
6265
|
+
FileCommands::Thumbnail(args) => file_thumbnail_command(&client, args).await?,
|
|
4566
6266
|
FileCommands::Download(args) => file_download_command(&client, args).await?,
|
|
4567
6267
|
FileCommands::Upload(args) => file_upload_command(&client, args).await?,
|
|
4568
6268
|
FileCommands::Mkdir(args) => file_mkdir_command(&client, args).await?,
|
|
4569
6269
|
FileCommands::Move(args) => file_move_command(&client, args).await?,
|
|
6270
|
+
FileCommands::Set(args) => file_set_command(&client, args).await?,
|
|
6271
|
+
FileCommands::MoveFolder(args) => file_move_folder_command(&client, args).await?,
|
|
6272
|
+
FileCommands::Rmdir(args) => file_rmdir_command(&client, args).await?,
|
|
4570
6273
|
FileCommands::Remove(args) => file_remove_command(&client, args).await?,
|
|
4571
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
|
+
},
|
|
4572
6299
|
Commands::Tool { command } => match command {
|
|
4573
6300
|
ToolCommands::List(args) => tool_list_command(&client, args).await?,
|
|
4574
6301
|
ToolCommands::Register(args) => tool_register_command(&client, args).await?,
|
|
6302
|
+
ToolCommands::Publish(args) => tool_publish_command(&client, args).await?,
|
|
4575
6303
|
ToolCommands::Enable(args) => tool_enable_command(&client, args).await?,
|
|
4576
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?,
|
|
4577
6310
|
ToolCommands::Run(args) => tool_run_command(&client, args).await?,
|
|
4578
6311
|
ToolCommands::Runs(args) => tool_runs_command(&client, args).await?,
|
|
4579
6312
|
ToolCommands::GetRun(args) => tool_get_run_command(&client, args).await?,
|
|
6313
|
+
ToolCommands::RunEvents(args) => tool_run_events_command(&client, args).await?,
|
|
6314
|
+
ToolCommands::TraceStatus(args) => tool_trace_status_command(&client, args).await?,
|
|
6315
|
+
ToolCommands::Local { command } => match command {
|
|
6316
|
+
ToolLocalCommands::Catalog(args) => tool_local_catalog_command(&client, args).await?,
|
|
6317
|
+
ToolLocalCommands::Install(args) => tool_local_install_command(&client, args).await?,
|
|
6318
|
+
ToolLocalCommands::CompleteRun(args) => {
|
|
6319
|
+
tool_local_complete_run_command(&client, args).await?
|
|
6320
|
+
}
|
|
6321
|
+
},
|
|
6322
|
+
},
|
|
6323
|
+
Commands::Logs { command } => match command {
|
|
6324
|
+
LogsCommands::Status => logs_status_command(cli.output).await?,
|
|
6325
|
+
LogsCommands::Consent(args) => logs_consent_command(args, cli.output).await?,
|
|
6326
|
+
LogsCommands::Tail(args) => logs_tail_command(args, cli.output).await?,
|
|
6327
|
+
LogsCommands::Upload(args) => logs_upload_command(&client, args, cli.output).await?,
|
|
4580
6328
|
},
|
|
4581
6329
|
}
|
|
4582
6330
|
|
|
@@ -4587,7 +6335,19 @@ async fn run_cli(cli: Cli) -> Result<()> {
|
|
|
4587
6335
|
async fn main() {
|
|
4588
6336
|
let cli = Cli::parse();
|
|
4589
6337
|
let output = cli.output;
|
|
6338
|
+
let command_summary = command_summary_for_logs();
|
|
6339
|
+
let started_at = now_epoch_ms();
|
|
6340
|
+
|
|
4590
6341
|
if let Err(error) = run_cli(cli).await {
|
|
6342
|
+
let duration_ms = now_epoch_ms().saturating_sub(started_at);
|
|
6343
|
+
append_runtime_log_event(
|
|
6344
|
+
&command_summary,
|
|
6345
|
+
&format!("command failed: {}", error),
|
|
6346
|
+
"error",
|
|
6347
|
+
Some(duration_ms),
|
|
6348
|
+
Some(1),
|
|
6349
|
+
);
|
|
6350
|
+
record_cli_crash_report(&command_summary, &error);
|
|
4591
6351
|
match output {
|
|
4592
6352
|
OutputFormat::Json => {
|
|
4593
6353
|
let payload = serde_json::json!({
|
|
@@ -4605,4 +6365,13 @@ async fn main() {
|
|
|
4605
6365
|
}
|
|
4606
6366
|
std::process::exit(1);
|
|
4607
6367
|
}
|
|
6368
|
+
|
|
6369
|
+
let duration_ms = now_epoch_ms().saturating_sub(started_at);
|
|
6370
|
+
append_runtime_log_event(
|
|
6371
|
+
&command_summary,
|
|
6372
|
+
"command completed",
|
|
6373
|
+
"info",
|
|
6374
|
+
Some(duration_ms),
|
|
6375
|
+
Some(0),
|
|
6376
|
+
);
|
|
4608
6377
|
}
|