@miosa/cli 1.0.2 → 1.0.3

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.
@@ -58,6 +58,15 @@ const TOOL_LIST = [
58
58
  type: "string",
59
59
  description: "Your internal project ID for attribution (optional)",
60
60
  },
61
+ gpu_model: {
62
+ type: "string",
63
+ description: "GPU model to attach (e.g. 'nvidia-a10g', 'nvidia-t4'). Omit for CPU-only.",
64
+ },
65
+ gpu_count: {
66
+ type: "integer",
67
+ description: "Number of GPUs to attach (default: 1 when gpu_model is set).",
68
+ default: 1,
69
+ },
61
70
  },
62
71
  required: ["name"],
63
72
  },
@@ -81,6 +90,72 @@ const TOOL_LIST = [
81
90
  required: ["computer_id"],
82
91
  },
83
92
  },
93
+ {
94
+ name: "computer_get",
95
+ description: "Get details and current status of a computer.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ computer_id: {
100
+ type: "string",
101
+ description: "ID of the computer to fetch.",
102
+ },
103
+ },
104
+ required: ["computer_id"],
105
+ },
106
+ },
107
+ {
108
+ name: "computer_start",
109
+ description: "Start a stopped computer.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ computer_id: { type: "string", description: "Computer ID." },
114
+ },
115
+ required: ["computer_id"],
116
+ },
117
+ },
118
+ {
119
+ name: "computer_stop",
120
+ description: "Stop a running computer.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ computer_id: { type: "string", description: "Computer ID." },
125
+ },
126
+ required: ["computer_id"],
127
+ },
128
+ },
129
+ {
130
+ name: "computer_restart",
131
+ description: "Restart a computer.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ computer_id: { type: "string", description: "Computer ID." },
136
+ },
137
+ required: ["computer_id"],
138
+ },
139
+ },
140
+ {
141
+ name: "computer_update",
142
+ description: "Rename a computer or update its metadata.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ computer_id: { type: "string", description: "Computer ID." },
147
+ name: {
148
+ type: "string",
149
+ description: "New human-readable name for the computer (optional).",
150
+ },
151
+ metadata: {
152
+ type: "object",
153
+ description: "Arbitrary key/value metadata to attach (optional).",
154
+ },
155
+ },
156
+ required: ["computer_id"],
157
+ },
158
+ },
84
159
  // Screenshot
85
160
  {
86
161
  name: "computer_screenshot",
@@ -331,397 +406,3790 @@ const TOOL_LIST = [
331
406
  required: ["computer_id", "path"],
332
407
  },
333
408
  },
334
- // Sandboxes
409
+ // Desktop — extended pointer / keyboard / window / env
335
410
  {
336
- name: "sandbox_create",
337
- description: "Create a new lightweight code sandbox (Firecracker microVM without desktop).",
411
+ name: "computer_right_click",
412
+ description: "Right-click at the given screen coordinates.",
338
413
  inputSchema: {
339
414
  type: "object",
340
415
  properties: {
341
- name: {
416
+ computer_id: { type: "string", description: "Computer ID." },
417
+ x: { type: "integer", description: "X coordinate in pixels" },
418
+ y: { type: "integer", description: "Y coordinate in pixels" },
419
+ },
420
+ required: ["computer_id", "x", "y"],
421
+ },
422
+ },
423
+ {
424
+ name: "computer_mouse_down",
425
+ description: "Press and hold a mouse button at (x, y). Pair with computer_mouse_up to release.",
426
+ inputSchema: {
427
+ type: "object",
428
+ properties: {
429
+ computer_id: { type: "string", description: "Computer ID." },
430
+ x: { type: "integer" },
431
+ y: { type: "integer" },
432
+ button: {
342
433
  type: "string",
343
- description: "Human-readable name for the sandbox",
434
+ enum: ["left", "right", "middle"],
435
+ default: "left",
344
436
  },
345
- template_id: {
437
+ },
438
+ required: ["computer_id", "x", "y"],
439
+ },
440
+ },
441
+ {
442
+ name: "computer_mouse_up",
443
+ description: "Release a held mouse button at (x, y).",
444
+ inputSchema: {
445
+ type: "object",
446
+ properties: {
447
+ computer_id: { type: "string", description: "Computer ID." },
448
+ x: { type: "integer" },
449
+ y: { type: "integer" },
450
+ button: {
346
451
  type: "string",
347
- description: "Template / image ID (default: miosa-sandbox)",
452
+ enum: ["left", "right", "middle"],
453
+ default: "left",
348
454
  },
349
- cpu_count: { type: "integer", description: "vCPU count" },
350
- memory_mb: { type: "integer", description: "Memory in MB" },
351
- timeout_sec: {
352
- type: "integer",
353
- description: "Idle timeout in seconds",
455
+ },
456
+ required: ["computer_id", "x", "y"],
457
+ },
458
+ },
459
+ {
460
+ name: "computer_key_down",
461
+ description: "Press and hold a key without releasing it. Pair with computer_key_up.",
462
+ inputSchema: {
463
+ type: "object",
464
+ properties: {
465
+ computer_id: { type: "string", description: "Computer ID." },
466
+ key: { type: "string", description: "Key name to hold down" },
467
+ },
468
+ required: ["computer_id", "key"],
469
+ },
470
+ },
471
+ {
472
+ name: "computer_key_up",
473
+ description: "Release a previously held key.",
474
+ inputSchema: {
475
+ type: "object",
476
+ properties: {
477
+ computer_id: { type: "string", description: "Computer ID." },
478
+ key: { type: "string", description: "Key name to release" },
479
+ },
480
+ required: ["computer_id", "key"],
481
+ },
482
+ },
483
+ {
484
+ name: "computer_wait",
485
+ description: "Pause execution inside the computer for N seconds.",
486
+ inputSchema: {
487
+ type: "object",
488
+ properties: {
489
+ computer_id: { type: "string", description: "Computer ID." },
490
+ seconds: {
491
+ type: "number",
492
+ description: "Seconds to wait (may be fractional, e.g. 0.5)",
354
493
  },
355
494
  },
495
+ required: ["computer_id", "seconds"],
356
496
  },
357
497
  },
358
498
  {
359
- name: "sandbox_exec",
360
- description: "Run a bash command inside a sandbox and return the output.",
499
+ name: "computer_focus_window",
500
+ description: "Bring a window to the foreground by its window ID.",
361
501
  inputSchema: {
362
502
  type: "object",
363
503
  properties: {
364
- sandbox_id: { type: "string", description: "Sandbox ID" },
365
- command: { type: "string", description: "Command to run" },
366
- cwd: { type: "string", description: "Working directory (optional)" },
367
- timeout: {
368
- type: "integer",
369
- description: "Timeout in seconds (optional)",
504
+ computer_id: { type: "string", description: "Computer ID." },
505
+ window_id: {
506
+ type: "string",
507
+ description: "Window ID from computer_windows",
370
508
  },
371
509
  },
372
- required: ["sandbox_id", "command"],
510
+ required: ["computer_id", "window_id"],
373
511
  },
374
512
  },
375
513
  {
376
- name: "sandbox_destroy",
377
- description: "Destroy a sandbox permanently.",
514
+ name: "computer_set_window_size",
515
+ description: "Resize a window to the given width and height in pixels.",
378
516
  inputSchema: {
379
517
  type: "object",
380
518
  properties: {
381
- sandbox_id: { type: "string", description: "Sandbox ID to destroy" },
519
+ computer_id: { type: "string", description: "Computer ID." },
520
+ window_id: {
521
+ type: "string",
522
+ description: "Window ID from computer_windows",
523
+ },
524
+ width: { type: "integer" },
525
+ height: { type: "integer" },
382
526
  },
383
- required: ["sandbox_id"],
527
+ required: ["computer_id", "window_id", "width", "height"],
384
528
  },
385
529
  },
386
- // Deploy
387
530
  {
388
- name: "deploy",
389
- description: "Trigger a redeploy for an existing MIOSA deployment.",
531
+ name: "computer_set_window_position",
532
+ description: "Move a window to the given screen coordinates.",
390
533
  inputSchema: {
391
534
  type: "object",
392
535
  properties: {
393
- deployment_id: {
536
+ computer_id: { type: "string", description: "Computer ID." },
537
+ window_id: {
394
538
  type: "string",
395
- description: "Deployment ID to redeploy",
539
+ description: "Window ID from computer_windows",
396
540
  },
541
+ x: { type: "integer" },
542
+ y: { type: "integer" },
397
543
  },
398
- required: ["deployment_id"],
544
+ required: ["computer_id", "window_id", "x", "y"],
399
545
  },
400
546
  },
401
- ];
402
- // ── Wire helpers ─────────────────────────────────────────────────────────────
403
- function ok(text) {
404
- return { content: [{ type: "text", text }] };
405
- }
406
- function err(msg) {
407
- return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
408
- }
409
- function image(pngBytes) {
410
- return {
411
- content: [
412
- {
413
- type: "image",
414
- data: pngBytes.toString("base64"),
415
- mimeType: "image/png",
547
+ {
548
+ name: "computer_maximize_window",
549
+ description: "Maximize a window.",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ computer_id: { type: "string", description: "Computer ID." },
554
+ window_id: {
555
+ type: "string",
556
+ description: "Window ID from computer_windows",
557
+ },
416
558
  },
417
- ],
418
- };
419
- }
420
- function send(response) {
421
- process.stdout.write(JSON.stringify(response) + "\n");
422
- }
423
- // ── Tool dispatch ────────────────────────────────────────────────────────────
424
- async function dispatchTool(client, name, args) {
425
- const cid = typeof args["computer_id"] === "string" ? args["computer_id"] : undefined;
426
- try {
427
- // ── Lifecycle ──────────────────────────────────────────────────────────
428
- if (name === "computer_create") {
429
- const body = { name: args["name"] };
430
- if (args["template_type"])
431
- body["template_type"] = args["template_type"];
432
- if (args["size"])
433
- body["size"] = args["size"];
434
- if (args["workspace_id"])
435
- body["workspace_id"] = args["workspace_id"];
436
- if (args["external_workspace_id"])
437
- body["external_workspace_id"] = args["external_workspace_id"];
438
- if (args["external_project_id"])
439
- body["external_project_id"] = args["external_project_id"];
440
- const computer = await client.apiPost("/api/v1/computers", body);
441
- const data = unwrapData(computer);
442
- const id = String(data["id"] ?? "");
443
- const compName = String(data["name"] ?? args["name"]);
444
- // Poll until the computer reaches "active" state (mirrors Python MCP behaviour)
445
- const POLL_INTERVAL_MS = 2_000;
446
- const POLL_TIMEOUT_MS = 30_000;
447
- const deadline = Date.now() + POLL_TIMEOUT_MS;
448
- let finalStatus = String(data["status"] ?? data["state"] ?? "created");
449
- while (finalStatus !== "active" && Date.now() < deadline) {
450
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
451
- try {
452
- const poll = await client.apiGet(`/api/v1/computers/${encodeURIComponent(id)}`);
453
- const pollData = unwrapData(poll);
454
- finalStatus = String(pollData["status"] ?? pollData["state"] ?? finalStatus);
455
- }
456
- catch {
457
- // Transient error during poll — keep waiting
458
- }
459
- }
460
- return ok(`Created computer '${compName}' (id=${id}, status=${finalStatus}).`);
461
- }
462
- if (name === "computer_list") {
463
- const result = await client.apiGet("/api/v1/computers");
464
- const items = listOf(result);
465
- if (items.length === 0)
466
- return ok("No computers found.");
467
- const lines = ["Available computers:"];
468
- for (const c of items) {
469
- const r = c;
470
- lines.push(` ${r["id"]} ${r["name"]} ${r["status"] ?? r["state"]}`);
471
- }
472
- return ok(lines.join("\n"));
473
- }
474
- if (name === "computer_destroy") {
475
- if (!cid)
476
- return err("computer_id is required");
477
- await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}`);
478
- return ok(`Computer ${cid} destroyed.`);
479
- }
480
- // ── Screenshot ────────────────────────────────────────────────────────
481
- if (name === "computer_screenshot") {
482
- if (!cid)
483
- return err("computer_id is required");
484
- const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/screenshot`);
485
- // The API returns { data: { image: "<base64>", format: "png" } }
486
- const data = unwrapData(result);
487
- const b64 = typeof data["image"] === "string" ? data["image"] : null;
488
- if (!b64)
489
- return err("Screenshot API returned no image data");
490
- return image(Buffer.from(b64, "base64"));
491
- }
492
- // ── Pointer ───────────────────────────────────────────────────────────
493
- if (name === "computer_click") {
494
- if (!cid)
495
- return err("computer_id is required");
496
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/click`, {
497
- x: args["x"],
498
- y: args["y"],
559
+ required: ["computer_id", "window_id"],
560
+ },
561
+ },
562
+ {
563
+ name: "computer_minimize_window",
564
+ description: "Minimize (iconify) a window.",
565
+ inputSchema: {
566
+ type: "object",
567
+ properties: {
568
+ computer_id: { type: "string", description: "Computer ID." },
569
+ window_id: {
570
+ type: "string",
571
+ description: "Window ID from computer_windows",
572
+ },
573
+ },
574
+ required: ["computer_id", "window_id"],
575
+ },
576
+ },
577
+ {
578
+ name: "computer_close_window",
579
+ description: "Close a window.",
580
+ inputSchema: {
581
+ type: "object",
582
+ properties: {
583
+ computer_id: { type: "string", description: "Computer ID." },
584
+ window_id: {
585
+ type: "string",
586
+ description: "Window ID from computer_windows",
587
+ },
588
+ },
589
+ required: ["computer_id", "window_id"],
590
+ },
591
+ },
592
+ {
593
+ name: "computer_get_desktop_env",
594
+ description: "Get desktop environment info (name, resolution, session type).",
595
+ inputSchema: {
596
+ type: "object",
597
+ properties: {
598
+ computer_id: { type: "string", description: "Computer ID." },
599
+ },
600
+ required: ["computer_id"],
601
+ },
602
+ },
603
+ {
604
+ name: "computer_set_wallpaper",
605
+ description: "Set the desktop wallpaper. Accepts an absolute path inside the VM (e.g. '/home/ubuntu/bg.png') or an https:// URL.",
606
+ inputSchema: {
607
+ type: "object",
608
+ properties: {
609
+ computer_id: { type: "string", description: "Computer ID." },
610
+ path: {
611
+ type: "string",
612
+ description: "Absolute VM path or https:// URL for the wallpaper image",
613
+ },
614
+ },
615
+ required: ["computer_id", "path"],
616
+ },
617
+ },
618
+ {
619
+ name: "computer_accessibility_tree",
620
+ description: "Get the AT-SPI accessibility tree for the current desktop state. Returns a nested JSON structure describing all visible UI elements with roles, names, bounding boxes, and parent/child relationships.",
621
+ inputSchema: {
622
+ type: "object",
623
+ properties: {
624
+ computer_id: { type: "string", description: "Computer ID." },
625
+ },
626
+ required: ["computer_id"],
627
+ },
628
+ },
629
+ // Files extended
630
+ {
631
+ name: "computer_list_files",
632
+ description: "List directory contents on the computer.",
633
+ inputSchema: {
634
+ type: "object",
635
+ properties: {
636
+ computer_id: { type: "string" },
637
+ path: {
638
+ type: "string",
639
+ description: "Directory path to list (default: /)",
640
+ },
641
+ },
642
+ required: ["computer_id"],
643
+ },
644
+ },
645
+ {
646
+ name: "computer_stat_file",
647
+ description: "Get file/directory metadata (size, type, permissions, mtime) inside the computer.",
648
+ inputSchema: {
649
+ type: "object",
650
+ properties: {
651
+ computer_id: { type: "string" },
652
+ path: { type: "string", description: "Absolute path inside the VM" },
653
+ },
654
+ required: ["computer_id", "path"],
655
+ },
656
+ },
657
+ {
658
+ name: "computer_mkdir",
659
+ description: "Create a directory (and any missing parents) inside the computer.",
660
+ inputSchema: {
661
+ type: "object",
662
+ properties: {
663
+ computer_id: { type: "string" },
664
+ path: {
665
+ type: "string",
666
+ description: "Absolute directory path to create",
667
+ },
668
+ recursive: {
669
+ type: "boolean",
670
+ description: "Create parent directories if missing (default: true)",
671
+ default: true,
672
+ },
673
+ },
674
+ required: ["computer_id", "path"],
675
+ },
676
+ },
677
+ {
678
+ name: "computer_rename_file",
679
+ description: "Rename or move a file/directory inside the computer.",
680
+ inputSchema: {
681
+ type: "object",
682
+ properties: {
683
+ computer_id: { type: "string" },
684
+ source: { type: "string", description: "Current absolute path" },
685
+ dest: { type: "string", description: "New absolute path" },
686
+ },
687
+ required: ["computer_id", "source", "dest"],
688
+ },
689
+ },
690
+ {
691
+ name: "computer_copy_file",
692
+ description: "Copy a file or directory tree inside the computer.",
693
+ inputSchema: {
694
+ type: "object",
695
+ properties: {
696
+ computer_id: { type: "string" },
697
+ source: { type: "string", description: "Source absolute path" },
698
+ dest: { type: "string", description: "Destination absolute path" },
699
+ recursive: {
700
+ type: "boolean",
701
+ description: "Copy directory trees recursively (default: false)",
702
+ default: false,
703
+ },
704
+ },
705
+ required: ["computer_id", "source", "dest"],
706
+ },
707
+ },
708
+ {
709
+ name: "computer_delete_file",
710
+ description: "Delete a file or directory inside the computer.",
711
+ inputSchema: {
712
+ type: "object",
713
+ properties: {
714
+ computer_id: { type: "string" },
715
+ path: { type: "string", description: "Absolute path to delete" },
716
+ },
717
+ required: ["computer_id", "path"],
718
+ },
719
+ },
720
+ {
721
+ name: "computer_upload_file",
722
+ description: "Upload a local file from the host machine into the computer at a given path.",
723
+ inputSchema: {
724
+ type: "object",
725
+ properties: {
726
+ computer_id: { type: "string" },
727
+ local_path: {
728
+ type: "string",
729
+ description: "Absolute path on the local host to upload",
730
+ },
731
+ remote_path: {
732
+ type: "string",
733
+ description: "Destination absolute path inside the VM",
734
+ },
735
+ },
736
+ required: ["computer_id", "local_path", "remote_path"],
737
+ },
738
+ },
739
+ // Checkpoints
740
+ {
741
+ name: "computer_checkpoint_create",
742
+ description: "Save the current state of the computer as a checkpoint (snapshot).",
743
+ inputSchema: {
744
+ type: "object",
745
+ properties: {
746
+ computer_id: { type: "string" },
747
+ comment: {
748
+ type: "string",
749
+ description: "Optional human-readable label for the checkpoint",
750
+ },
751
+ },
752
+ required: ["computer_id"],
753
+ },
754
+ },
755
+ {
756
+ name: "computer_checkpoint_list",
757
+ description: "List all saved checkpoints for the computer.",
758
+ inputSchema: {
759
+ type: "object",
760
+ properties: {
761
+ computer_id: { type: "string" },
762
+ },
763
+ required: ["computer_id"],
764
+ },
765
+ },
766
+ {
767
+ name: "computer_checkpoint_restore",
768
+ description: "Restore the computer to a previously saved checkpoint.",
769
+ inputSchema: {
770
+ type: "object",
771
+ properties: {
772
+ computer_id: { type: "string" },
773
+ checkpoint_id: {
774
+ type: "string",
775
+ description: "ID of the checkpoint to restore",
776
+ },
777
+ },
778
+ required: ["computer_id", "checkpoint_id"],
779
+ },
780
+ },
781
+ {
782
+ name: "computer_checkpoint_delete",
783
+ description: "Delete a saved checkpoint.",
784
+ inputSchema: {
785
+ type: "object",
786
+ properties: {
787
+ computer_id: { type: "string" },
788
+ checkpoint_id: {
789
+ type: "string",
790
+ description: "ID of the checkpoint to delete",
791
+ },
792
+ },
793
+ required: ["computer_id", "checkpoint_id"],
794
+ },
795
+ },
796
+ // Services
797
+ {
798
+ name: "computer_service_create",
799
+ description: "Create and start a long-running background service on the computer (like systemd — process manager for your VM).",
800
+ inputSchema: {
801
+ type: "object",
802
+ properties: {
803
+ computer_id: { type: "string" },
804
+ name: { type: "string", description: "Service name (must be unique)" },
805
+ command: { type: "string", description: "Shell command to run" },
806
+ working_dir: {
807
+ type: "string",
808
+ description: "Working directory for the service (optional)",
809
+ },
810
+ port: {
811
+ type: "integer",
812
+ description: "Port the service listens on (optional)",
813
+ },
814
+ restart_policy: {
815
+ type: "string",
816
+ enum: ["always", "on-failure", "never"],
817
+ description: "Restart behaviour (default: always)",
818
+ },
819
+ },
820
+ required: ["computer_id", "name", "command"],
821
+ },
822
+ },
823
+ {
824
+ name: "computer_service_list",
825
+ description: "List all background services registered on the computer.",
826
+ inputSchema: {
827
+ type: "object",
828
+ properties: {
829
+ computer_id: { type: "string" },
830
+ },
831
+ required: ["computer_id"],
832
+ },
833
+ },
834
+ {
835
+ name: "computer_service_start",
836
+ description: "Start a stopped background service.",
837
+ inputSchema: {
838
+ type: "object",
839
+ properties: {
840
+ computer_id: { type: "string" },
841
+ service_id: { type: "string", description: "Service ID to start" },
842
+ },
843
+ required: ["computer_id", "service_id"],
844
+ },
845
+ },
846
+ {
847
+ name: "computer_service_stop",
848
+ description: "Stop a running background service (sends SIGTERM).",
849
+ inputSchema: {
850
+ type: "object",
851
+ properties: {
852
+ computer_id: { type: "string" },
853
+ service_id: { type: "string", description: "Service ID to stop" },
854
+ },
855
+ required: ["computer_id", "service_id"],
856
+ },
857
+ },
858
+ {
859
+ name: "computer_service_restart",
860
+ description: "Restart a background service (stop then start).",
861
+ inputSchema: {
862
+ type: "object",
863
+ properties: {
864
+ computer_id: { type: "string" },
865
+ service_id: { type: "string", description: "Service ID to restart" },
866
+ },
867
+ required: ["computer_id", "service_id"],
868
+ },
869
+ },
870
+ {
871
+ name: "computer_service_logs",
872
+ description: "Retrieve recent log output from a background service (last 100 lines).",
873
+ inputSchema: {
874
+ type: "object",
875
+ properties: {
876
+ computer_id: { type: "string" },
877
+ service_id: { type: "string", description: "Service ID" },
878
+ },
879
+ required: ["computer_id", "service_id"],
880
+ },
881
+ },
882
+ {
883
+ name: "computer_service_delete",
884
+ description: "Delete a background service (stops it first if running).",
885
+ inputSchema: {
886
+ type: "object",
887
+ properties: {
888
+ computer_id: { type: "string" },
889
+ service_id: { type: "string", description: "Service ID to delete" },
890
+ },
891
+ required: ["computer_id", "service_id"],
892
+ },
893
+ },
894
+ // Env vars
895
+ {
896
+ name: "computer_env_list",
897
+ description: "List all environment variables set on the computer.",
898
+ inputSchema: {
899
+ type: "object",
900
+ properties: {
901
+ computer_id: { type: "string" },
902
+ },
903
+ required: ["computer_id"],
904
+ },
905
+ },
906
+ {
907
+ name: "computer_env_set",
908
+ description: "Set (create or update) an environment variable on the computer.",
909
+ inputSchema: {
910
+ type: "object",
911
+ properties: {
912
+ computer_id: { type: "string" },
913
+ name: { type: "string", description: "Variable name" },
914
+ value: { type: "string", description: "Variable value" },
915
+ },
916
+ required: ["computer_id", "name", "value"],
917
+ },
918
+ },
919
+ {
920
+ name: "computer_env_delete",
921
+ description: "Delete an environment variable from the computer.",
922
+ inputSchema: {
923
+ type: "object",
924
+ properties: {
925
+ computer_id: { type: "string" },
926
+ name: {
927
+ type: "string",
928
+ description: "Variable name to delete",
929
+ },
930
+ },
931
+ required: ["computer_id", "name"],
932
+ },
933
+ },
934
+ // Logs
935
+ {
936
+ name: "computer_logs",
937
+ description: "Get recent VM-level logs from the computer.",
938
+ inputSchema: {
939
+ type: "object",
940
+ properties: {
941
+ computer_id: { type: "string" },
942
+ lines: {
943
+ type: "integer",
944
+ description: "Number of recent log lines to return (optional)",
945
+ },
946
+ },
947
+ required: ["computer_id"],
948
+ },
949
+ },
950
+ // Domains
951
+ {
952
+ name: "computer_domain_add",
953
+ description: "Add a custom domain to the computer. Returns CNAME verification instructions to add to your DNS registrar.",
954
+ inputSchema: {
955
+ type: "object",
956
+ properties: {
957
+ computer_id: { type: "string" },
958
+ fqdn: {
959
+ type: "string",
960
+ description: "Fully-qualified domain name (e.g. app.example.com)",
961
+ },
962
+ },
963
+ required: ["computer_id", "fqdn"],
964
+ },
965
+ },
966
+ {
967
+ name: "computer_domain_list",
968
+ description: "List all custom domains registered for the computer.",
969
+ inputSchema: {
970
+ type: "object",
971
+ properties: {
972
+ computer_id: { type: "string" },
973
+ },
974
+ required: ["computer_id"],
975
+ },
976
+ },
977
+ {
978
+ name: "computer_domain_delete",
979
+ description: "Delete a custom domain mapping from the computer.",
980
+ inputSchema: {
981
+ type: "object",
982
+ properties: {
983
+ computer_id: { type: "string" },
984
+ domain_id: {
985
+ type: "string",
986
+ description: "Domain ID to delete",
987
+ },
988
+ },
989
+ required: ["computer_id", "domain_id"],
990
+ },
991
+ },
992
+ // Sandboxes
993
+ {
994
+ name: "sandbox_create",
995
+ description: "Create a new lightweight code sandbox (Firecracker microVM without desktop).",
996
+ inputSchema: {
997
+ type: "object",
998
+ properties: {
999
+ name: {
1000
+ type: "string",
1001
+ description: "Human-readable name for the sandbox",
1002
+ },
1003
+ template_id: {
1004
+ type: "string",
1005
+ description: "Template / image ID (default: miosa-sandbox)",
1006
+ },
1007
+ cpu_count: { type: "integer", description: "vCPU count" },
1008
+ memory_mb: { type: "integer", description: "Memory in MB" },
1009
+ timeout_sec: {
1010
+ type: "integer",
1011
+ description: "Idle timeout in seconds",
1012
+ },
1013
+ },
1014
+ },
1015
+ },
1016
+ {
1017
+ name: "sandbox_exec",
1018
+ description: "Run a bash command inside a sandbox and return the output.",
1019
+ inputSchema: {
1020
+ type: "object",
1021
+ properties: {
1022
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1023
+ command: { type: "string", description: "Command to run" },
1024
+ cwd: { type: "string", description: "Working directory (optional)" },
1025
+ timeout: {
1026
+ type: "integer",
1027
+ description: "Timeout in seconds (optional)",
1028
+ },
1029
+ },
1030
+ required: ["sandbox_id", "command"],
1031
+ },
1032
+ },
1033
+ {
1034
+ name: "sandbox_destroy",
1035
+ description: "Destroy a sandbox permanently.",
1036
+ inputSchema: {
1037
+ type: "object",
1038
+ properties: {
1039
+ sandbox_id: { type: "string", description: "Sandbox ID to destroy" },
1040
+ },
1041
+ required: ["sandbox_id"],
1042
+ },
1043
+ },
1044
+ // Sandbox lifecycle (list/get/pause/resume)
1045
+ {
1046
+ name: "sandbox_list",
1047
+ description: "List all sandboxes in the tenant.",
1048
+ inputSchema: {
1049
+ type: "object",
1050
+ properties: {
1051
+ state: {
1052
+ type: "string",
1053
+ enum: ["provisioning", "running", "paused", "destroyed", "error"],
1054
+ description: "Filter by state (optional)",
1055
+ },
1056
+ },
1057
+ },
1058
+ },
1059
+ {
1060
+ name: "sandbox_get",
1061
+ description: "Get details of a specific sandbox.",
1062
+ inputSchema: {
1063
+ type: "object",
1064
+ properties: {
1065
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1066
+ },
1067
+ required: ["sandbox_id"],
1068
+ },
1069
+ },
1070
+ {
1071
+ name: "sandbox_pause",
1072
+ description: "Pause a running sandbox (suspends it to save compute).",
1073
+ inputSchema: {
1074
+ type: "object",
1075
+ properties: {
1076
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1077
+ },
1078
+ required: ["sandbox_id"],
1079
+ },
1080
+ },
1081
+ {
1082
+ name: "sandbox_resume",
1083
+ description: "Resume a paused sandbox.",
1084
+ inputSchema: {
1085
+ type: "object",
1086
+ properties: {
1087
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1088
+ },
1089
+ required: ["sandbox_id"],
1090
+ },
1091
+ },
1092
+ // Sandbox files
1093
+ {
1094
+ name: "sandbox_write_file",
1095
+ description: "Write text content to a file path inside a sandbox.",
1096
+ inputSchema: {
1097
+ type: "object",
1098
+ properties: {
1099
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1100
+ path: {
1101
+ type: "string",
1102
+ description: "Absolute path inside the sandbox",
1103
+ },
1104
+ content: { type: "string", description: "File content (text)" },
1105
+ },
1106
+ required: ["sandbox_id", "path", "content"],
1107
+ },
1108
+ },
1109
+ {
1110
+ name: "sandbox_read_file",
1111
+ description: "Read a file from inside a sandbox and return its content as text.",
1112
+ inputSchema: {
1113
+ type: "object",
1114
+ properties: {
1115
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1116
+ path: {
1117
+ type: "string",
1118
+ description: "Absolute path inside the sandbox",
1119
+ },
1120
+ },
1121
+ required: ["sandbox_id", "path"],
1122
+ },
1123
+ },
1124
+ {
1125
+ name: "sandbox_list_files",
1126
+ description: "List files in a directory inside a sandbox.",
1127
+ inputSchema: {
1128
+ type: "object",
1129
+ properties: {
1130
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1131
+ path: {
1132
+ type: "string",
1133
+ description: "Directory path to list (default: /workspace)",
1134
+ default: "/workspace",
1135
+ },
1136
+ depth: { type: "integer", description: "Recursion depth (optional)" },
1137
+ },
1138
+ required: ["sandbox_id"],
1139
+ },
1140
+ },
1141
+ {
1142
+ name: "sandbox_upload",
1143
+ description: "Upload a file (UTF-8 text or base64 binary) into a sandbox.",
1144
+ inputSchema: {
1145
+ type: "object",
1146
+ properties: {
1147
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1148
+ path: {
1149
+ type: "string",
1150
+ description: "Destination path inside the sandbox",
1151
+ },
1152
+ content: {
1153
+ type: "string",
1154
+ description: "File content as UTF-8 text or base64-encoded binary",
1155
+ },
1156
+ },
1157
+ required: ["sandbox_id", "path", "content"],
1158
+ },
1159
+ },
1160
+ // Sandbox exec
1161
+ {
1162
+ name: "sandbox_python",
1163
+ description: "Run Python code inside a sandbox and return stdout, stderr, and exit code.",
1164
+ inputSchema: {
1165
+ type: "object",
1166
+ properties: {
1167
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1168
+ code: { type: "string", description: "Python code to execute" },
1169
+ timeout: {
1170
+ type: "integer",
1171
+ description: "Timeout in seconds (optional)",
1172
+ },
1173
+ },
1174
+ required: ["sandbox_id", "code"],
1175
+ },
1176
+ },
1177
+ // Sandbox snapshots
1178
+ {
1179
+ name: "sandbox_snapshot_create",
1180
+ description: "Create a snapshot of the current sandbox state.",
1181
+ inputSchema: {
1182
+ type: "object",
1183
+ properties: {
1184
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1185
+ comment: {
1186
+ type: "string",
1187
+ description: "Optional comment for the snapshot",
1188
+ },
1189
+ },
1190
+ required: ["sandbox_id"],
1191
+ },
1192
+ },
1193
+ {
1194
+ name: "sandbox_snapshot_list",
1195
+ description: "List all snapshots for a sandbox.",
1196
+ inputSchema: {
1197
+ type: "object",
1198
+ properties: {
1199
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1200
+ },
1201
+ required: ["sandbox_id"],
1202
+ },
1203
+ },
1204
+ {
1205
+ name: "sandbox_snapshot_restore",
1206
+ description: "Restore a sandbox from a specific snapshot.",
1207
+ inputSchema: {
1208
+ type: "object",
1209
+ properties: {
1210
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1211
+ snapshot_id: {
1212
+ type: "string",
1213
+ description: "Snapshot ID to restore from",
1214
+ },
1215
+ },
1216
+ required: ["sandbox_id", "snapshot_id"],
1217
+ },
1218
+ },
1219
+ // Sandbox logs
1220
+ {
1221
+ name: "sandbox_logs",
1222
+ description: "Get logs from a sandbox.",
1223
+ inputSchema: {
1224
+ type: "object",
1225
+ properties: {
1226
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1227
+ lines: {
1228
+ type: "integer",
1229
+ description: "Number of log lines to return (optional)",
1230
+ },
1231
+ },
1232
+ required: ["sandbox_id"],
1233
+ },
1234
+ },
1235
+ // Sandbox preview
1236
+ {
1237
+ name: "sandbox_expose",
1238
+ description: "Expose a port on a sandbox and return a publicly accessible preview URL.",
1239
+ inputSchema: {
1240
+ type: "object",
1241
+ properties: {
1242
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1243
+ port: {
1244
+ type: "integer",
1245
+ description: "Port number to expose (optional; exposes default port if omitted)",
1246
+ },
1247
+ },
1248
+ required: ["sandbox_id"],
1249
+ },
1250
+ },
1251
+ // Sandbox deploy
1252
+ {
1253
+ name: "sandbox_deploy",
1254
+ description: "Deploy sandbox contents to production.",
1255
+ inputSchema: {
1256
+ type: "object",
1257
+ properties: {
1258
+ sandbox_id: { type: "string", description: "Sandbox ID" },
1259
+ name: { type: "string", description: "Deployment name (optional)" },
1260
+ output_path: {
1261
+ type: "string",
1262
+ description: "Path inside the sandbox to deploy (optional)",
1263
+ },
1264
+ entrypoint: {
1265
+ type: "string",
1266
+ description: "Entrypoint command or file (optional)",
1267
+ },
1268
+ domain: { type: "string", description: "Custom domain (optional)" },
1269
+ },
1270
+ required: ["sandbox_id"],
1271
+ },
1272
+ },
1273
+ // Sandbox templates
1274
+ {
1275
+ name: "sandbox_template_list",
1276
+ description: "List available sandbox templates.",
1277
+ inputSchema: { type: "object", properties: {} },
1278
+ },
1279
+ {
1280
+ name: "sandbox_template_create",
1281
+ description: "Create a custom sandbox template from a build spec.",
1282
+ inputSchema: {
1283
+ type: "object",
1284
+ properties: {
1285
+ name: { type: "string", description: "Template name" },
1286
+ build_spec: {
1287
+ type: "object",
1288
+ description: "Build specification (Dockerfile, packages, etc.)",
1289
+ },
1290
+ slug: {
1291
+ type: "string",
1292
+ description: "URL-friendly identifier (optional)",
1293
+ },
1294
+ description: {
1295
+ type: "string",
1296
+ description: "Human-readable description (optional)",
1297
+ },
1298
+ },
1299
+ required: ["name", "build_spec"],
1300
+ },
1301
+ },
1302
+ // Deploy
1303
+ {
1304
+ name: "deploy",
1305
+ description: "Trigger a redeploy for an existing MIOSA deployment.",
1306
+ inputSchema: {
1307
+ type: "object",
1308
+ properties: {
1309
+ deployment_id: {
1310
+ type: "string",
1311
+ description: "Deployment ID to redeploy",
1312
+ },
1313
+ },
1314
+ required: ["deployment_id"],
1315
+ },
1316
+ },
1317
+ // Deployments
1318
+ {
1319
+ name: "deployment_list",
1320
+ description: "List all deployments in the tenant.",
1321
+ inputSchema: { type: "object", properties: {} },
1322
+ },
1323
+ {
1324
+ name: "deployment_get",
1325
+ description: "Get details of a specific deployment.",
1326
+ inputSchema: {
1327
+ type: "object",
1328
+ properties: {
1329
+ deployment_id: { type: "string", description: "Deployment ID" },
1330
+ },
1331
+ required: ["deployment_id"],
1332
+ },
1333
+ },
1334
+ {
1335
+ name: "deployment_create",
1336
+ description: "Create a new deployment.",
1337
+ inputSchema: {
1338
+ type: "object",
1339
+ properties: {
1340
+ name: { type: "string", description: "Deployment name" },
1341
+ type: {
1342
+ type: "string",
1343
+ description: "Deployment type (e.g. web, worker)",
1344
+ },
1345
+ source: { type: "object", description: "Source configuration" },
1346
+ env_vars: {
1347
+ type: "object",
1348
+ description: "Environment variables as key-value pairs",
1349
+ },
1350
+ region: { type: "string", description: "Deployment region (optional)" },
1351
+ },
1352
+ required: ["name"],
1353
+ },
1354
+ },
1355
+ {
1356
+ name: "deployment_delete",
1357
+ description: "Delete a deployment permanently.",
1358
+ inputSchema: {
1359
+ type: "object",
1360
+ properties: {
1361
+ deployment_id: {
1362
+ type: "string",
1363
+ description: "Deployment ID to delete",
1364
+ },
1365
+ },
1366
+ required: ["deployment_id"],
1367
+ },
1368
+ },
1369
+ {
1370
+ name: "deployment_publish",
1371
+ description: "Publish a new version of a deployment.",
1372
+ inputSchema: {
1373
+ type: "object",
1374
+ properties: {
1375
+ deployment_id: { type: "string", description: "Deployment ID" },
1376
+ source: {
1377
+ type: "object",
1378
+ description: "Source configuration for the new version",
1379
+ },
1380
+ },
1381
+ required: ["deployment_id"],
1382
+ },
1383
+ },
1384
+ {
1385
+ name: "deployment_rollback",
1386
+ description: "Rollback a deployment to a previous version.",
1387
+ inputSchema: {
1388
+ type: "object",
1389
+ properties: {
1390
+ deployment_id: { type: "string", description: "Deployment ID" },
1391
+ version_id: {
1392
+ type: "string",
1393
+ description: "Version ID to roll back to",
1394
+ },
1395
+ },
1396
+ required: ["deployment_id", "version_id"],
1397
+ },
1398
+ },
1399
+ {
1400
+ name: "deployment_env_list",
1401
+ description: "List environment variables for a deployment.",
1402
+ inputSchema: {
1403
+ type: "object",
1404
+ properties: {
1405
+ deployment_id: { type: "string", description: "Deployment ID" },
1406
+ },
1407
+ required: ["deployment_id"],
1408
+ },
1409
+ },
1410
+ {
1411
+ name: "deployment_env_set",
1412
+ description: "Set (create or update) an environment variable for a deployment.",
1413
+ inputSchema: {
1414
+ type: "object",
1415
+ properties: {
1416
+ deployment_id: { type: "string", description: "Deployment ID" },
1417
+ key: { type: "string", description: "Environment variable name" },
1418
+ value: { type: "string", description: "Environment variable value" },
1419
+ },
1420
+ required: ["deployment_id", "key", "value"],
1421
+ },
1422
+ },
1423
+ {
1424
+ name: "deployment_logs",
1425
+ description: "Get logs for a deployment.",
1426
+ inputSchema: {
1427
+ type: "object",
1428
+ properties: {
1429
+ deployment_id: { type: "string", description: "Deployment ID" },
1430
+ lines: {
1431
+ type: "integer",
1432
+ description: "Number of log lines to return (default: 100)",
1433
+ default: 100,
1434
+ },
1435
+ since: {
1436
+ type: "string",
1437
+ description: "ISO 8601 timestamp to fetch logs from (optional)",
1438
+ },
1439
+ },
1440
+ required: ["deployment_id"],
1441
+ },
1442
+ },
1443
+ {
1444
+ name: "deployment_version_list",
1445
+ description: "List all versions of a deployment.",
1446
+ inputSchema: {
1447
+ type: "object",
1448
+ properties: {
1449
+ deployment_id: { type: "string", description: "Deployment ID" },
1450
+ },
1451
+ required: ["deployment_id"],
1452
+ },
1453
+ },
1454
+ {
1455
+ name: "deployment_version_promote",
1456
+ description: "Promote a specific version to be the active deployment.",
1457
+ inputSchema: {
1458
+ type: "object",
1459
+ properties: {
1460
+ deployment_id: { type: "string", description: "Deployment ID" },
1461
+ version_id: { type: "string", description: "Version ID to promote" },
1462
+ },
1463
+ required: ["deployment_id", "version_id"],
1464
+ },
1465
+ },
1466
+ // Storage
1467
+ {
1468
+ name: "storage_bucket_list",
1469
+ description: "List all storage buckets in the tenant.",
1470
+ inputSchema: { type: "object", properties: {} },
1471
+ },
1472
+ {
1473
+ name: "storage_bucket_create",
1474
+ description: "Create a new storage bucket.",
1475
+ inputSchema: {
1476
+ type: "object",
1477
+ properties: {
1478
+ name: { type: "string", description: "Bucket name" },
1479
+ region: { type: "string", description: "Bucket region (optional)" },
1480
+ public: {
1481
+ type: "boolean",
1482
+ description: "Whether the bucket is publicly readable (default: false)",
1483
+ default: false,
1484
+ },
1485
+ },
1486
+ required: ["name"],
1487
+ },
1488
+ },
1489
+ {
1490
+ name: "storage_bucket_delete",
1491
+ description: "Delete a storage bucket.",
1492
+ inputSchema: {
1493
+ type: "object",
1494
+ properties: {
1495
+ bucket_id: {
1496
+ type: "string",
1497
+ description: "Bucket ID or name to delete",
1498
+ },
1499
+ },
1500
+ required: ["bucket_id"],
1501
+ },
1502
+ },
1503
+ {
1504
+ name: "storage_object_list",
1505
+ description: "List objects in a storage bucket, optionally filtered by prefix.",
1506
+ inputSchema: {
1507
+ type: "object",
1508
+ properties: {
1509
+ bucket_id: { type: "string", description: "Bucket ID or name" },
1510
+ prefix: {
1511
+ type: "string",
1512
+ description: "Key prefix to filter by (optional)",
1513
+ },
1514
+ },
1515
+ required: ["bucket_id"],
1516
+ },
1517
+ },
1518
+ {
1519
+ name: "storage_object_upload",
1520
+ description: "Upload an object to a storage bucket.",
1521
+ inputSchema: {
1522
+ type: "object",
1523
+ properties: {
1524
+ bucket_id: { type: "string", description: "Bucket ID or name" },
1525
+ key: { type: "string", description: "Object key (path within bucket)" },
1526
+ content: {
1527
+ type: "string",
1528
+ description: "Object content (text or base64-encoded binary)",
1529
+ },
1530
+ content_type: {
1531
+ type: "string",
1532
+ description: "MIME type of the object (optional)",
1533
+ },
1534
+ },
1535
+ required: ["bucket_id", "key", "content"],
1536
+ },
1537
+ },
1538
+ {
1539
+ name: "storage_object_download",
1540
+ description: "Download an object from a storage bucket.",
1541
+ inputSchema: {
1542
+ type: "object",
1543
+ properties: {
1544
+ bucket_id: { type: "string", description: "Bucket ID or name" },
1545
+ key: { type: "string", description: "Object key to download" },
1546
+ },
1547
+ required: ["bucket_id", "key"],
1548
+ },
1549
+ },
1550
+ {
1551
+ name: "storage_object_delete",
1552
+ description: "Delete an object from a storage bucket.",
1553
+ inputSchema: {
1554
+ type: "object",
1555
+ properties: {
1556
+ bucket_id: { type: "string", description: "Bucket ID or name" },
1557
+ key: { type: "string", description: "Object key to delete" },
1558
+ },
1559
+ required: ["bucket_id", "key"],
1560
+ },
1561
+ },
1562
+ {
1563
+ name: "storage_presign",
1564
+ description: "Get a presigned URL for temporary access to a storage object.",
1565
+ inputSchema: {
1566
+ type: "object",
1567
+ properties: {
1568
+ bucket_id: { type: "string", description: "Bucket ID or name" },
1569
+ key: { type: "string", description: "Object key" },
1570
+ expires_in: {
1571
+ type: "integer",
1572
+ description: "URL expiry in seconds (default: 3600)",
1573
+ default: 3600,
1574
+ },
1575
+ method: {
1576
+ type: "string",
1577
+ enum: ["GET", "PUT"],
1578
+ description: "HTTP method (default: GET)",
1579
+ default: "GET",
1580
+ },
1581
+ },
1582
+ required: ["bucket_id", "key"],
1583
+ },
1584
+ },
1585
+ // Databases
1586
+ {
1587
+ name: "database_list",
1588
+ description: "List all managed databases in the tenant.",
1589
+ inputSchema: { type: "object", properties: {} },
1590
+ },
1591
+ {
1592
+ name: "database_create",
1593
+ description: "Create a new managed database.",
1594
+ inputSchema: {
1595
+ type: "object",
1596
+ properties: {
1597
+ name: { type: "string", description: "Database name" },
1598
+ engine: {
1599
+ type: "string",
1600
+ enum: ["postgres", "mysql", "redis"],
1601
+ description: "Database engine",
1602
+ },
1603
+ version: { type: "string", description: "Engine version (optional)" },
1604
+ size: { type: "string", description: "Database size/tier (optional)" },
1605
+ region: { type: "string", description: "Region (optional)" },
1606
+ },
1607
+ required: ["name", "engine"],
1608
+ },
1609
+ },
1610
+ {
1611
+ name: "database_get",
1612
+ description: "Get details of a specific database.",
1613
+ inputSchema: {
1614
+ type: "object",
1615
+ properties: {
1616
+ database_id: { type: "string", description: "Database ID" },
1617
+ },
1618
+ required: ["database_id"],
1619
+ },
1620
+ },
1621
+ {
1622
+ name: "database_delete",
1623
+ description: "Delete a managed database permanently.",
1624
+ inputSchema: {
1625
+ type: "object",
1626
+ properties: {
1627
+ database_id: { type: "string", description: "Database ID to delete" },
1628
+ },
1629
+ required: ["database_id"],
1630
+ },
1631
+ },
1632
+ {
1633
+ name: "database_credentials",
1634
+ description: "Get the connection string and credentials for a database.",
1635
+ inputSchema: {
1636
+ type: "object",
1637
+ properties: {
1638
+ database_id: { type: "string", description: "Database ID" },
1639
+ },
1640
+ required: ["database_id"],
1641
+ },
1642
+ },
1643
+ {
1644
+ name: "database_logs",
1645
+ description: "Get logs for a managed database.",
1646
+ inputSchema: {
1647
+ type: "object",
1648
+ properties: {
1649
+ database_id: { type: "string", description: "Database ID" },
1650
+ lines: {
1651
+ type: "integer",
1652
+ description: "Number of log lines to return (default: 100)",
1653
+ default: 100,
1654
+ },
1655
+ since: {
1656
+ type: "string",
1657
+ description: "ISO 8601 timestamp to fetch logs from (optional)",
1658
+ },
1659
+ },
1660
+ required: ["database_id"],
1661
+ },
1662
+ },
1663
+ // Workspaces
1664
+ {
1665
+ name: "workspace_list",
1666
+ description: "List all workspaces in the tenant.",
1667
+ inputSchema: { type: "object", properties: {} },
1668
+ },
1669
+ {
1670
+ name: "workspace_create",
1671
+ description: "Create a new workspace.",
1672
+ inputSchema: {
1673
+ type: "object",
1674
+ properties: {
1675
+ name: { type: "string", description: "Workspace name" },
1676
+ description: {
1677
+ type: "string",
1678
+ description: "Workspace description (optional)",
1679
+ },
1680
+ },
1681
+ required: ["name"],
1682
+ },
1683
+ },
1684
+ {
1685
+ name: "workspace_get",
1686
+ description: "Get details of a specific workspace.",
1687
+ inputSchema: {
1688
+ type: "object",
1689
+ properties: {
1690
+ workspace_id: { type: "string", description: "Workspace ID" },
1691
+ },
1692
+ required: ["workspace_id"],
1693
+ },
1694
+ },
1695
+ {
1696
+ name: "workspace_update",
1697
+ description: "Update a workspace's name or description.",
1698
+ inputSchema: {
1699
+ type: "object",
1700
+ properties: {
1701
+ workspace_id: { type: "string", description: "Workspace ID" },
1702
+ name: { type: "string", description: "New workspace name (optional)" },
1703
+ description: {
1704
+ type: "string",
1705
+ description: "New description (optional)",
1706
+ },
1707
+ },
1708
+ required: ["workspace_id"],
1709
+ },
1710
+ },
1711
+ {
1712
+ name: "workspace_stats",
1713
+ description: "Get resource statistics for a workspace (computers, sandboxes, databases, etc.).",
1714
+ inputSchema: {
1715
+ type: "object",
1716
+ properties: {
1717
+ workspace_id: { type: "string", description: "Workspace ID" },
1718
+ },
1719
+ required: ["workspace_id"],
1720
+ },
1721
+ },
1722
+ {
1723
+ name: "workspace_usage",
1724
+ description: "Get usage data (compute hours, storage, bandwidth) for a workspace.",
1725
+ inputSchema: {
1726
+ type: "object",
1727
+ properties: {
1728
+ workspace_id: { type: "string", description: "Workspace ID" },
1729
+ period: {
1730
+ type: "string",
1731
+ description: "Billing period (e.g. '2026-05'). Defaults to current month.",
1732
+ },
1733
+ },
1734
+ required: ["workspace_id"],
1735
+ },
1736
+ },
1737
+ // Billing
1738
+ {
1739
+ name: "billing_usage",
1740
+ description: "Get current billing period usage for the tenant.",
1741
+ inputSchema: { type: "object", properties: {} },
1742
+ },
1743
+ {
1744
+ name: "billing_plan",
1745
+ description: "Get the current billing plan details for the tenant.",
1746
+ inputSchema: { type: "object", properties: {} },
1747
+ },
1748
+ // Tunnels / Port forwarding
1749
+ {
1750
+ name: "computer_expose_port",
1751
+ description: "Expose a port on the computer and return the public URL. The URL follows the pattern https://{port}-{slug}.computer.miosa.ai.",
1752
+ inputSchema: {
1753
+ type: "object",
1754
+ properties: {
1755
+ computer_id: { type: "string", description: "Computer ID." },
1756
+ port: { type: "integer", description: "Port number to expose" },
1757
+ protocol: {
1758
+ type: "string",
1759
+ enum: ["http", "https", "tcp"],
1760
+ description: "Protocol (default: http)",
1761
+ },
1762
+ },
1763
+ required: ["computer_id", "port"],
1764
+ },
1765
+ },
1766
+ {
1767
+ name: "computer_list_ports",
1768
+ description: "List all currently exposed ports on the computer.",
1769
+ inputSchema: {
1770
+ type: "object",
1771
+ properties: {
1772
+ computer_id: { type: "string", description: "Computer ID." },
1773
+ },
1774
+ required: ["computer_id"],
1775
+ },
1776
+ },
1777
+ {
1778
+ name: "computer_preview_url",
1779
+ description: "Return the public preview URL for a given port on the computer. Format: https://{port}-{slug}.computer.miosa.ai",
1780
+ inputSchema: {
1781
+ type: "object",
1782
+ properties: {
1783
+ computer_id: { type: "string", description: "Computer ID." },
1784
+ port: { type: "integer", description: "Port number" },
1785
+ },
1786
+ required: ["computer_id", "port"],
1787
+ },
1788
+ },
1789
+ // Network policy
1790
+ {
1791
+ name: "computer_network_policy_get",
1792
+ description: "Get the current network policy (firewall rules) for the computer.",
1793
+ inputSchema: {
1794
+ type: "object",
1795
+ properties: {
1796
+ computer_id: { type: "string", description: "Computer ID." },
1797
+ },
1798
+ required: ["computer_id"],
1799
+ },
1800
+ },
1801
+ {
1802
+ name: "computer_network_policy_set",
1803
+ description: "Set the network policy (firewall rules) for the computer.",
1804
+ inputSchema: {
1805
+ type: "object",
1806
+ properties: {
1807
+ computer_id: { type: "string", description: "Computer ID." },
1808
+ rules: {
1809
+ type: "array",
1810
+ items: { type: "object" },
1811
+ description: "List of firewall rule objects (e.g. {direction, protocol, port, action})",
1812
+ },
1813
+ default_effect: {
1814
+ type: "string",
1815
+ enum: ["allow", "deny"],
1816
+ description: "Default action when no rule matches (default: allow)",
1817
+ },
1818
+ },
1819
+ required: ["computer_id", "rules"],
1820
+ },
1821
+ },
1822
+ {
1823
+ name: "computer_network_policy_reset",
1824
+ description: "Reset the network policy for the computer to the platform default (allow all).",
1825
+ inputSchema: {
1826
+ type: "object",
1827
+ properties: {
1828
+ computer_id: { type: "string", description: "Computer ID." },
1829
+ },
1830
+ required: ["computer_id"],
1831
+ },
1832
+ },
1833
+ // Webhooks
1834
+ {
1835
+ name: "webhook_list",
1836
+ description: "List all webhooks registered in the tenant.",
1837
+ inputSchema: { type: "object", properties: {} },
1838
+ },
1839
+ {
1840
+ name: "webhook_create",
1841
+ description: "Create a new webhook endpoint.",
1842
+ inputSchema: {
1843
+ type: "object",
1844
+ properties: {
1845
+ url: {
1846
+ type: "string",
1847
+ description: "HTTPS URL to deliver webhook events to",
1848
+ },
1849
+ events: {
1850
+ type: "array",
1851
+ items: { type: "string" },
1852
+ description: "List of event types to subscribe to (e.g. ['computer.started', 'computer.stopped'])",
1853
+ },
1854
+ },
1855
+ required: ["url", "events"],
1856
+ },
1857
+ },
1858
+ {
1859
+ name: "webhook_delete",
1860
+ description: "Delete a webhook.",
1861
+ inputSchema: {
1862
+ type: "object",
1863
+ properties: {
1864
+ webhook_id: { type: "string", description: "Webhook ID to delete" },
1865
+ },
1866
+ required: ["webhook_id"],
1867
+ },
1868
+ },
1869
+ {
1870
+ name: "webhook_test",
1871
+ description: "Send a test event delivery to a webhook endpoint.",
1872
+ inputSchema: {
1873
+ type: "object",
1874
+ properties: {
1875
+ webhook_id: { type: "string", description: "Webhook ID to test" },
1876
+ },
1877
+ required: ["webhook_id"],
1878
+ },
1879
+ },
1880
+ // Functions
1881
+ {
1882
+ name: "function_list",
1883
+ description: "List all serverless functions in the tenant.",
1884
+ inputSchema: { type: "object", properties: {} },
1885
+ },
1886
+ {
1887
+ name: "function_create",
1888
+ description: "Create a new serverless function.",
1889
+ inputSchema: {
1890
+ type: "object",
1891
+ properties: {
1892
+ name: { type: "string", description: "Function name" },
1893
+ runtime: {
1894
+ type: "string",
1895
+ description: "Runtime identifier (e.g. 'node20', 'python311', 'go122')",
1896
+ },
1897
+ code: {
1898
+ type: "string",
1899
+ description: "Inline function source code (optional)",
1900
+ },
1901
+ },
1902
+ required: ["name", "runtime"],
1903
+ },
1904
+ },
1905
+ {
1906
+ name: "function_invoke",
1907
+ description: "Invoke a serverless function and return its response.",
1908
+ inputSchema: {
1909
+ type: "object",
1910
+ properties: {
1911
+ function_id: { type: "string", description: "Function ID to invoke" },
1912
+ payload: {
1913
+ type: "object",
1914
+ description: "JSON payload to pass to the function (optional)",
1915
+ },
1916
+ },
1917
+ required: ["function_id"],
1918
+ },
1919
+ },
1920
+ {
1921
+ name: "function_delete",
1922
+ description: "Delete a serverless function permanently.",
1923
+ inputSchema: {
1924
+ type: "object",
1925
+ properties: {
1926
+ function_id: { type: "string", description: "Function ID to delete" },
1927
+ },
1928
+ required: ["function_id"],
1929
+ },
1930
+ },
1931
+ // API Keys
1932
+ {
1933
+ name: "api_key_list",
1934
+ description: "List all API keys for the tenant.",
1935
+ inputSchema: { type: "object", properties: {} },
1936
+ },
1937
+ {
1938
+ name: "api_key_create",
1939
+ description: "Create a new API key.",
1940
+ inputSchema: {
1941
+ type: "object",
1942
+ properties: {
1943
+ name: {
1944
+ type: "string",
1945
+ description: "Human-readable label for the key",
1946
+ },
1947
+ scopes: {
1948
+ type: "array",
1949
+ items: { type: "string" },
1950
+ description: "Permission scopes for the key (optional; defaults to full access)",
1951
+ },
1952
+ },
1953
+ required: ["name"],
1954
+ },
1955
+ },
1956
+ {
1957
+ name: "api_key_delete",
1958
+ description: "Revoke and delete an API key.",
1959
+ inputSchema: {
1960
+ type: "object",
1961
+ properties: {
1962
+ key_id: { type: "string", description: "API key ID to delete" },
1963
+ },
1964
+ required: ["key_id"],
1965
+ },
1966
+ },
1967
+ // Regions
1968
+ {
1969
+ name: "region_list",
1970
+ description: "List available regions and their GPU availability.",
1971
+ inputSchema: { type: "object", properties: {} },
1972
+ },
1973
+ {
1974
+ name: "computer_list_regions",
1975
+ description: "List available compute regions with GPU availability details.",
1976
+ inputSchema: { type: "object", properties: {} },
1977
+ },
1978
+ // Computer templates
1979
+ {
1980
+ name: "computer_template_list",
1981
+ description: "List computer templates available in a workspace.",
1982
+ inputSchema: {
1983
+ type: "object",
1984
+ properties: {
1985
+ workspace_id: {
1986
+ type: "string",
1987
+ description: "Workspace ID to list templates for",
1988
+ },
1989
+ },
1990
+ required: ["workspace_id"],
1991
+ },
1992
+ },
1993
+ {
1994
+ name: "computer_template_create",
1995
+ description: "Create a new computer template in a workspace.",
1996
+ inputSchema: {
1997
+ type: "object",
1998
+ properties: {
1999
+ workspace_id: {
2000
+ type: "string",
2001
+ description: "Workspace ID to create the template in",
2002
+ },
2003
+ name: {
2004
+ type: "string",
2005
+ description: "Human-readable name for the template",
2006
+ },
2007
+ template_type: {
2008
+ type: "string",
2009
+ description: "Base template type (e.g. miosa-desktop)",
2010
+ },
2011
+ size: {
2012
+ type: "string",
2013
+ enum: ["small", "medium", "large", "xl"],
2014
+ description: "VM size for the template",
2015
+ },
2016
+ selected_apps: {
2017
+ type: "array",
2018
+ items: { type: "string" },
2019
+ description: "List of app identifiers to pre-install",
2020
+ },
2021
+ settings: {
2022
+ type: "object",
2023
+ description: "Additional template settings (key-value pairs)",
2024
+ },
2025
+ },
2026
+ required: ["workspace_id", "name"],
2027
+ },
2028
+ },
2029
+ // Settings
2030
+ {
2031
+ name: "settings_get",
2032
+ description: "Get all tenant-level settings.",
2033
+ inputSchema: { type: "object", properties: {} },
2034
+ },
2035
+ {
2036
+ name: "settings_get_branding",
2037
+ description: "Get tenant branding settings (logo, wallpaper, colours).",
2038
+ inputSchema: { type: "object", properties: {} },
2039
+ },
2040
+ {
2041
+ name: "settings_update_branding",
2042
+ description: "Update tenant branding settings.",
2043
+ inputSchema: {
2044
+ type: "object",
2045
+ properties: {
2046
+ desktop_wallpaper_url: {
2047
+ type: "string",
2048
+ description: "HTTPS URL for the default desktop wallpaper (optional)",
2049
+ },
2050
+ logo_url: {
2051
+ type: "string",
2052
+ description: "HTTPS URL for the tenant logo (optional)",
2053
+ },
2054
+ },
2055
+ },
2056
+ },
2057
+ {
2058
+ name: "settings_compute_pricing",
2059
+ description: "Get compute pricing information for available sizes and GPU types.",
2060
+ inputSchema: { type: "object", properties: {} },
2061
+ },
2062
+ // Sandbox template extensions
2063
+ {
2064
+ name: "sandbox_template_get",
2065
+ description: "Get details of a specific sandbox template.",
2066
+ inputSchema: {
2067
+ type: "object",
2068
+ properties: {
2069
+ template_id: {
2070
+ type: "string",
2071
+ description: "Sandbox template ID or slug",
2072
+ },
2073
+ },
2074
+ required: ["template_id"],
2075
+ },
2076
+ },
2077
+ {
2078
+ name: "sandbox_template_builds",
2079
+ description: "List builds for a sandbox template.",
2080
+ inputSchema: {
2081
+ type: "object",
2082
+ properties: {
2083
+ template_id: {
2084
+ type: "string",
2085
+ description: "Sandbox template ID or slug",
2086
+ },
2087
+ },
2088
+ required: ["template_id"],
2089
+ },
2090
+ },
2091
+ // Cron jobs
2092
+ {
2093
+ name: "cron_list",
2094
+ description: "List all cron jobs in the tenant, optionally filtered by computer.",
2095
+ inputSchema: {
2096
+ type: "object",
2097
+ properties: {
2098
+ computer_id: {
2099
+ type: "string",
2100
+ description: "Filter cron jobs by computer ID (optional)",
2101
+ },
2102
+ },
2103
+ },
2104
+ },
2105
+ {
2106
+ name: "cron_create",
2107
+ description: "Create a new cron job that runs a command on a schedule.",
2108
+ inputSchema: {
2109
+ type: "object",
2110
+ properties: {
2111
+ computer_id: {
2112
+ type: "string",
2113
+ description: "ID of the computer to run the cron job on",
2114
+ },
2115
+ schedule: {
2116
+ type: "string",
2117
+ description: "Cron schedule expression (e.g. '0 * * * *' for hourly)",
2118
+ },
2119
+ command: {
2120
+ type: "string",
2121
+ description: "Shell command to execute",
2122
+ },
2123
+ name: {
2124
+ type: "string",
2125
+ description: "Human-readable name for the cron job (optional)",
2126
+ },
2127
+ },
2128
+ required: ["computer_id", "schedule", "command"],
2129
+ },
2130
+ },
2131
+ {
2132
+ name: "cron_get",
2133
+ description: "Get details of a specific cron job.",
2134
+ inputSchema: {
2135
+ type: "object",
2136
+ properties: {
2137
+ cron_id: { type: "string", description: "Cron job ID" },
2138
+ },
2139
+ required: ["cron_id"],
2140
+ },
2141
+ },
2142
+ {
2143
+ name: "cron_delete",
2144
+ description: "Delete a cron job permanently.",
2145
+ inputSchema: {
2146
+ type: "object",
2147
+ properties: {
2148
+ cron_id: { type: "string", description: "Cron job ID to delete" },
2149
+ },
2150
+ required: ["cron_id"],
2151
+ },
2152
+ },
2153
+ {
2154
+ name: "cron_pause",
2155
+ description: "Pause a cron job so it stops running on schedule.",
2156
+ inputSchema: {
2157
+ type: "object",
2158
+ properties: {
2159
+ cron_id: { type: "string", description: "Cron job ID to pause" },
2160
+ },
2161
+ required: ["cron_id"],
2162
+ },
2163
+ },
2164
+ {
2165
+ name: "cron_resume",
2166
+ description: "Resume a paused cron job.",
2167
+ inputSchema: {
2168
+ type: "object",
2169
+ properties: {
2170
+ cron_id: { type: "string", description: "Cron job ID to resume" },
2171
+ },
2172
+ required: ["cron_id"],
2173
+ },
2174
+ },
2175
+ {
2176
+ name: "cron_run_now",
2177
+ description: "Trigger an immediate one-off execution of a cron job outside its schedule.",
2178
+ inputSchema: {
2179
+ type: "object",
2180
+ properties: {
2181
+ cron_id: {
2182
+ type: "string",
2183
+ description: "Cron job ID to run immediately",
2184
+ },
2185
+ },
2186
+ required: ["cron_id"],
2187
+ },
2188
+ },
2189
+ {
2190
+ name: "cron_executions",
2191
+ description: "List recent execution history for a cron job.",
2192
+ inputSchema: {
2193
+ type: "object",
2194
+ properties: {
2195
+ cron_id: { type: "string", description: "Cron job ID" },
2196
+ },
2197
+ required: ["cron_id"],
2198
+ },
2199
+ },
2200
+ // Volumes
2201
+ {
2202
+ name: "volume_list",
2203
+ description: "List all volumes in the tenant.",
2204
+ inputSchema: { type: "object", properties: {} },
2205
+ },
2206
+ {
2207
+ name: "volume_create",
2208
+ description: "Create a new persistent volume.",
2209
+ inputSchema: {
2210
+ type: "object",
2211
+ properties: {
2212
+ name: {
2213
+ type: "string",
2214
+ description: "Human-readable name for the volume",
2215
+ },
2216
+ size_gb: {
2217
+ type: "integer",
2218
+ description: "Size of the volume in GB (optional)",
2219
+ },
2220
+ region: {
2221
+ type: "string",
2222
+ description: "Region to create the volume in (optional)",
2223
+ },
2224
+ },
2225
+ required: ["name"],
2226
+ },
2227
+ },
2228
+ {
2229
+ name: "volume_get",
2230
+ description: "Get details of a specific volume.",
2231
+ inputSchema: {
2232
+ type: "object",
2233
+ properties: {
2234
+ volume_id: { type: "string", description: "Volume ID" },
2235
+ },
2236
+ required: ["volume_id"],
2237
+ },
2238
+ },
2239
+ {
2240
+ name: "volume_delete",
2241
+ description: "Delete a volume permanently.",
2242
+ inputSchema: {
2243
+ type: "object",
2244
+ properties: {
2245
+ volume_id: { type: "string", description: "Volume ID to delete" },
2246
+ },
2247
+ required: ["volume_id"],
2248
+ },
2249
+ },
2250
+ {
2251
+ name: "volume_attach",
2252
+ description: "Attach a volume to a computer at a given mount path.",
2253
+ inputSchema: {
2254
+ type: "object",
2255
+ properties: {
2256
+ computer_id: {
2257
+ type: "string",
2258
+ description: "Computer ID to attach the volume to",
2259
+ },
2260
+ volume_id: { type: "string", description: "Volume ID to attach" },
2261
+ mount_path: {
2262
+ type: "string",
2263
+ description: "Path inside the VM to mount the volume (optional)",
2264
+ },
2265
+ },
2266
+ required: ["computer_id", "volume_id"],
2267
+ },
2268
+ },
2269
+ {
2270
+ name: "volume_detach",
2271
+ description: "Detach a volume attachment from a computer.",
2272
+ inputSchema: {
2273
+ type: "object",
2274
+ properties: {
2275
+ computer_id: { type: "string", description: "Computer ID" },
2276
+ attachment_id: {
2277
+ type: "string",
2278
+ description: "Attachment ID to remove",
2279
+ },
2280
+ },
2281
+ required: ["computer_id", "attachment_id"],
2282
+ },
2283
+ },
2284
+ ];
2285
+ // ── Wire helpers ─────────────────────────────────────────────────────────────
2286
+ function ok(text) {
2287
+ return { content: [{ type: "text", text }] };
2288
+ }
2289
+ function err(msg) {
2290
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
2291
+ }
2292
+ function image(pngBytes) {
2293
+ return {
2294
+ content: [
2295
+ {
2296
+ type: "image",
2297
+ data: pngBytes.toString("base64"),
2298
+ mimeType: "image/png",
2299
+ },
2300
+ ],
2301
+ };
2302
+ }
2303
+ function send(response) {
2304
+ process.stdout.write(JSON.stringify(response) + "\n");
2305
+ }
2306
+ // ── Tool dispatch ────────────────────────────────────────────────────────────
2307
+ async function dispatchTool(client, name, args) {
2308
+ const cid = typeof args["computer_id"] === "string" ? args["computer_id"] : undefined;
2309
+ try {
2310
+ // ── Lifecycle ──────────────────────────────────────────────────────────
2311
+ if (name === "computer_create") {
2312
+ const body = { name: args["name"] };
2313
+ if (args["template_type"])
2314
+ body["template_type"] = args["template_type"];
2315
+ if (args["size"])
2316
+ body["size"] = args["size"];
2317
+ if (args["workspace_id"])
2318
+ body["workspace_id"] = args["workspace_id"];
2319
+ if (args["external_workspace_id"])
2320
+ body["external_workspace_id"] = args["external_workspace_id"];
2321
+ if (args["external_project_id"])
2322
+ body["external_project_id"] = args["external_project_id"];
2323
+ if (args["gpu_model"]) {
2324
+ body["gpu_model"] = args["gpu_model"];
2325
+ body["gpu_count"] = args["gpu_count"] ?? 1;
2326
+ }
2327
+ const computer = await client.apiPost("/api/v1/computers", body);
2328
+ const data = unwrapData(computer);
2329
+ const id = String(data["id"] ?? "");
2330
+ const compName = String(data["name"] ?? args["name"]);
2331
+ // Poll until the computer reaches "active" state (mirrors Python MCP behaviour)
2332
+ const POLL_INTERVAL_MS = 2_000;
2333
+ const POLL_TIMEOUT_MS = 30_000;
2334
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
2335
+ let finalStatus = String(data["status"] ?? data["state"] ?? "created");
2336
+ while (finalStatus !== "active" && Date.now() < deadline) {
2337
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
2338
+ try {
2339
+ const poll = await client.apiGet(`/api/v1/computers/${encodeURIComponent(id)}`);
2340
+ const pollData = unwrapData(poll);
2341
+ finalStatus = String(pollData["status"] ?? pollData["state"] ?? finalStatus);
2342
+ }
2343
+ catch {
2344
+ // Transient error during poll — keep waiting
2345
+ }
2346
+ }
2347
+ return ok(`Created computer '${compName}' (id=${id}, status=${finalStatus}).`);
2348
+ }
2349
+ if (name === "computer_list") {
2350
+ const result = await client.apiGet("/api/v1/computers");
2351
+ const items = listOf(result);
2352
+ if (items.length === 0)
2353
+ return ok("No computers found.");
2354
+ const lines = ["Available computers:"];
2355
+ for (const c of items) {
2356
+ const r = c;
2357
+ lines.push(` ${r["id"]} ${r["name"]} ${r["status"] ?? r["state"]}`);
2358
+ }
2359
+ return ok(lines.join("\n"));
2360
+ }
2361
+ if (name === "computer_destroy") {
2362
+ if (!cid)
2363
+ return err("computer_id is required");
2364
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}`);
2365
+ return ok(`Computer ${cid} destroyed.`);
2366
+ }
2367
+ if (name === "computer_get") {
2368
+ if (!cid)
2369
+ return err("computer_id is required");
2370
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}`);
2371
+ const data = unwrapData(result);
2372
+ return ok(`id=${data["id"]} name=${JSON.stringify(data["name"])} status=${data["status"] ?? data["state"]}`);
2373
+ }
2374
+ if (name === "computer_start") {
2375
+ if (!cid)
2376
+ return err("computer_id is required");
2377
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/start`, {});
2378
+ return ok(`Computer ${cid} start issued.`);
2379
+ }
2380
+ if (name === "computer_stop") {
2381
+ if (!cid)
2382
+ return err("computer_id is required");
2383
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/stop`, {});
2384
+ return ok(`Computer ${cid} stop issued.`);
2385
+ }
2386
+ if (name === "computer_restart") {
2387
+ if (!cid)
2388
+ return err("computer_id is required");
2389
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/restart`, {});
2390
+ return ok(`Computer ${cid} restart issued.`);
2391
+ }
2392
+ if (name === "computer_update") {
2393
+ if (!cid)
2394
+ return err("computer_id is required");
2395
+ const body = {};
2396
+ if (typeof args["name"] === "string")
2397
+ body["name"] = args["name"];
2398
+ if (args["metadata"] !== undefined)
2399
+ body["metadata"] = args["metadata"];
2400
+ const result = await client.apiPatch(`/api/v1/computers/${encodeURIComponent(cid)}`, body);
2401
+ const data = unwrapData(result);
2402
+ return ok(`Computer ${cid} updated: name=${JSON.stringify(data["name"] ?? args["name"])}.`);
2403
+ }
2404
+ // ── Screenshot ────────────────────────────────────────────────────────
2405
+ if (name === "computer_screenshot") {
2406
+ if (!cid)
2407
+ return err("computer_id is required");
2408
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/screenshot`);
2409
+ // The API returns { data: { image: "<base64>", format: "png" } }
2410
+ const data = unwrapData(result);
2411
+ const b64 = typeof data["image"] === "string" ? data["image"] : null;
2412
+ if (!b64)
2413
+ return err("Screenshot API returned no image data");
2414
+ return image(Buffer.from(b64, "base64"));
2415
+ }
2416
+ // ── Pointer ───────────────────────────────────────────────────────────
2417
+ if (name === "computer_click") {
2418
+ if (!cid)
2419
+ return err("computer_id is required");
2420
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/click`, {
2421
+ x: args["x"],
2422
+ y: args["y"],
2423
+ button: args["button"] ?? "left",
2424
+ });
2425
+ return ok(`Clicked (${args["x"]}, ${args["y"]}) button=${args["button"] ?? "left"}`);
2426
+ }
2427
+ if (name === "computer_double_click") {
2428
+ if (!cid)
2429
+ return err("computer_id is required");
2430
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/double-click`, {
2431
+ x: args["x"],
2432
+ y: args["y"],
2433
+ });
2434
+ return ok(`Double-clicked (${args["x"]}, ${args["y"]})`);
2435
+ }
2436
+ if (name === "computer_move_cursor") {
2437
+ if (!cid)
2438
+ return err("computer_id is required");
2439
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/move`, {
2440
+ x: args["x"],
2441
+ y: args["y"],
2442
+ });
2443
+ return ok(`Moved cursor to (${args["x"]}, ${args["y"]})`);
2444
+ }
2445
+ if (name === "computer_drag") {
2446
+ if (!cid)
2447
+ return err("computer_id is required");
2448
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/drag`, {
2449
+ from_x: args["from_x"],
2450
+ from_y: args["from_y"],
2451
+ to_x: args["to_x"],
2452
+ to_y: args["to_y"],
2453
+ });
2454
+ return ok(`Dragged from (${args["from_x"]}, ${args["from_y"]}) to (${args["to_x"]}, ${args["to_y"]})`);
2455
+ }
2456
+ if (name === "computer_scroll") {
2457
+ if (!cid)
2458
+ return err("computer_id is required");
2459
+ const body = {
2460
+ direction: args["direction"] ?? "down",
2461
+ clicks: args["clicks"] ?? 3,
2462
+ };
2463
+ if (args["x"] !== undefined)
2464
+ body["x"] = args["x"];
2465
+ if (args["y"] !== undefined)
2466
+ body["y"] = args["y"];
2467
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/scroll`, body);
2468
+ return ok(`Scrolled ${body["direction"]} by ${body["clicks"]} clicks`);
2469
+ }
2470
+ // ── Keyboard ──────────────────────────────────────────────────────────
2471
+ if (name === "computer_type") {
2472
+ if (!cid)
2473
+ return err("computer_id is required");
2474
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/type`, {
2475
+ text: args["text"],
2476
+ });
2477
+ const preview = typeof args["text"] === "string"
2478
+ ? args["text"].slice(0, 40) + (args["text"].length > 40 ? "..." : "")
2479
+ : "";
2480
+ return ok(`Typed: ${JSON.stringify(preview)}`);
2481
+ }
2482
+ if (name === "computer_key") {
2483
+ if (!cid)
2484
+ return err("computer_id is required");
2485
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/key`, {
2486
+ key: args["key"],
2487
+ });
2488
+ return ok(`Pressed key: ${args["key"]}`);
2489
+ }
2490
+ if (name === "computer_hotkey") {
2491
+ if (!cid)
2492
+ return err("computer_id is required");
2493
+ const keys = args["keys"];
2494
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/hotkey`, {
2495
+ keys,
2496
+ });
2497
+ return ok(`Pressed hotkey: ${keys.join("+")}`);
2498
+ }
2499
+ // ── Display info ──────────────────────────────────────────────────────
2500
+ if (name === "computer_get_screen_size") {
2501
+ if (!cid)
2502
+ return err("computer_id is required");
2503
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/screen-size`);
2504
+ const data = unwrapData(result);
2505
+ return ok(`Screen size: ${data["width"]}x${data["height"]} px`);
2506
+ }
2507
+ if (name === "computer_get_cursor_position") {
2508
+ if (!cid)
2509
+ return err("computer_id is required");
2510
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/cursor`);
2511
+ const data = unwrapData(result);
2512
+ return ok(`Cursor position: x=${data["x"]}, y=${data["y"]}`);
2513
+ }
2514
+ // ── Clipboard ─────────────────────────────────────────────────────────
2515
+ if (name === "computer_get_clipboard") {
2516
+ if (!cid)
2517
+ return err("computer_id is required");
2518
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/clipboard`);
2519
+ const data = unwrapData(result);
2520
+ return ok(`Clipboard content:\n${data["text"] ?? ""}`);
2521
+ }
2522
+ if (name === "computer_set_clipboard") {
2523
+ if (!cid)
2524
+ return err("computer_id is required");
2525
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/clipboard`, {
2526
+ text: args["text"],
2527
+ });
2528
+ return ok("Clipboard updated.");
2529
+ }
2530
+ // ── Window management ─────────────────────────────────────────────────
2531
+ if (name === "computer_windows") {
2532
+ if (!cid)
2533
+ return err("computer_id is required");
2534
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/windows`);
2535
+ const windows = listOf(result);
2536
+ if (windows.length === 0)
2537
+ return ok("No open windows found.");
2538
+ const lines = ["Open windows:"];
2539
+ for (const w of windows) {
2540
+ const r = w;
2541
+ const focused = r["focused"] ? " [focused]" : "";
2542
+ lines.push(` id=${r["id"]} title=${JSON.stringify(r["title"])} app=${JSON.stringify(r["app"])}` +
2543
+ ` pos=(${r["x"]},${r["y"]}) size=${r["width"]}x${r["height"]}${focused}`);
2544
+ }
2545
+ return ok(lines.join("\n"));
2546
+ }
2547
+ if (name === "computer_launch") {
2548
+ if (!cid)
2549
+ return err("computer_id is required");
2550
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/launch`, {
2551
+ app: args["app"],
2552
+ });
2553
+ return ok(`Launched: ${args["app"]}`);
2554
+ }
2555
+ // ── Shell & Files ─────────────────────────────────────────────────────
2556
+ if (name === "computer_bash") {
2557
+ if (!cid)
2558
+ return err("computer_id is required");
2559
+ const body = { command: args["command"] };
2560
+ if (args["timeout"] !== undefined)
2561
+ body["timeout"] = args["timeout"];
2562
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/exec`, body);
2563
+ const data = unwrapData(result);
2564
+ const parts = [];
2565
+ if (data["output"] ?? data["stdout"]) {
2566
+ parts.push(`stdout:\n${data["output"] ?? data["stdout"]}`);
2567
+ }
2568
+ if (data["stderr"])
2569
+ parts.push(`stderr:\n${data["stderr"]}`);
2570
+ parts.push(`exit_code: ${data["exit_code"] ?? 0}`);
2571
+ return ok(parts.join("\n"));
2572
+ }
2573
+ if (name === "computer_write_file") {
2574
+ if (!cid)
2575
+ return err("computer_id is required");
2576
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/write`, {
2577
+ path: args["path"],
2578
+ content: args["content"],
2579
+ encoding: "utf8",
2580
+ });
2581
+ const len = typeof args["content"] === "string" ? args["content"].length : 0;
2582
+ return ok(`Wrote ${len} bytes to ${args["path"]}`);
2583
+ }
2584
+ if (name === "computer_read_file") {
2585
+ if (!cid)
2586
+ return err("computer_id is required");
2587
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/files/download?path=${encodeURIComponent(String(args["path"] ?? ""))}`);
2588
+ const data = unwrapData(result);
2589
+ const content = typeof data["content"] === "string"
2590
+ ? Buffer.from(data["content"], "base64").toString("utf8")
2591
+ : typeof data["text"] === "string"
2592
+ ? data["text"]
2593
+ : JSON.stringify(data);
2594
+ return ok(content);
2595
+ }
2596
+ // ── Extended pointer ──────────────────────────────────────────────────
2597
+ if (name === "computer_right_click") {
2598
+ if (!cid)
2599
+ return err("computer_id is required");
2600
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/click`, { x: args["x"], y: args["y"], button: "right" });
2601
+ return ok(`Right-clicked (${args["x"]}, ${args["y"]})`);
2602
+ }
2603
+ if (name === "computer_mouse_down") {
2604
+ if (!cid)
2605
+ return err("computer_id is required");
2606
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/mouse-down`, {
2607
+ x: args["x"],
2608
+ y: args["y"],
2609
+ button: args["button"] ?? "left",
2610
+ });
2611
+ return ok(`Mouse down at (${args["x"]}, ${args["y"]}) button=${args["button"] ?? "left"}`);
2612
+ }
2613
+ if (name === "computer_mouse_up") {
2614
+ if (!cid)
2615
+ return err("computer_id is required");
2616
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/mouse-up`, {
2617
+ x: args["x"],
2618
+ y: args["y"],
499
2619
  button: args["button"] ?? "left",
500
2620
  });
501
- return ok(`Clicked (${args["x"]}, ${args["y"]}) button=${args["button"] ?? "left"}`);
2621
+ return ok(`Mouse up at (${args["x"]}, ${args["y"]}) button=${args["button"] ?? "left"}`);
2622
+ }
2623
+ // ── Extended keyboard ─────────────────────────────────────────────────
2624
+ if (name === "computer_key_down") {
2625
+ if (!cid)
2626
+ return err("computer_id is required");
2627
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/key-down`, { key: args["key"] });
2628
+ return ok(`Key down: ${args["key"]}`);
2629
+ }
2630
+ if (name === "computer_key_up") {
2631
+ if (!cid)
2632
+ return err("computer_id is required");
2633
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/key-up`, { key: args["key"] });
2634
+ return ok(`Key up: ${args["key"]}`);
2635
+ }
2636
+ // ── Wait ──────────────────────────────────────────────────────────────
2637
+ if (name === "computer_wait") {
2638
+ if (!cid)
2639
+ return err("computer_id is required");
2640
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/wait`, { seconds: args["seconds"] });
2641
+ return ok(`Waited ${args["seconds"]}s`);
2642
+ }
2643
+ // ── Window management (extended) ──────────────────────────────────────
2644
+ if (name === "computer_focus_window") {
2645
+ if (!cid)
2646
+ return err("computer_id is required");
2647
+ const wid = String(args["window_id"] ?? "");
2648
+ if (!wid)
2649
+ return err("window_id is required");
2650
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/window/focus`, { window_id: wid });
2651
+ return ok(`Focused window ${wid}`);
2652
+ }
2653
+ if (name === "computer_set_window_size") {
2654
+ if (!cid)
2655
+ return err("computer_id is required");
2656
+ const wid = String(args["window_id"] ?? "");
2657
+ if (!wid)
2658
+ return err("window_id is required");
2659
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/window/${encodeURIComponent(wid)}/resize`, { width: args["width"], height: args["height"] });
2660
+ return ok(`Resized window ${wid} to ${args["width"]}x${args["height"]}`);
2661
+ }
2662
+ if (name === "computer_set_window_position") {
2663
+ if (!cid)
2664
+ return err("computer_id is required");
2665
+ const wid = String(args["window_id"] ?? "");
2666
+ if (!wid)
2667
+ return err("window_id is required");
2668
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/window/${encodeURIComponent(wid)}/move`, { x: args["x"], y: args["y"] });
2669
+ return ok(`Moved window ${wid} to (${args["x"]}, ${args["y"]})`);
2670
+ }
2671
+ if (name === "computer_maximize_window") {
2672
+ if (!cid)
2673
+ return err("computer_id is required");
2674
+ const wid = String(args["window_id"] ?? "");
2675
+ if (!wid)
2676
+ return err("window_id is required");
2677
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/window/${encodeURIComponent(wid)}/maximize`, {});
2678
+ return ok(`Maximized window ${wid}`);
2679
+ }
2680
+ if (name === "computer_minimize_window") {
2681
+ if (!cid)
2682
+ return err("computer_id is required");
2683
+ const wid = String(args["window_id"] ?? "");
2684
+ if (!wid)
2685
+ return err("window_id is required");
2686
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/window/${encodeURIComponent(wid)}/minimize`, {});
2687
+ return ok(`Minimized window ${wid}`);
2688
+ }
2689
+ if (name === "computer_close_window") {
2690
+ if (!cid)
2691
+ return err("computer_id is required");
2692
+ const wid = String(args["window_id"] ?? "");
2693
+ if (!wid)
2694
+ return err("window_id is required");
2695
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/window/${encodeURIComponent(wid)}/close`, {});
2696
+ return ok(`Closed window ${wid}`);
2697
+ }
2698
+ // ── Desktop environment ───────────────────────────────────────────────
2699
+ if (name === "computer_get_desktop_env") {
2700
+ if (!cid)
2701
+ return err("computer_id is required");
2702
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/environment`);
2703
+ const data = unwrapData(result);
2704
+ return ok(`desktop_env: name=${JSON.stringify(data["name"])} resolution=${data["resolution"]} session_type=${JSON.stringify(data["session_type"])}`);
2705
+ }
2706
+ if (name === "computer_set_wallpaper") {
2707
+ if (!cid)
2708
+ return err("computer_id is required");
2709
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/wallpaper`, { path: args["path"] });
2710
+ return ok(`Wallpaper set to: ${args["path"]}`);
2711
+ }
2712
+ if (name === "computer_accessibility_tree") {
2713
+ if (!cid)
2714
+ return err("computer_id is required");
2715
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/accessibility-tree`);
2716
+ const tree = unwrapData(result);
2717
+ const json = JSON.stringify(tree, null, 2);
2718
+ return ok(json.length > 8000 ? json.slice(0, 8000) + "\n…(truncated)" : json);
2719
+ }
2720
+ // ── Files — extended ──────────────────────────────────────────────────
2721
+ if (name === "computer_list_files") {
2722
+ if (!cid)
2723
+ return err("computer_id is required");
2724
+ const path = typeof args["path"] === "string" ? args["path"] : undefined;
2725
+ const qs = path ? `?path=${encodeURIComponent(path)}` : "";
2726
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/files${qs}`);
2727
+ const items = listOf(result);
2728
+ if (items.length === 0)
2729
+ return ok(`No files found at ${path ?? "/"}.`);
2730
+ const lines = [`Files at ${path ?? "/"}:`];
2731
+ for (const e of items) {
2732
+ const r = e;
2733
+ lines.push(` ${r["name"]} ${r["type"] ?? r["kind"] ?? ""} ${r["size"] ?? ""}`);
2734
+ }
2735
+ return ok(lines.join("\n"));
2736
+ }
2737
+ if (name === "computer_stat_file") {
2738
+ if (!cid)
2739
+ return err("computer_id is required");
2740
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/stat`, { path: args["path"] });
2741
+ const data = unwrapData(result);
2742
+ const lines = [
2743
+ `path: ${args["path"]}`,
2744
+ `type: ${data["type"] ?? data["kind"] ?? ""}`,
2745
+ `size: ${data["size"] ?? ""}`,
2746
+ `mode: ${data["mode"] ?? ""}`,
2747
+ `mtime: ${data["mtime"] ?? data["modified_at"] ?? ""}`,
2748
+ ];
2749
+ return ok(lines.join("\n"));
2750
+ }
2751
+ if (name === "computer_mkdir") {
2752
+ if (!cid)
2753
+ return err("computer_id is required");
2754
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/mkdir`, {
2755
+ path: args["path"],
2756
+ recursive: args["recursive"] !== false,
2757
+ mode: "0755",
2758
+ });
2759
+ return ok(`Created directory ${args["path"]}`);
2760
+ }
2761
+ if (name === "computer_rename_file") {
2762
+ if (!cid)
2763
+ return err("computer_id is required");
2764
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/rename`, { from: args["source"], to: args["dest"] });
2765
+ return ok(`Renamed ${args["source"]} -> ${args["dest"]}`);
2766
+ }
2767
+ if (name === "computer_copy_file") {
2768
+ if (!cid)
2769
+ return err("computer_id is required");
2770
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/copy`, {
2771
+ from: args["source"],
2772
+ to: args["dest"],
2773
+ recursive: args["recursive"] === true,
2774
+ });
2775
+ return ok(`Copied ${args["source"]} -> ${args["dest"]}`);
2776
+ }
2777
+ if (name === "computer_delete_file") {
2778
+ if (!cid)
2779
+ return err("computer_id is required");
2780
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}/files?path=${encodeURIComponent(String(args["path"] ?? ""))}`);
2781
+ return ok(`Deleted ${args["path"]}`);
2782
+ }
2783
+ if (name === "computer_upload_file") {
2784
+ if (!cid)
2785
+ return err("computer_id is required");
2786
+ // Read the local file and POST it as base64 via the write endpoint
2787
+ const fs = await import("node:fs/promises");
2788
+ const localBytes = await fs.readFile(String(args["local_path"]));
2789
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/write`, {
2790
+ path: args["remote_path"],
2791
+ content: localBytes.toString("base64"),
2792
+ encoding: "base64",
2793
+ });
2794
+ return ok(`Uploaded ${args["local_path"]} -> ${args["remote_path"]}`);
2795
+ }
2796
+ // ── Checkpoints ───────────────────────────────────────────────────────
2797
+ if (name === "computer_checkpoint_create") {
2798
+ if (!cid)
2799
+ return err("computer_id is required");
2800
+ const body = {};
2801
+ if (args["comment"])
2802
+ body["comment"] = args["comment"];
2803
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/snapshots`, body);
2804
+ const data = unwrapData(result);
2805
+ const snap_id = String(data["id"] ?? "");
2806
+ const snap_status = String(data["status"] ?? "");
2807
+ const snap_comment = data["comment"]
2808
+ ? ` comment=${JSON.stringify(data["comment"])}`
2809
+ : "";
2810
+ return ok(`Checkpoint created: id=${snap_id} status=${snap_status}${snap_comment}`);
2811
+ }
2812
+ if (name === "computer_checkpoint_list") {
2813
+ if (!cid)
2814
+ return err("computer_id is required");
2815
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/snapshots`);
2816
+ const items = listOf(result);
2817
+ if (items.length === 0)
2818
+ return ok("No checkpoints found.");
2819
+ const lines = ["Checkpoints:"];
2820
+ for (const s of items) {
2821
+ const r = s;
2822
+ const c = r["comment"] ? ` ${JSON.stringify(r["comment"])}` : "";
2823
+ lines.push(` ${r["id"]} ${r["status"]} ${r["created_at"] ?? ""}${c}`);
2824
+ }
2825
+ return ok(lines.join("\n"));
2826
+ }
2827
+ if (name === "computer_checkpoint_restore") {
2828
+ if (!cid)
2829
+ return err("computer_id is required");
2830
+ const snapId = String(args["checkpoint_id"] ?? "");
2831
+ if (!snapId)
2832
+ return err("checkpoint_id is required");
2833
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/restore/${encodeURIComponent(snapId)}`, {});
2834
+ const raw = unwrapData(result);
2835
+ const compData = raw["computer"] ?? raw;
2836
+ const newId = String(compData["id"] ?? "");
2837
+ const newStatus = String(compData["status"] ?? compData["state"] ?? "");
2838
+ return ok(`Restored checkpoint ${snapId} -> new computer id=${newId} status=${newStatus}.`);
2839
+ }
2840
+ if (name === "computer_checkpoint_delete") {
2841
+ if (!cid)
2842
+ return err("computer_id is required");
2843
+ const snapId = String(args["checkpoint_id"] ?? "");
2844
+ if (!snapId)
2845
+ return err("checkpoint_id is required");
2846
+ const result = await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}/snapshots/${encodeURIComponent(snapId)}`);
2847
+ const data = unwrapData(result);
2848
+ return ok(`Checkpoint ${data["id"] ?? snapId} deleted (status=${data["status"] ?? "deleted"}).`);
2849
+ }
2850
+ // ── Services ──────────────────────────────────────────────────────────
2851
+ if (name === "computer_service_create") {
2852
+ if (!cid)
2853
+ return err("computer_id is required");
2854
+ const body = {
2855
+ name: args["name"],
2856
+ command: args["command"],
2857
+ };
2858
+ if (args["working_dir"])
2859
+ body["working_dir"] = args["working_dir"];
2860
+ if (args["port"] !== undefined)
2861
+ body["port"] = args["port"];
2862
+ if (args["restart_policy"])
2863
+ body["restart_policy"] = args["restart_policy"];
2864
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/services`, body);
2865
+ const data = unwrapData(result);
2866
+ return ok(`Service created: id=${data["id"]} name=${JSON.stringify(data["name"])} status=${data["status"] ?? data["state"] ?? ""}`);
2867
+ }
2868
+ if (name === "computer_service_list") {
2869
+ if (!cid)
2870
+ return err("computer_id is required");
2871
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/services`);
2872
+ const items = listOf(result);
2873
+ if (items.length === 0)
2874
+ return ok("No services found.");
2875
+ const lines = ["Services:"];
2876
+ for (const s of items) {
2877
+ const r = s;
2878
+ lines.push(` ${r["id"]} ${JSON.stringify(r["name"])} ${r["status"] ?? r["state"] ?? ""}`);
2879
+ }
2880
+ return ok(lines.join("\n"));
2881
+ }
2882
+ if (name === "computer_service_start") {
2883
+ if (!cid)
2884
+ return err("computer_id is required");
2885
+ const sid = String(args["service_id"] ?? "");
2886
+ if (!sid)
2887
+ return err("service_id is required");
2888
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/services/${encodeURIComponent(sid)}/start`, {});
2889
+ const data = unwrapData(result);
2890
+ return ok(`Service ${data["id"] ?? sid} started (status=${data["status"] ?? ""}).`);
2891
+ }
2892
+ if (name === "computer_service_stop") {
2893
+ if (!cid)
2894
+ return err("computer_id is required");
2895
+ const sid = String(args["service_id"] ?? "");
2896
+ if (!sid)
2897
+ return err("service_id is required");
2898
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/services/${encodeURIComponent(sid)}/stop`, {});
2899
+ const data = unwrapData(result);
2900
+ return ok(`Service ${data["id"] ?? sid} stopped (status=${data["status"] ?? ""}).`);
2901
+ }
2902
+ if (name === "computer_service_restart") {
2903
+ if (!cid)
2904
+ return err("computer_id is required");
2905
+ const sid = String(args["service_id"] ?? "");
2906
+ if (!sid)
2907
+ return err("service_id is required");
2908
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/services/${encodeURIComponent(sid)}/restart`, {});
2909
+ const data = unwrapData(result);
2910
+ return ok(`Service ${data["id"] ?? sid} restarted (status=${data["status"] ?? ""}).`);
2911
+ }
2912
+ if (name === "computer_service_logs") {
2913
+ if (!cid)
2914
+ return err("computer_id is required");
2915
+ const sid = String(args["service_id"] ?? "");
2916
+ if (!sid)
2917
+ return err("service_id is required");
2918
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/services/${encodeURIComponent(sid)}/logs?follow=false`);
2919
+ const data = unwrapData(result);
2920
+ const logLines = data["lines"] ?? data["logs"] ?? data["data"];
2921
+ if (Array.isArray(logLines) && logLines.length > 0) {
2922
+ return ok(logLines.map((l) => String(l)).join("\n"));
2923
+ }
2924
+ if (typeof logLines === "string")
2925
+ return ok(logLines);
2926
+ return ok("No log output.");
2927
+ }
2928
+ if (name === "computer_service_delete") {
2929
+ if (!cid)
2930
+ return err("computer_id is required");
2931
+ const sid = String(args["service_id"] ?? "");
2932
+ if (!sid)
2933
+ return err("service_id is required");
2934
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}/services/${encodeURIComponent(sid)}`);
2935
+ return ok(`Service ${sid} deleted.`);
2936
+ }
2937
+ // ── Env vars ──────────────────────────────────────────────────────────
2938
+ if (name === "computer_env_list") {
2939
+ if (!cid)
2940
+ return err("computer_id is required");
2941
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/env`);
2942
+ const items = Array.isArray(result)
2943
+ ? result
2944
+ : (() => {
2945
+ const d = unwrapData(result);
2946
+ return ((Array.isArray(d["env"]) ? d["env"] : null) ??
2947
+ (Array.isArray(d["items"]) ? d["items"] : null) ??
2948
+ (Array.isArray(d["data"]) ? d["data"] : null) ??
2949
+ []);
2950
+ })();
2951
+ if (items.length === 0)
2952
+ return ok("No environment variables set.");
2953
+ const lines = ["Environment variables:"];
2954
+ for (const e of items) {
2955
+ const r = e;
2956
+ const k = String(r["name"] ?? r["key"] ?? "");
2957
+ const v = String(r["value"] ?? "");
2958
+ lines.push(` ${k}=${v}`);
2959
+ }
2960
+ return ok(lines.join("\n"));
2961
+ }
2962
+ if (name === "computer_env_set") {
2963
+ if (!cid)
2964
+ return err("computer_id is required");
2965
+ await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/env`, {
2966
+ name: args["name"],
2967
+ value: args["value"],
2968
+ });
2969
+ return ok(`Set env var ${args["name"]}.`);
2970
+ }
2971
+ if (name === "computer_env_delete") {
2972
+ if (!cid)
2973
+ return err("computer_id is required");
2974
+ const varName = String(args["name"] ?? "");
2975
+ if (!varName)
2976
+ return err("name is required");
2977
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}/env/${encodeURIComponent(varName)}`);
2978
+ return ok(`Deleted env var ${varName}.`);
2979
+ }
2980
+ // ── Logs ──────────────────────────────────────────────────────────────
2981
+ if (name === "computer_logs") {
2982
+ if (!cid)
2983
+ return err("computer_id is required");
2984
+ const qs = args["lines"] !== undefined
2985
+ ? `?lines=${encodeURIComponent(String(args["lines"]))}`
2986
+ : "";
2987
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/logs${qs}`);
2988
+ const data = unwrapData(result);
2989
+ const logLines = data["lines"] ?? data["logs"] ?? data["data"];
2990
+ if (Array.isArray(logLines) && logLines.length > 0) {
2991
+ return ok(logLines.map((l) => String(l)).join("\n"));
2992
+ }
2993
+ if (typeof logLines === "string")
2994
+ return ok(logLines);
2995
+ if (typeof result === "string")
2996
+ return ok(result);
2997
+ return ok("No logs.");
2998
+ }
2999
+ // ── Domains ───────────────────────────────────────────────────────────
3000
+ if (name === "computer_domain_add") {
3001
+ if (!cid)
3002
+ return err("computer_id is required");
3003
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/domains`, { fqdn: args["fqdn"] });
3004
+ const data = unwrapData(result);
3005
+ const lines = [
3006
+ `Domain registered: id=${data["id"]} fqdn=${data["fqdn"]} status=${data["status"] ?? ""}`,
3007
+ ];
3008
+ if (data["verification_target"])
3009
+ lines.push(` CNAME target: ${data["verification_target"]}`);
3010
+ if (data["instructions"])
3011
+ lines.push(` Instructions: ${data["instructions"]}`);
3012
+ return ok(lines.join("\n"));
3013
+ }
3014
+ if (name === "computer_domain_list") {
3015
+ if (!cid)
3016
+ return err("computer_id is required");
3017
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/domains`);
3018
+ const items = listOf(result);
3019
+ if (items.length === 0)
3020
+ return ok("No custom domains registered.");
3021
+ const lines = ["Custom domains:"];
3022
+ for (const d of items) {
3023
+ const r = d;
3024
+ lines.push(` ${r["id"]} ${r["fqdn"]} ${r["status"] ?? ""}`);
3025
+ }
3026
+ return ok(lines.join("\n"));
3027
+ }
3028
+ if (name === "computer_domain_delete") {
3029
+ if (!cid)
3030
+ return err("computer_id is required");
3031
+ const domainId = String(args["domain_id"] ?? "");
3032
+ if (!domainId)
3033
+ return err("domain_id is required");
3034
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}/domains/${encodeURIComponent(domainId)}`);
3035
+ return ok(`Domain ${domainId} deleted.`);
3036
+ }
3037
+ // ── Sandboxes ─────────────────────────────────────────────────────────
3038
+ if (name === "sandbox_create") {
3039
+ const body = {};
3040
+ if (args["name"])
3041
+ body["name"] = args["name"];
3042
+ if (args["template_id"])
3043
+ body["template_id"] = args["template_id"];
3044
+ if (args["cpu_count"] !== undefined)
3045
+ body["cpu_count"] = args["cpu_count"];
3046
+ if (args["memory_mb"] !== undefined)
3047
+ body["memory_mb"] = args["memory_mb"];
3048
+ if (args["timeout_sec"] !== undefined)
3049
+ body["timeout_sec"] = args["timeout_sec"];
3050
+ const result = await client.apiPost("/api/v1/sandboxes", body);
3051
+ const data = unwrapData(result);
3052
+ const sid = String(data["id"] ?? "");
3053
+ return ok(`Created sandbox '${data["name"] ?? sid}' (id=${sid}).`);
3054
+ }
3055
+ if (name === "sandbox_exec") {
3056
+ const sid = String(args["sandbox_id"] ?? "");
3057
+ if (!sid)
3058
+ return err("sandbox_id is required");
3059
+ const body = { command: args["command"] };
3060
+ if (args["cwd"])
3061
+ body["cwd"] = args["cwd"];
3062
+ if (args["timeout"] !== undefined)
3063
+ body["timeout"] = args["timeout"];
3064
+ const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/exec`, body);
3065
+ const data = unwrapData(result);
3066
+ const parts = [];
3067
+ if (data["output"] ?? data["stdout"]) {
3068
+ parts.push(`stdout:\n${data["output"] ?? data["stdout"]}`);
3069
+ }
3070
+ if (data["stderr"])
3071
+ parts.push(`stderr:\n${data["stderr"]}`);
3072
+ parts.push(`exit_code: ${data["exit_code"] ?? 0}`);
3073
+ return ok(parts.join("\n"));
3074
+ }
3075
+ if (name === "sandbox_destroy") {
3076
+ const sid = String(args["sandbox_id"] ?? "");
3077
+ if (!sid)
3078
+ return err("sandbox_id is required");
3079
+ await client.apiDelete(`/api/v1/sandboxes/${encodeURIComponent(sid)}`);
3080
+ return ok(`Sandbox ${sid} destroyed.`);
3081
+ }
3082
+ // ── Sandbox list/get/pause/resume ─────────────────────────────────────
3083
+ if (name === "sandbox_list") {
3084
+ const params = new URLSearchParams();
3085
+ if (args["state"])
3086
+ params.set("state", String(args["state"]));
3087
+ const qs = params.toString() ? `?${params.toString()}` : "";
3088
+ const result = await client.apiGet(`/api/v1/sandboxes${qs}`);
3089
+ const items = listOf(result);
3090
+ if (items.length === 0)
3091
+ return ok("No sandboxes found.");
3092
+ const lines = ["Sandboxes:"];
3093
+ for (const s of items) {
3094
+ const r = s;
3095
+ lines.push(` ${r["id"]} ${r["name"] ?? ""} ${r["state"] ?? r["status"] ?? ""} template=${r["template_id"] ?? ""}`);
3096
+ }
3097
+ return ok(lines.join("\n"));
3098
+ }
3099
+ if (name === "sandbox_get") {
3100
+ const sid = String(args["sandbox_id"] ?? "");
3101
+ if (!sid)
3102
+ return err("sandbox_id is required");
3103
+ const result = await client.apiGet(`/api/v1/sandboxes/${encodeURIComponent(sid)}`);
3104
+ const data = unwrapData(result);
3105
+ const lines = [
3106
+ `id: ${data["id"]}`,
3107
+ `state: ${data["state"] ?? data["status"] ?? ""}`,
3108
+ `template_id: ${data["template_id"] ?? ""}`,
3109
+ `ready: ${data["ready"] ?? ""}`,
3110
+ ];
3111
+ if (data["name"])
3112
+ lines.splice(1, 0, `name: ${data["name"]}`);
3113
+ if (data["preview_url"])
3114
+ lines.push(`preview_url: ${data["preview_url"]}`);
3115
+ if (data["boot_ms"] !== undefined)
3116
+ lines.push(`boot_ms: ${data["boot_ms"]}`);
3117
+ return ok(lines.join("\n"));
3118
+ }
3119
+ if (name === "sandbox_pause") {
3120
+ const sid = String(args["sandbox_id"] ?? "");
3121
+ if (!sid)
3122
+ return err("sandbox_id is required");
3123
+ await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/pause`, {});
3124
+ return ok(`Sandbox ${sid} paused.`);
3125
+ }
3126
+ if (name === "sandbox_resume") {
3127
+ const sid = String(args["sandbox_id"] ?? "");
3128
+ if (!sid)
3129
+ return err("sandbox_id is required");
3130
+ await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/resume`, {});
3131
+ return ok(`Sandbox ${sid} resumed.`);
3132
+ }
3133
+ // ── Sandbox files ─────────────────────────────────────────────────────
3134
+ if (name === "sandbox_write_file") {
3135
+ const sid = String(args["sandbox_id"] ?? "");
3136
+ if (!sid)
3137
+ return err("sandbox_id is required");
3138
+ await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/files`, {
3139
+ path: args["path"],
3140
+ content: Buffer.from(typeof args["content"] === "string" ? args["content"] : "", "utf8").toString("base64"),
3141
+ });
3142
+ const len = typeof args["content"] === "string" ? args["content"].length : 0;
3143
+ return ok(`Wrote ${len} bytes to ${args["path"]}`);
502
3144
  }
503
- if (name === "computer_double_click") {
504
- if (!cid)
505
- return err("computer_id is required");
506
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/double-click`, {
507
- x: args["x"],
508
- y: args["y"],
3145
+ if (name === "sandbox_read_file") {
3146
+ const sid = String(args["sandbox_id"] ?? "");
3147
+ if (!sid)
3148
+ return err("sandbox_id is required");
3149
+ const path = String(args["path"] ?? "").replace(/^\//, "");
3150
+ const result = await client.apiGet(`/api/v1/sandboxes/${encodeURIComponent(sid)}/files/${encodeURIComponent(path)}`);
3151
+ const data = unwrapData(result);
3152
+ if (typeof data["content"] === "string") {
3153
+ try {
3154
+ return ok(Buffer.from(data["content"], "base64").toString("utf8"));
3155
+ }
3156
+ catch {
3157
+ return ok(data["content"]);
3158
+ }
3159
+ }
3160
+ if (typeof data["text"] === "string")
3161
+ return ok(data["text"]);
3162
+ return ok(JSON.stringify(data));
3163
+ }
3164
+ if (name === "sandbox_list_files") {
3165
+ const sid = String(args["sandbox_id"] ?? "");
3166
+ if (!sid)
3167
+ return err("sandbox_id is required");
3168
+ const path = String(args["path"] ?? "/workspace");
3169
+ const params = new URLSearchParams({ path });
3170
+ if (args["depth"] !== undefined)
3171
+ params.set("depth", String(args["depth"]));
3172
+ const result = await client.apiGet(`/api/v1/sandboxes/${encodeURIComponent(sid)}/files?${params.toString()}`);
3173
+ const data = unwrapData(result);
3174
+ return ok(JSON.stringify(data, null, 2));
3175
+ }
3176
+ if (name === "sandbox_upload") {
3177
+ const sid = String(args["sandbox_id"] ?? "");
3178
+ if (!sid)
3179
+ return err("sandbox_id is required");
3180
+ await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/files`, {
3181
+ path: args["path"],
3182
+ content: Buffer.from(typeof args["content"] === "string" ? args["content"] : "", "utf8").toString("base64"),
509
3183
  });
510
- return ok(`Double-clicked (${args["x"]}, ${args["y"]})`);
3184
+ return ok(`Uploaded ${args["path"]} to sandbox ${sid}.`);
511
3185
  }
512
- if (name === "computer_move_cursor") {
513
- if (!cid)
514
- return err("computer_id is required");
515
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/move`, {
516
- x: args["x"],
517
- y: args["y"],
3186
+ // ── Sandbox python ────────────────────────────────────────────────────
3187
+ if (name === "sandbox_python") {
3188
+ const sid = String(args["sandbox_id"] ?? "");
3189
+ if (!sid)
3190
+ return err("sandbox_id is required");
3191
+ const code = String(args["code"] ?? "");
3192
+ const tmpPath = "/tmp/_mcp_run.py";
3193
+ // Write code file
3194
+ await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/files`, {
3195
+ path: tmpPath,
3196
+ content: Buffer.from(code, "utf8").toString("base64"),
518
3197
  });
519
- return ok(`Moved cursor to (${args["x"]}, ${args["y"]})`);
3198
+ // Execute it
3199
+ const execBody = {
3200
+ command: `python3 ${tmpPath}`,
3201
+ };
3202
+ if (args["timeout"] !== undefined)
3203
+ execBody["timeout"] = args["timeout"];
3204
+ const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/exec`, execBody);
3205
+ const data = unwrapData(result);
3206
+ const parts = [];
3207
+ if (data["stdout"] ?? data["output"])
3208
+ parts.push(`stdout:\n${data["stdout"] ?? data["output"]}`);
3209
+ if (data["stderr"])
3210
+ parts.push(`stderr:\n${data["stderr"]}`);
3211
+ parts.push(`exit_code: ${data["exit_code"] ?? 0}`);
3212
+ return ok(parts.join("\n"));
520
3213
  }
521
- if (name === "computer_drag") {
522
- if (!cid)
523
- return err("computer_id is required");
524
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/drag`, {
525
- from_x: args["from_x"],
526
- from_y: args["from_y"],
527
- to_x: args["to_x"],
528
- to_y: args["to_y"],
3214
+ // ── Sandbox snapshots ─────────────────────────────────────────────────
3215
+ if (name === "sandbox_snapshot_create") {
3216
+ const sid = String(args["sandbox_id"] ?? "");
3217
+ if (!sid)
3218
+ return err("sandbox_id is required");
3219
+ const body = {};
3220
+ if (args["comment"])
3221
+ body["comment"] = args["comment"];
3222
+ const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/snapshots`, body);
3223
+ const data = unwrapData(result);
3224
+ const snapId = data["id"] ?? data["snapshot_id"] ?? "unknown";
3225
+ return ok(`Snapshot created: id=${snapId}`);
3226
+ }
3227
+ if (name === "sandbox_snapshot_list") {
3228
+ const sid = String(args["sandbox_id"] ?? "");
3229
+ if (!sid)
3230
+ return err("sandbox_id is required");
3231
+ const result = await client.apiGet(`/api/v1/sandboxes/${encodeURIComponent(sid)}/snapshots`);
3232
+ const items = listOf(result);
3233
+ if (items.length === 0)
3234
+ return ok("No snapshots found.");
3235
+ const lines = ["Snapshots:"];
3236
+ for (const s of items) {
3237
+ const r = s;
3238
+ lines.push(` ${r["id"]} ${r["created_at"] ?? ""} ${r["comment"] ?? ""}`);
3239
+ }
3240
+ return ok(lines.join("\n"));
3241
+ }
3242
+ if (name === "sandbox_snapshot_restore") {
3243
+ const sid = String(args["sandbox_id"] ?? "");
3244
+ if (!sid)
3245
+ return err("sandbox_id is required");
3246
+ const snapId = String(args["snapshot_id"] ?? "");
3247
+ if (!snapId)
3248
+ return err("snapshot_id is required");
3249
+ const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/restore/${encodeURIComponent(snapId)}`, {});
3250
+ const data = unwrapData(result);
3251
+ const state = data["state"] ?? data["status"] ?? "unknown";
3252
+ return ok(`Sandbox ${sid} restored from snapshot ${snapId} (state=${state}).`);
3253
+ }
3254
+ // ── Sandbox logs ──────────────────────────────────────────────────────
3255
+ if (name === "sandbox_logs") {
3256
+ const sid = String(args["sandbox_id"] ?? "");
3257
+ if (!sid)
3258
+ return err("sandbox_id is required");
3259
+ const params = new URLSearchParams();
3260
+ if (args["lines"] !== undefined)
3261
+ params.set("lines", String(args["lines"]));
3262
+ const qs = params.toString() ? `?${params.toString()}` : "";
3263
+ const result = await client.apiGet(`/api/v1/sandboxes/${encodeURIComponent(sid)}/logs${qs}`);
3264
+ const data = unwrapData(result);
3265
+ if (Array.isArray(data)) {
3266
+ return ok(data.length > 0 ? data.join("\n") : "No logs.");
3267
+ }
3268
+ return ok(JSON.stringify(data, null, 2));
3269
+ }
3270
+ // ── Sandbox expose ────────────────────────────────────────────────────
3271
+ if (name === "sandbox_expose") {
3272
+ const sid = String(args["sandbox_id"] ?? "");
3273
+ if (!sid)
3274
+ return err("sandbox_id is required");
3275
+ const body = {};
3276
+ if (args["port"] !== undefined)
3277
+ body["port"] = args["port"];
3278
+ const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/expose`, body);
3279
+ const data = unwrapData(result);
3280
+ const url = data["url"] ?? data["preview_url"] ?? "";
3281
+ return ok(`Preview URL: ${url}`);
3282
+ }
3283
+ // ── Sandbox deploy ────────────────────────────────────────────────────
3284
+ if (name === "sandbox_deploy") {
3285
+ const sid = String(args["sandbox_id"] ?? "");
3286
+ if (!sid)
3287
+ return err("sandbox_id is required");
3288
+ const body = {};
3289
+ if (args["name"])
3290
+ body["name"] = args["name"];
3291
+ if (args["output_path"])
3292
+ body["output_path"] = args["output_path"];
3293
+ if (args["entrypoint"])
3294
+ body["entrypoint"] = args["entrypoint"];
3295
+ if (args["domain"])
3296
+ body["domain"] = args["domain"];
3297
+ const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/deploy`, body);
3298
+ const data = unwrapData(result);
3299
+ const deployId = data["id"] ?? data["deployment_id"] ?? "unknown";
3300
+ const url = data["url"] ?? data["preview_url"] ?? "";
3301
+ return ok(`Deployed sandbox ${sid} (deployment id=${deployId}, url=${url}).`);
3302
+ }
3303
+ // ── Sandbox templates ─────────────────────────────────────────────────
3304
+ if (name === "sandbox_template_list") {
3305
+ const result = await client.apiGet("/api/v1/sandbox-templates");
3306
+ const data = unwrapData(result);
3307
+ const items = Array.isArray(data)
3308
+ ? data
3309
+ : (() => {
3310
+ const r = data;
3311
+ for (const key of ["templates", "data", "items"]) {
3312
+ if (Array.isArray(r[key]))
3313
+ return r[key];
3314
+ }
3315
+ return Object.values(r);
3316
+ })();
3317
+ if (items.length === 0)
3318
+ return ok("No templates found.");
3319
+ const lines = ["Templates:"];
3320
+ for (const t of items) {
3321
+ const r = t;
3322
+ lines.push(` ${r["id"] ?? r["slug"]} ${r["name"]} ${r["description"] ?? ""}`);
3323
+ }
3324
+ return ok(lines.join("\n"));
3325
+ }
3326
+ if (name === "sandbox_template_create") {
3327
+ const body = {
3328
+ name: args["name"],
3329
+ build_spec: args["build_spec"],
3330
+ };
3331
+ if (args["slug"])
3332
+ body["slug"] = args["slug"];
3333
+ if (args["description"])
3334
+ body["description"] = args["description"];
3335
+ const result = await client.apiPost("/api/v1/sandbox-templates", body);
3336
+ const data = unwrapData(result);
3337
+ return ok(`Created template '${data["name"] ?? args["name"]}' (id=${data["id"]}).`);
3338
+ }
3339
+ // ── Deploy ────────────────────────────────────────────────────────────
3340
+ if (name === "deploy") {
3341
+ const did = String(args["deployment_id"] ?? "");
3342
+ if (!did)
3343
+ return err("deployment_id is required");
3344
+ const result = await client.apiPost(`/api/v1/deployments/${encodeURIComponent(did)}/redeploy`, {});
3345
+ const data = unwrapData(result);
3346
+ return ok(`Redeploy queued (build id: ${data["id"] ?? "unknown"})`);
3347
+ }
3348
+ // ── Deployments ──────────────────────────────────────────────────────
3349
+ if (name === "deployment_list") {
3350
+ const result = await client.apiGet("/api/v1/deployments");
3351
+ const items = listOf(result);
3352
+ if (items.length === 0)
3353
+ return ok("No deployments found.");
3354
+ const lines = ["Deployments:"];
3355
+ for (const d of items) {
3356
+ const r = d;
3357
+ lines.push(" " +
3358
+ r["id"] +
3359
+ " " +
3360
+ r["name"] +
3361
+ " " +
3362
+ (r["status"] ?? r["state"] ?? ""));
3363
+ }
3364
+ return ok(lines.join("\n"));
3365
+ }
3366
+ if (name === "deployment_get") {
3367
+ const did = String(args["deployment_id"] ?? "");
3368
+ if (!did)
3369
+ return err("deployment_id is required");
3370
+ const result = await client.apiGet("/api/v1/deployments/" + encodeURIComponent(did));
3371
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3372
+ }
3373
+ if (name === "deployment_create") {
3374
+ const body = { name: args["name"] };
3375
+ if (args["type"])
3376
+ body["type"] = args["type"];
3377
+ if (args["source"])
3378
+ body["source"] = args["source"];
3379
+ if (args["env_vars"])
3380
+ body["env_vars"] = args["env_vars"];
3381
+ if (args["region"])
3382
+ body["region"] = args["region"];
3383
+ const result = await client.apiPost("/api/v1/deployments", body);
3384
+ const data = unwrapData(result);
3385
+ return ok("Created deployment '" +
3386
+ String(data["name"] ?? args["name"]) +
3387
+ "' (id=" +
3388
+ data["id"] +
3389
+ ")");
3390
+ }
3391
+ if (name === "deployment_delete") {
3392
+ const did = String(args["deployment_id"] ?? "");
3393
+ if (!did)
3394
+ return err("deployment_id is required");
3395
+ await client.apiDelete("/api/v1/deployments/" + encodeURIComponent(did));
3396
+ return ok("Deployment " + did + " deleted.");
3397
+ }
3398
+ if (name === "deployment_publish") {
3399
+ const did = String(args["deployment_id"] ?? "");
3400
+ if (!did)
3401
+ return err("deployment_id is required");
3402
+ const body = {};
3403
+ if (args["source"])
3404
+ body["source"] = args["source"];
3405
+ const result = await client.apiPost("/api/v1/deployments/" + encodeURIComponent(did) + "/publish", body);
3406
+ const data = unwrapData(result);
3407
+ return ok("Published deployment " +
3408
+ did +
3409
+ " (version id=" +
3410
+ (data["id"] ?? "unknown") +
3411
+ ")");
3412
+ }
3413
+ if (name === "deployment_rollback") {
3414
+ const did = String(args["deployment_id"] ?? "");
3415
+ const vid = String(args["version_id"] ?? "");
3416
+ if (!did)
3417
+ return err("deployment_id is required");
3418
+ if (!vid)
3419
+ return err("version_id is required");
3420
+ await client.apiPost("/api/v1/deployments/" + encodeURIComponent(did) + "/rollback", { version_id: vid });
3421
+ return ok("Deployment " + did + " rolled back to version " + vid + ".");
3422
+ }
3423
+ if (name === "deployment_env_list") {
3424
+ const did = String(args["deployment_id"] ?? "");
3425
+ if (!did)
3426
+ return err("deployment_id is required");
3427
+ const result = await client.apiGet("/api/v1/deployments/" + encodeURIComponent(did) + "/env");
3428
+ const envVars = unwrapData(result);
3429
+ if (!envVars ||
3430
+ (Array.isArray(envVars) && envVars.length === 0))
3431
+ return ok("No environment variables set.");
3432
+ const lines = ["Environment variables:"];
3433
+ if (typeof envVars === "object" && !Array.isArray(envVars)) {
3434
+ for (const [k, v] of Object.entries(envVars)) {
3435
+ lines.push(" " + k + "=" + String(v));
3436
+ }
3437
+ }
3438
+ else if (Array.isArray(envVars)) {
3439
+ for (const e of envVars) {
3440
+ lines.push(" " + e["key"] + "=" + e["value"]);
3441
+ }
3442
+ }
3443
+ return ok(lines.join("\n"));
3444
+ }
3445
+ if (name === "deployment_env_set") {
3446
+ const did = String(args["deployment_id"] ?? "");
3447
+ if (!did)
3448
+ return err("deployment_id is required");
3449
+ await client.apiPost("/api/v1/deployments/" + encodeURIComponent(did) + "/env", { key: args["key"], value: args["value"] });
3450
+ return ok("Set " + String(args["key"]) + " on deployment " + did + ".");
3451
+ }
3452
+ if (name === "deployment_logs") {
3453
+ const did = String(args["deployment_id"] ?? "");
3454
+ if (!did)
3455
+ return err("deployment_id is required");
3456
+ const params = new URLSearchParams({
3457
+ lines: String(args["lines"] ?? 100),
529
3458
  });
530
- return ok(`Dragged from (${args["from_x"]}, ${args["from_y"]}) to (${args["to_x"]}, ${args["to_y"]})`);
3459
+ if (args["since"])
3460
+ params.set("since", String(args["since"]));
3461
+ const result = await client.apiGet("/api/v1/deployments/" +
3462
+ encodeURIComponent(did) +
3463
+ "/logs?" +
3464
+ params.toString());
3465
+ const logs = unwrapData(result);
3466
+ if (Array.isArray(logs))
3467
+ return ok(logs.length
3468
+ ? logs.map(String).join("\n")
3469
+ : "No logs.");
3470
+ return ok(String(logs));
3471
+ }
3472
+ if (name === "deployment_version_list") {
3473
+ const did = String(args["deployment_id"] ?? "");
3474
+ if (!did)
3475
+ return err("deployment_id is required");
3476
+ const result = await client.apiGet("/api/v1/deployments/" + encodeURIComponent(did) + "/versions");
3477
+ const versions = listOf(result);
3478
+ if (versions.length === 0)
3479
+ return ok("No versions found.");
3480
+ const lines = ["Versions:"];
3481
+ for (const v of versions) {
3482
+ const r = v;
3483
+ lines.push(" " +
3484
+ r["id"] +
3485
+ " " +
3486
+ (r["created_at"] ?? "") +
3487
+ " " +
3488
+ (r["status"] ?? ""));
3489
+ }
3490
+ return ok(lines.join("\n"));
3491
+ }
3492
+ if (name === "deployment_version_promote") {
3493
+ const did = String(args["deployment_id"] ?? "");
3494
+ const vid = String(args["version_id"] ?? "");
3495
+ if (!did)
3496
+ return err("deployment_id is required");
3497
+ if (!vid)
3498
+ return err("version_id is required");
3499
+ await client.apiPost("/api/v1/deployments/" +
3500
+ encodeURIComponent(did) +
3501
+ "/versions/" +
3502
+ encodeURIComponent(vid) +
3503
+ "/promote", {});
3504
+ return ok("Version " + vid + " promoted on deployment " + did + ".");
3505
+ }
3506
+ // ── Storage ───────────────────────────────────────────────────────────
3507
+ if (name === "storage_bucket_list") {
3508
+ const result = await client.apiGet("/api/v1/storage/buckets");
3509
+ const items = listOf(result);
3510
+ if (items.length === 0)
3511
+ return ok("No buckets found.");
3512
+ const lines = ["Buckets:"];
3513
+ for (const b of items) {
3514
+ const r = b;
3515
+ lines.push(" " + r["id"] + " " + r["name"] + " " + (r["region"] ?? ""));
3516
+ }
3517
+ return ok(lines.join("\n"));
3518
+ }
3519
+ if (name === "storage_bucket_create") {
3520
+ const body = { name: args["name"] };
3521
+ if (args["region"])
3522
+ body["region"] = args["region"];
3523
+ if (args["public"] !== undefined)
3524
+ body["public"] = args["public"];
3525
+ const result = await client.apiPost("/api/v1/storage/buckets", body);
3526
+ const data = unwrapData(result);
3527
+ return ok("Created bucket '" +
3528
+ String(data["name"] ?? args["name"]) +
3529
+ "' (id=" +
3530
+ data["id"] +
3531
+ ")");
3532
+ }
3533
+ if (name === "storage_bucket_delete") {
3534
+ const bid = String(args["bucket_id"] ?? "");
3535
+ if (!bid)
3536
+ return err("bucket_id is required");
3537
+ await client.apiDelete("/api/v1/storage/buckets/" + encodeURIComponent(bid));
3538
+ return ok("Bucket " + bid + " deleted.");
3539
+ }
3540
+ if (name === "storage_object_list") {
3541
+ const bid = String(args["bucket_id"] ?? "");
3542
+ if (!bid)
3543
+ return err("bucket_id is required");
3544
+ const params = new URLSearchParams();
3545
+ if (args["prefix"])
3546
+ params.set("prefix", String(args["prefix"]));
3547
+ const qs = params.toString() ? "?" + params.toString() : "";
3548
+ const result = await client.apiGet("/api/v1/storage/buckets/" + encodeURIComponent(bid) + "/objects" + qs);
3549
+ const items = listOf(result);
3550
+ if (items.length === 0)
3551
+ return ok("No objects found.");
3552
+ const lines = ["Objects:"];
3553
+ for (const o of items) {
3554
+ const r = o;
3555
+ lines.push(" " +
3556
+ r["key"] +
3557
+ " " +
3558
+ (r["size"] ?? "") +
3559
+ " " +
3560
+ (r["last_modified"] ?? ""));
3561
+ }
3562
+ return ok(lines.join("\n"));
3563
+ }
3564
+ if (name === "storage_object_upload") {
3565
+ const bid = String(args["bucket_id"] ?? "");
3566
+ if (!bid)
3567
+ return err("bucket_id is required");
3568
+ await client.apiPost("/api/v1/storage/buckets/" + encodeURIComponent(bid) + "/objects", {
3569
+ key: args["key"],
3570
+ content: args["content"],
3571
+ content_type: args["content_type"] ?? "application/octet-stream",
3572
+ });
3573
+ return ok("Uploaded " + String(args["key"]) + " to bucket " + bid + ".");
3574
+ }
3575
+ if (name === "storage_object_download") {
3576
+ const bid = String(args["bucket_id"] ?? "");
3577
+ if (!bid)
3578
+ return err("bucket_id is required");
3579
+ const result = await client.apiGet("/api/v1/storage/buckets/" +
3580
+ encodeURIComponent(bid) +
3581
+ "/objects/" +
3582
+ encodeURIComponent(String(args["key"] ?? "")));
3583
+ const data = unwrapData(result);
3584
+ const content = typeof data["content"] === "string"
3585
+ ? Buffer.from(data["content"], "base64").toString("utf8")
3586
+ : typeof data["text"] === "string"
3587
+ ? data["text"]
3588
+ : JSON.stringify(data);
3589
+ return ok(content);
3590
+ }
3591
+ if (name === "storage_object_delete") {
3592
+ const bid = String(args["bucket_id"] ?? "");
3593
+ if (!bid)
3594
+ return err("bucket_id is required");
3595
+ await client.apiDelete("/api/v1/storage/buckets/" +
3596
+ encodeURIComponent(bid) +
3597
+ "/objects/" +
3598
+ encodeURIComponent(String(args["key"] ?? "")));
3599
+ return ok("Deleted " + String(args["key"]) + " from bucket " + bid + ".");
3600
+ }
3601
+ if (name === "storage_presign") {
3602
+ const bid = String(args["bucket_id"] ?? "");
3603
+ if (!bid)
3604
+ return err("bucket_id is required");
3605
+ const result = await client.apiPost("/api/v1/storage/buckets/" + encodeURIComponent(bid) + "/presign", {
3606
+ key: args["key"],
3607
+ expires_in: args["expires_in"] ?? 3600,
3608
+ method: args["method"] ?? "GET",
3609
+ });
3610
+ const data = unwrapData(result);
3611
+ return ok("Presigned URL: " + (data["url"] ?? JSON.stringify(data)));
3612
+ }
3613
+ // ── Databases ─────────────────────────────────────────────────────────
3614
+ if (name === "database_list") {
3615
+ const result = await client.apiGet("/api/v1/databases");
3616
+ const items = listOf(result);
3617
+ if (items.length === 0)
3618
+ return ok("No databases found.");
3619
+ const lines = ["Databases:"];
3620
+ for (const d of items) {
3621
+ const r = d;
3622
+ lines.push(" " +
3623
+ r["id"] +
3624
+ " " +
3625
+ r["name"] +
3626
+ " " +
3627
+ (r["engine"] ?? "") +
3628
+ " " +
3629
+ (r["status"] ?? ""));
3630
+ }
3631
+ return ok(lines.join("\n"));
531
3632
  }
532
- if (name === "computer_scroll") {
533
- if (!cid)
534
- return err("computer_id is required");
3633
+ if (name === "database_create") {
535
3634
  const body = {
536
- direction: args["direction"] ?? "down",
537
- clicks: args["clicks"] ?? 3,
3635
+ name: args["name"],
3636
+ engine: args["engine"],
538
3637
  };
539
- if (args["x"] !== undefined)
540
- body["x"] = args["x"];
541
- if (args["y"] !== undefined)
542
- body["y"] = args["y"];
543
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/scroll`, body);
544
- return ok(`Scrolled ${body["direction"]} by ${body["clicks"]} clicks`);
3638
+ for (const key of ["version", "size", "region"]) {
3639
+ if (args[key])
3640
+ body[key] = args[key];
3641
+ }
3642
+ const result = await client.apiPost("/api/v1/databases", body);
3643
+ const data = unwrapData(result);
3644
+ return ok("Created database '" +
3645
+ String(data["name"] ?? args["name"]) +
3646
+ "' (id=" +
3647
+ data["id"] +
3648
+ ")");
545
3649
  }
546
- // ── Keyboard ──────────────────────────────────────────────────────────
547
- if (name === "computer_type") {
548
- if (!cid)
549
- return err("computer_id is required");
550
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/type`, {
551
- text: args["text"],
552
- });
553
- const preview = typeof args["text"] === "string"
554
- ? args["text"].slice(0, 40) + (args["text"].length > 40 ? "..." : "")
555
- : "";
556
- return ok(`Typed: ${JSON.stringify(preview)}`);
3650
+ if (name === "database_get") {
3651
+ const dbid = String(args["database_id"] ?? "");
3652
+ if (!dbid)
3653
+ return err("database_id is required");
3654
+ const result = await client.apiGet("/api/v1/databases/" + encodeURIComponent(dbid));
3655
+ return ok(JSON.stringify(unwrapData(result), null, 2));
557
3656
  }
558
- if (name === "computer_key") {
559
- if (!cid)
560
- return err("computer_id is required");
561
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/key`, {
562
- key: args["key"],
3657
+ if (name === "database_delete") {
3658
+ const dbid = String(args["database_id"] ?? "");
3659
+ if (!dbid)
3660
+ return err("database_id is required");
3661
+ await client.apiDelete("/api/v1/databases/" + encodeURIComponent(dbid));
3662
+ return ok("Database " + dbid + " deleted.");
3663
+ }
3664
+ if (name === "database_credentials") {
3665
+ const dbid = String(args["database_id"] ?? "");
3666
+ if (!dbid)
3667
+ return err("database_id is required");
3668
+ const result = await client.apiGet("/api/v1/databases/" + encodeURIComponent(dbid) + "/credentials");
3669
+ const data = unwrapData(result);
3670
+ const lines = ["Database credentials:"];
3671
+ for (const field of [
3672
+ "connection_string",
3673
+ "host",
3674
+ "port",
3675
+ "database",
3676
+ "username",
3677
+ "password",
3678
+ ]) {
3679
+ if (data[field])
3680
+ lines.push(" " + field + ": " + data[field]);
3681
+ }
3682
+ return ok(lines.join("\n"));
3683
+ }
3684
+ if (name === "database_logs") {
3685
+ const dbid = String(args["database_id"] ?? "");
3686
+ if (!dbid)
3687
+ return err("database_id is required");
3688
+ const params = new URLSearchParams({
3689
+ lines: String(args["lines"] ?? 100),
563
3690
  });
564
- return ok(`Pressed key: ${args["key"]}`);
3691
+ if (args["since"])
3692
+ params.set("since", String(args["since"]));
3693
+ const result = await client.apiGet("/api/v1/databases/" +
3694
+ encodeURIComponent(dbid) +
3695
+ "/logs?" +
3696
+ params.toString());
3697
+ const logs = unwrapData(result);
3698
+ if (Array.isArray(logs))
3699
+ return ok(logs.length
3700
+ ? logs.map(String).join("\n")
3701
+ : "No logs.");
3702
+ return ok(String(logs));
565
3703
  }
566
- if (name === "computer_hotkey") {
3704
+ // ── Workspaces ────────────────────────────────────────────────────────
3705
+ if (name === "workspace_list") {
3706
+ const result = await client.apiGet("/api/v1/workspaces");
3707
+ const items = listOf(result);
3708
+ if (items.length === 0)
3709
+ return ok("No workspaces found.");
3710
+ const lines = ["Workspaces:"];
3711
+ for (const w of items) {
3712
+ const r = w;
3713
+ lines.push(" " + r["id"] + " " + r["name"]);
3714
+ }
3715
+ return ok(lines.join("\n"));
3716
+ }
3717
+ if (name === "workspace_create") {
3718
+ const body = { name: args["name"] };
3719
+ if (args["description"])
3720
+ body["description"] = args["description"];
3721
+ const result = await client.apiPost("/api/v1/workspaces", body);
3722
+ const data = unwrapData(result);
3723
+ return ok("Created workspace '" +
3724
+ String(data["name"] ?? args["name"]) +
3725
+ "' (id=" +
3726
+ data["id"] +
3727
+ ")");
3728
+ }
3729
+ if (name === "workspace_get") {
3730
+ const wid = String(args["workspace_id"] ?? "");
3731
+ if (!wid)
3732
+ return err("workspace_id is required");
3733
+ const result = await client.apiGet("/api/v1/workspaces/" + encodeURIComponent(wid));
3734
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3735
+ }
3736
+ if (name === "workspace_update") {
3737
+ const wid = String(args["workspace_id"] ?? "");
3738
+ if (!wid)
3739
+ return err("workspace_id is required");
3740
+ const body = {};
3741
+ if (args["name"])
3742
+ body["name"] = args["name"];
3743
+ if (args["description"])
3744
+ body["description"] = args["description"];
3745
+ await client.apiPost("/api/v1/workspaces/" + encodeURIComponent(wid), body);
3746
+ return ok("Workspace " + wid + " updated.");
3747
+ }
3748
+ if (name === "workspace_stats") {
3749
+ const wid = String(args["workspace_id"] ?? "");
3750
+ if (!wid)
3751
+ return err("workspace_id is required");
3752
+ const result = await client.apiGet("/api/v1/workspaces/" + encodeURIComponent(wid) + "/stats");
3753
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3754
+ }
3755
+ if (name === "workspace_usage") {
3756
+ const wid = String(args["workspace_id"] ?? "");
3757
+ if (!wid)
3758
+ return err("workspace_id is required");
3759
+ const params = new URLSearchParams();
3760
+ if (args["period"])
3761
+ params.set("period", String(args["period"]));
3762
+ const qs = params.toString() ? "?" + params.toString() : "";
3763
+ const result = await client.apiGet("/api/v1/workspaces/" + encodeURIComponent(wid) + "/usage" + qs);
3764
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3765
+ }
3766
+ // ── Billing ───────────────────────────────────────────────────────────
3767
+ if (name === "billing_usage") {
3768
+ const result = await client.apiGet("/api/v1/billing/usage");
3769
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3770
+ }
3771
+ if (name === "billing_plan") {
3772
+ const result = await client.apiGet("/api/v1/billing/plan");
3773
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3774
+ }
3775
+ // ── Tunnels / Port forwarding ─────────────────────────────────────────
3776
+ if (name === "computer_expose_port") {
567
3777
  if (!cid)
568
3778
  return err("computer_id is required");
569
- const keys = args["keys"];
570
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/hotkey`, {
571
- keys,
572
- });
573
- return ok(`Pressed hotkey: ${keys.join("+")}`);
3779
+ const body = { port: args["port"] };
3780
+ if (args["protocol"])
3781
+ body["protocol"] = args["protocol"];
3782
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/ports`, body);
3783
+ const data = unwrapData(result);
3784
+ const url = String(data["url"] ?? data["public_url"] ?? "");
3785
+ return ok(`Port ${args["port"]} exposed. URL: ${url}`);
574
3786
  }
575
- // ── Display info ──────────────────────────────────────────────────────
576
- if (name === "computer_get_screen_size") {
3787
+ if (name === "computer_list_ports") {
577
3788
  if (!cid)
578
3789
  return err("computer_id is required");
579
- const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/screen-size`);
580
- const data = unwrapData(result);
581
- return ok(`Screen size: ${data["width"]}x${data["height"]} px`);
3790
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/ports`);
3791
+ const items = listOf(result);
3792
+ if (items.length === 0)
3793
+ return ok("No ports currently exposed.");
3794
+ const lines = ["Exposed ports:"];
3795
+ for (const p of items) {
3796
+ const r = p;
3797
+ lines.push(` port=${r["port"]} protocol=${r["protocol"] ?? "http"} url=${r["url"] ?? r["public_url"] ?? ""}`);
3798
+ }
3799
+ return ok(lines.join("\n"));
582
3800
  }
583
- if (name === "computer_get_cursor_position") {
3801
+ if (name === "computer_preview_url") {
584
3802
  if (!cid)
585
3803
  return err("computer_id is required");
586
- const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/cursor`);
3804
+ const port = args["port"];
3805
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/ports/${encodeURIComponent(String(port))}/url`);
587
3806
  const data = unwrapData(result);
588
- return ok(`Cursor position: x=${data["x"]}, y=${data["y"]}`);
3807
+ const url = String(data["url"] ?? data["public_url"] ?? "") ||
3808
+ `https://${port}-${cid}.computer.miosa.ai`;
3809
+ return ok(`Preview URL: ${url}`);
589
3810
  }
590
- // ── Clipboard ─────────────────────────────────────────────────────────
591
- if (name === "computer_get_clipboard") {
3811
+ // ── Network policy ────────────────────────────────────────────────────
3812
+ if (name === "computer_network_policy_get") {
592
3813
  if (!cid)
593
3814
  return err("computer_id is required");
594
- const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/clipboard`);
595
- const data = unwrapData(result);
596
- return ok(`Clipboard content:\n${data["text"] ?? ""}`);
3815
+ const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/network-policy`);
3816
+ return ok(JSON.stringify(unwrapData(result), null, 2));
597
3817
  }
598
- if (name === "computer_set_clipboard") {
3818
+ if (name === "computer_network_policy_set") {
599
3819
  if (!cid)
600
3820
  return err("computer_id is required");
601
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/clipboard`, {
602
- text: args["text"],
603
- });
604
- return ok("Clipboard updated.");
3821
+ const body = { rules: args["rules"] };
3822
+ if (args["default_effect"])
3823
+ body["default_effect"] = args["default_effect"];
3824
+ await client.apiPut(`/api/v1/computers/${encodeURIComponent(cid)}/network-policy`, body);
3825
+ return ok(`Network policy updated for computer ${cid}.`);
605
3826
  }
606
- // ── Window management ─────────────────────────────────────────────────
607
- if (name === "computer_windows") {
3827
+ if (name === "computer_network_policy_reset") {
608
3828
  if (!cid)
609
3829
  return err("computer_id is required");
610
- const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/windows`);
611
- const windows = listOf(result);
612
- if (windows.length === 0)
613
- return ok("No open windows found.");
614
- const lines = ["Open windows:"];
615
- for (const w of windows) {
3830
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(cid)}/network-policy`);
3831
+ return ok(`Network policy reset to default for computer ${cid}.`);
3832
+ }
3833
+ // ── Webhooks ──────────────────────────────────────────────────────────
3834
+ if (name === "webhook_list") {
3835
+ const result = await client.apiGet("/api/v1/webhooks");
3836
+ const items = listOf(result);
3837
+ if (items.length === 0)
3838
+ return ok("No webhooks found.");
3839
+ const lines = ["Webhooks:"];
3840
+ for (const w of items) {
616
3841
  const r = w;
617
- const focused = r["focused"] ? " [focused]" : "";
618
- lines.push(` id=${r["id"]} title=${JSON.stringify(r["title"])} app=${JSON.stringify(r["app"])}` +
619
- ` pos=(${r["x"]},${r["y"]}) size=${r["width"]}x${r["height"]}${focused}`);
3842
+ lines.push(` ${r["id"]} ${r["url"]} events=${JSON.stringify(r["events"] ?? [])}`);
620
3843
  }
621
3844
  return ok(lines.join("\n"));
622
3845
  }
623
- if (name === "computer_launch") {
624
- if (!cid)
625
- return err("computer_id is required");
626
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/desktop/launch`, {
627
- app: args["app"],
3846
+ if (name === "webhook_create") {
3847
+ const result = await client.apiPost("/api/v1/webhooks", {
3848
+ url: args["url"],
3849
+ events: args["events"],
628
3850
  });
629
- return ok(`Launched: ${args["app"]}`);
3851
+ const data = unwrapData(result);
3852
+ return ok(`Created webhook (id=${data["id"] ?? "?"}).`);
630
3853
  }
631
- // ── Shell & Files ─────────────────────────────────────────────────────
632
- if (name === "computer_bash") {
633
- if (!cid)
634
- return err("computer_id is required");
635
- const body = { command: args["command"] };
636
- if (args["timeout"] !== undefined)
637
- body["timeout"] = args["timeout"];
638
- const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/exec`, body);
3854
+ if (name === "webhook_delete") {
3855
+ const whId = String(args["webhook_id"] ?? "");
3856
+ if (!whId)
3857
+ return err("webhook_id is required");
3858
+ await client.apiDelete(`/api/v1/webhooks/${encodeURIComponent(whId)}`);
3859
+ return ok(`Webhook ${whId} deleted.`);
3860
+ }
3861
+ if (name === "webhook_test") {
3862
+ const whId = String(args["webhook_id"] ?? "");
3863
+ if (!whId)
3864
+ return err("webhook_id is required");
3865
+ const result = await client.apiPost(`/api/v1/webhooks/${encodeURIComponent(whId)}/test`, {});
639
3866
  const data = unwrapData(result);
640
- const parts = [];
641
- if (data["output"] ?? data["stdout"]) {
642
- parts.push(`stdout:\n${data["output"] ?? data["stdout"]}`);
643
- }
644
- if (data["stderr"])
645
- parts.push(`stderr:\n${data["stderr"]}`);
646
- parts.push(`exit_code: ${data["exit_code"] ?? 0}`);
647
- return ok(parts.join("\n"));
3867
+ const status = String(data["status"] ?? "delivered");
3868
+ return ok(`Test event sent to webhook ${whId} (status=${status}).`);
648
3869
  }
649
- if (name === "computer_write_file") {
650
- if (!cid)
651
- return err("computer_id is required");
652
- await client.apiPost(`/api/v1/computers/${encodeURIComponent(cid)}/files/write`, {
653
- path: args["path"],
654
- content: args["content"],
655
- encoding: "utf8",
656
- });
657
- const len = typeof args["content"] === "string" ? args["content"].length : 0;
658
- return ok(`Wrote ${len} bytes to ${args["path"]}`);
3870
+ // ── Functions ─────────────────────────────────────────────────────────
3871
+ if (name === "function_list") {
3872
+ const result = await client.apiGet("/api/v1/functions");
3873
+ const items = listOf(result);
3874
+ if (items.length === 0)
3875
+ return ok("No functions found.");
3876
+ const lines = ["Functions:"];
3877
+ for (const f of items) {
3878
+ const r = f;
3879
+ lines.push(` ${r["id"]} ${r["name"]} runtime=${r["runtime"] ?? ""}`);
3880
+ }
3881
+ return ok(lines.join("\n"));
659
3882
  }
660
- if (name === "computer_read_file") {
661
- if (!cid)
662
- return err("computer_id is required");
663
- const result = await client.apiGet(`/api/v1/computers/${encodeURIComponent(cid)}/files/download?path=${encodeURIComponent(String(args["path"] ?? ""))}`);
3883
+ if (name === "function_create") {
3884
+ const body = {
3885
+ name: args["name"],
3886
+ runtime: args["runtime"],
3887
+ };
3888
+ if (args["code"])
3889
+ body["code"] = args["code"];
3890
+ const result = await client.apiPost("/api/v1/functions", body);
664
3891
  const data = unwrapData(result);
665
- const content = typeof data["content"] === "string"
666
- ? Buffer.from(data["content"], "base64").toString("utf8")
667
- : typeof data["text"] === "string"
668
- ? data["text"]
669
- : JSON.stringify(data);
670
- return ok(content);
3892
+ return ok(`Created function '${data["name"] ?? args["name"]}' (id=${data["id"]}).`);
671
3893
  }
672
- // ── Sandboxes ─────────────────────────────────────────────────────────
673
- if (name === "sandbox_create") {
3894
+ if (name === "function_invoke") {
3895
+ const fnId = String(args["function_id"] ?? "");
3896
+ if (!fnId)
3897
+ return err("function_id is required");
674
3898
  const body = {};
3899
+ if (args["payload"] !== undefined)
3900
+ body["payload"] = args["payload"];
3901
+ const result = await client.apiPost(`/api/v1/functions/${encodeURIComponent(fnId)}/invoke`, body);
3902
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3903
+ }
3904
+ if (name === "function_delete") {
3905
+ const fnId = String(args["function_id"] ?? "");
3906
+ if (!fnId)
3907
+ return err("function_id is required");
3908
+ await client.apiDelete(`/api/v1/functions/${encodeURIComponent(fnId)}`);
3909
+ return ok(`Function ${fnId} deleted.`);
3910
+ }
3911
+ // ── API Keys ──────────────────────────────────────────────────────────
3912
+ if (name === "api_key_list") {
3913
+ const result = await client.apiGet("/api/v1/api-keys");
3914
+ const items = listOf(result);
3915
+ if (items.length === 0)
3916
+ return ok("No API keys found.");
3917
+ const lines = ["API keys:"];
3918
+ for (const k of items) {
3919
+ const r = k;
3920
+ const scopes = Array.isArray(r["scopes"])
3921
+ ? r["scopes"].join(", ")
3922
+ : String(r["scopes"] ?? "");
3923
+ lines.push(` ${r["id"]} ${r["name"]} scopes=[${scopes}]`);
3924
+ }
3925
+ return ok(lines.join("\n"));
3926
+ }
3927
+ if (name === "api_key_create") {
3928
+ const body = { name: args["name"] };
3929
+ if (args["scopes"])
3930
+ body["scopes"] = args["scopes"];
3931
+ const result = await client.apiPost("/api/v1/api-keys", body);
3932
+ const data = unwrapData(result);
3933
+ const keyId = String(data["id"] ?? "?");
3934
+ const keyValue = String(data["key"] ?? data["token"] ?? data["secret"] ?? "");
3935
+ let msg = `Created API key '${args["name"]}' (id=${keyId}).`;
3936
+ if (keyValue)
3937
+ msg += `\nKey value (shown once): ${keyValue}`;
3938
+ return ok(msg);
3939
+ }
3940
+ if (name === "api_key_delete") {
3941
+ const keyId = String(args["key_id"] ?? "");
3942
+ if (!keyId)
3943
+ return err("key_id is required");
3944
+ await client.apiDelete(`/api/v1/api-keys/${encodeURIComponent(keyId)}`);
3945
+ return ok(`API key ${keyId} deleted.`);
3946
+ }
3947
+ // ── Cron jobs ─────────────────────────────────────────────────────────
3948
+ if (name === "cron_list") {
3949
+ const params = new URLSearchParams();
3950
+ if (args["computer_id"])
3951
+ params.set("computer_id", String(args["computer_id"]));
3952
+ const qs = params.toString() ? `?${params.toString()}` : "";
3953
+ const result = await client.apiGet(`/api/v1/cron-jobs${qs}`);
3954
+ const items = listOf(result);
3955
+ if (items.length === 0)
3956
+ return ok("No cron jobs found.");
3957
+ const lines = ["Cron jobs:"];
3958
+ for (const j of items) {
3959
+ const r = j;
3960
+ lines.push(` ${r["id"]} ${r["name"] ?? ""} ${r["schedule"] ?? ""} status=${r["status"] ?? r["state"] ?? ""}`);
3961
+ }
3962
+ return ok(lines.join("\n"));
3963
+ }
3964
+ if (name === "cron_create") {
3965
+ const body = {
3966
+ computer_id: args["computer_id"],
3967
+ schedule: args["schedule"],
3968
+ command: args["command"],
3969
+ };
675
3970
  if (args["name"])
676
3971
  body["name"] = args["name"];
677
- if (args["template_id"])
678
- body["template_id"] = args["template_id"];
679
- if (args["cpu_count"] !== undefined)
680
- body["cpu_count"] = args["cpu_count"];
681
- if (args["memory_mb"] !== undefined)
682
- body["memory_mb"] = args["memory_mb"];
683
- if (args["timeout_sec"] !== undefined)
684
- body["timeout_sec"] = args["timeout_sec"];
685
- const result = await client.apiPost("/api/v1/sandboxes", body);
3972
+ const result = await client.apiPost("/api/v1/cron-jobs", body);
686
3973
  const data = unwrapData(result);
687
- const sid = String(data["id"] ?? "");
688
- return ok(`Created sandbox '${data["name"] ?? sid}' (id=${sid}).`);
3974
+ return ok(`Created cron job '${data["name"] ?? args["name"] ?? ""}' (id=${data["id"]}).`);
689
3975
  }
690
- if (name === "sandbox_exec") {
691
- const sid = String(args["sandbox_id"] ?? "");
692
- if (!sid)
693
- return err("sandbox_id is required");
694
- const body = { command: args["command"] };
695
- if (args["cwd"])
696
- body["cwd"] = args["cwd"];
697
- if (args["timeout"] !== undefined)
698
- body["timeout"] = args["timeout"];
699
- const result = await client.apiPost(`/api/v1/sandboxes/${encodeURIComponent(sid)}/exec`, body);
3976
+ if (name === "cron_get") {
3977
+ const cronId = String(args["cron_id"] ?? "");
3978
+ if (!cronId)
3979
+ return err("cron_id is required");
3980
+ const result = await client.apiGet(`/api/v1/cron-jobs/${encodeURIComponent(cronId)}`);
3981
+ return ok(JSON.stringify(unwrapData(result), null, 2));
3982
+ }
3983
+ if (name === "cron_delete") {
3984
+ const cronId = String(args["cron_id"] ?? "");
3985
+ if (!cronId)
3986
+ return err("cron_id is required");
3987
+ await client.apiDelete(`/api/v1/cron-jobs/${encodeURIComponent(cronId)}`);
3988
+ return ok(`Cron job ${cronId} deleted.`);
3989
+ }
3990
+ if (name === "cron_pause") {
3991
+ const cronId = String(args["cron_id"] ?? "");
3992
+ if (!cronId)
3993
+ return err("cron_id is required");
3994
+ await client.apiPost(`/api/v1/cron-jobs/${encodeURIComponent(cronId)}/pause`, {});
3995
+ return ok(`Cron job ${cronId} paused.`);
3996
+ }
3997
+ if (name === "cron_resume") {
3998
+ const cronId = String(args["cron_id"] ?? "");
3999
+ if (!cronId)
4000
+ return err("cron_id is required");
4001
+ await client.apiPost(`/api/v1/cron-jobs/${encodeURIComponent(cronId)}/resume`, {});
4002
+ return ok(`Cron job ${cronId} resumed.`);
4003
+ }
4004
+ if (name === "cron_run_now") {
4005
+ const cronId = String(args["cron_id"] ?? "");
4006
+ if (!cronId)
4007
+ return err("cron_id is required");
4008
+ const result = await client.apiPost(`/api/v1/cron-jobs/${encodeURIComponent(cronId)}/run-now`, {});
700
4009
  const data = unwrapData(result);
701
- const parts = [];
702
- if (data["output"] ?? data["stdout"]) {
703
- parts.push(`stdout:\n${data["output"] ?? data["stdout"]}`);
4010
+ const execId = data["id"] ?? data["execution_id"];
4011
+ return ok(`Cron job ${cronId} triggered.${execId ? ` Execution id=${execId}.` : ""}`);
4012
+ }
4013
+ if (name === "cron_executions") {
4014
+ const cronId = String(args["cron_id"] ?? "");
4015
+ if (!cronId)
4016
+ return err("cron_id is required");
4017
+ const result = await client.apiGet(`/api/v1/cron-jobs/${encodeURIComponent(cronId)}/executions`);
4018
+ const items = listOf(result);
4019
+ if (items.length === 0)
4020
+ return ok("No executions found.");
4021
+ const lines = ["Executions:"];
4022
+ for (const e of items) {
4023
+ const r = e;
4024
+ lines.push(` ${r["id"]} ${r["started_at"] ?? r["created_at"] ?? ""} status=${r["status"] ?? ""} exit_code=${r["exit_code"] ?? ""}`);
704
4025
  }
705
- if (data["stderr"])
706
- parts.push(`stderr:\n${data["stderr"]}`);
707
- parts.push(`exit_code: ${data["exit_code"] ?? 0}`);
708
- return ok(parts.join("\n"));
4026
+ return ok(lines.join("\n"));
709
4027
  }
710
- if (name === "sandbox_destroy") {
711
- const sid = String(args["sandbox_id"] ?? "");
712
- if (!sid)
713
- return err("sandbox_id is required");
714
- await client.apiDelete(`/api/v1/sandboxes/${encodeURIComponent(sid)}`);
715
- return ok(`Sandbox ${sid} destroyed.`);
4028
+ // ── Regions ────────────────────────────────────────────────────────────
4029
+ if (name === "region_list" || name === "computer_list_regions") {
4030
+ const result = await client.apiGet("/api/v1/regions");
4031
+ const regions = (() => {
4032
+ const d = unwrapData(result);
4033
+ if (Array.isArray(d))
4034
+ return d;
4035
+ const r = d;
4036
+ for (const key of ["regions", "data", "items"]) {
4037
+ if (Array.isArray(r[key]))
4038
+ return r[key];
4039
+ }
4040
+ return Object.values(r);
4041
+ })();
4042
+ if (regions.length === 0)
4043
+ return ok("No regions found.");
4044
+ const lines = ["Regions:"];
4045
+ for (const region of regions) {
4046
+ const r = region;
4047
+ const gpuTypes = r["gpu_types"] ?? r["gpus"] ?? [];
4048
+ const gpuInfo = Array.isArray(gpuTypes) && gpuTypes.length > 0
4049
+ ? ` gpus=${JSON.stringify(gpuTypes)}`
4050
+ : "";
4051
+ lines.push(` ${r["id"] ?? r["slug"] ?? ""} ${r["name"] ?? ""} status=${r["status"] ?? "available"}${gpuInfo}`);
4052
+ }
4053
+ return ok(lines.join("\n"));
716
4054
  }
717
- // ── Deploy ────────────────────────────────────────────────────────────
718
- if (name === "deploy") {
719
- const did = String(args["deployment_id"] ?? "");
720
- if (!did)
721
- return err("deployment_id is required");
722
- const result = await client.apiPost(`/api/v1/deployments/${encodeURIComponent(did)}/redeploy`, {});
4055
+ // ── Computer templates ─────────────────────────────────────────────────
4056
+ if (name === "computer_template_list") {
4057
+ const wid = String(args["workspace_id"] ?? "");
4058
+ if (!wid)
4059
+ return err("workspace_id is required");
4060
+ const result = await client.apiGet(`/api/v1/workspaces/${encodeURIComponent(wid)}/computer-templates`);
4061
+ const items = listOf(result);
4062
+ if (items.length === 0)
4063
+ return ok("No computer templates found.");
4064
+ const lines = ["Computer templates:"];
4065
+ for (const t of items) {
4066
+ const r = t;
4067
+ lines.push(` ${r["id"]} ${r["name"]} type=${r["template_type"] ?? ""} size=${r["size"] ?? ""}`);
4068
+ }
4069
+ return ok(lines.join("\n"));
4070
+ }
4071
+ if (name === "computer_template_create") {
4072
+ const wid = String(args["workspace_id"] ?? "");
4073
+ if (!wid)
4074
+ return err("workspace_id is required");
4075
+ const body = { name: args["name"] };
4076
+ if (args["template_type"])
4077
+ body["template_type"] = args["template_type"];
4078
+ if (args["size"])
4079
+ body["size"] = args["size"];
4080
+ if (args["selected_apps"])
4081
+ body["selected_apps"] = args["selected_apps"];
4082
+ if (args["settings"])
4083
+ body["settings"] = args["settings"];
4084
+ const result = await client.apiPost(`/api/v1/workspaces/${encodeURIComponent(wid)}/computer-templates`, body);
723
4085
  const data = unwrapData(result);
724
- return ok(`Redeploy queued (build id: ${data["id"] ?? "unknown"})`);
4086
+ return ok(`Created computer template '${data["name"] ?? args["name"]}' (id=${data["id"]}).`);
4087
+ }
4088
+ // ── Settings ───────────────────────────────────────────────────────────
4089
+ if (name === "settings_get") {
4090
+ const result = await client.apiGet("/api/v1/settings");
4091
+ return ok(JSON.stringify(unwrapData(result), null, 2));
4092
+ }
4093
+ if (name === "settings_get_branding") {
4094
+ const result = await client.apiGet("/api/v1/settings/branding");
4095
+ return ok(JSON.stringify(unwrapData(result), null, 2));
4096
+ }
4097
+ if (name === "settings_update_branding") {
4098
+ const body = {};
4099
+ if (args["desktop_wallpaper_url"])
4100
+ body["desktop_wallpaper_url"] = args["desktop_wallpaper_url"];
4101
+ if (args["logo_url"])
4102
+ body["logo_url"] = args["logo_url"];
4103
+ const result = await client.apiPut("/api/v1/settings/branding", body);
4104
+ return ok(`Branding updated: ${JSON.stringify(unwrapData(result), null, 2)}`);
4105
+ }
4106
+ if (name === "settings_compute_pricing") {
4107
+ const result = await client.apiGet("/api/v1/settings/compute-pricing");
4108
+ return ok(JSON.stringify(unwrapData(result), null, 2));
4109
+ }
4110
+ // ── Sandbox template extensions ────────────────────────────────────────
4111
+ if (name === "sandbox_template_get") {
4112
+ const tid = String(args["template_id"] ?? "");
4113
+ if (!tid)
4114
+ return err("template_id is required");
4115
+ const result = await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(tid)}`);
4116
+ return ok(JSON.stringify(unwrapData(result), null, 2));
4117
+ }
4118
+ if (name === "sandbox_template_builds") {
4119
+ const tid = String(args["template_id"] ?? "");
4120
+ if (!tid)
4121
+ return err("template_id is required");
4122
+ const result = await client.apiGet(`/api/v1/sandbox-templates/${encodeURIComponent(tid)}/builds`);
4123
+ const items = listOf(result);
4124
+ if (items.length === 0)
4125
+ return ok("No builds found.");
4126
+ const lines = ["Builds:"];
4127
+ for (const b of items) {
4128
+ const r = b;
4129
+ lines.push(` ${r["id"]} ${r["status"] ?? ""} created_at=${r["created_at"] ?? ""}`);
4130
+ }
4131
+ return ok(lines.join("\n"));
4132
+ }
4133
+ // ── Volumes ───────────────────────────────────────────────────────────
4134
+ if (name === "volume_list") {
4135
+ const result = await client.apiGet("/api/v1/volumes");
4136
+ const items = listOf(result);
4137
+ if (items.length === 0)
4138
+ return ok("No volumes found.");
4139
+ const lines = ["Volumes:"];
4140
+ for (const v of items) {
4141
+ const r = v;
4142
+ lines.push(` ${r["id"]} ${r["name"]} size_gb=${r["size_gb"] ?? ""} region=${r["region"] ?? ""} status=${r["status"] ?? ""}`);
4143
+ }
4144
+ return ok(lines.join("\n"));
4145
+ }
4146
+ if (name === "volume_create") {
4147
+ const body = { name: args["name"] };
4148
+ if (args["size_gb"] !== undefined)
4149
+ body["size_gb"] = args["size_gb"];
4150
+ if (args["region"])
4151
+ body["region"] = args["region"];
4152
+ const result = await client.apiPost("/api/v1/volumes", body);
4153
+ const data = unwrapData(result);
4154
+ return ok(`Created volume '${data["name"] ?? args["name"]}' (id=${data["id"]}).`);
4155
+ }
4156
+ if (name === "volume_get") {
4157
+ const vid = String(args["volume_id"] ?? "");
4158
+ if (!vid)
4159
+ return err("volume_id is required");
4160
+ const result = await client.apiGet(`/api/v1/volumes/${encodeURIComponent(vid)}`);
4161
+ const data = unwrapData(result);
4162
+ return ok(`id=${data["id"]} name=${JSON.stringify(data["name"])} size_gb=${data["size_gb"] ?? ""} region=${data["region"] ?? ""} status=${data["status"] ?? ""}`);
4163
+ }
4164
+ if (name === "volume_delete") {
4165
+ const vid = String(args["volume_id"] ?? "");
4166
+ if (!vid)
4167
+ return err("volume_id is required");
4168
+ await client.apiDelete(`/api/v1/volumes/${encodeURIComponent(vid)}`);
4169
+ return ok(`Volume ${vid} deleted.`);
4170
+ }
4171
+ if (name === "volume_attach") {
4172
+ const attachCid = String(args["computer_id"] ?? "");
4173
+ if (!attachCid)
4174
+ return err("computer_id is required");
4175
+ const attachBody = {
4176
+ volume_id: args["volume_id"],
4177
+ };
4178
+ if (args["mount_path"])
4179
+ attachBody["mount_path"] = args["mount_path"];
4180
+ const result = await client.apiPost(`/api/v1/computers/${encodeURIComponent(attachCid)}/volumes`, attachBody);
4181
+ const data = unwrapData(result);
4182
+ return ok(`Volume ${args["volume_id"]} attached to computer ${attachCid} (attachment id=${data["id"] ?? "?"}).`);
4183
+ }
4184
+ if (name === "volume_detach") {
4185
+ const detachCid = String(args["computer_id"] ?? "");
4186
+ if (!detachCid)
4187
+ return err("computer_id is required");
4188
+ const attId = String(args["attachment_id"] ?? "");
4189
+ if (!attId)
4190
+ return err("attachment_id is required");
4191
+ await client.apiDelete(`/api/v1/computers/${encodeURIComponent(detachCid)}/volumes/${encodeURIComponent(attId)}`);
4192
+ return ok(`Attachment ${attId} removed from computer ${detachCid}.`);
725
4193
  }
726
4194
  return err(`Unknown tool: ${name}`);
727
4195
  }