focalapi-cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -155,7 +155,7 @@ function displayWidth(s) {
155
155
  }
156
156
 
157
157
  // src/lib/version.ts
158
- var VERSION = true ? "0.1.1" : "0.0.0-dev";
158
+ var VERSION = true ? "0.2.0" : "0.0.0-dev";
159
159
 
160
160
  // src/commands/auth.ts
161
161
  import { createInterface } from "readline/promises";
@@ -465,6 +465,65 @@ function registerAuth(program) {
465
465
  });
466
466
  }
467
467
 
468
+ // src/lib/model-selection.ts
469
+ var RECOMMENDED_MODELS = {
470
+ image: [
471
+ "seedream-5-0-260128",
472
+ "gpt-image-2",
473
+ "gemini-3.1-flash-image",
474
+ "grok-imagine-image-quality",
475
+ "seedream-4-5-251128"
476
+ ],
477
+ video: [
478
+ "dreamina-seedance-2-5-260628",
479
+ "veo-3.1-generate-preview",
480
+ "grok-imagine-video-1.5",
481
+ "dreamina-seedance-2-0-260128",
482
+ "veo-3.1-fast-generate-preview"
483
+ ]
484
+ };
485
+ function parseCreativeCapability(value) {
486
+ const normalized = value.trim().toLowerCase();
487
+ if (normalized === "image" || normalized === "video") return normalized;
488
+ throw new ApiError("invalid_request", `\u4E0D\u652F\u6301\u7684\u521B\u4F5C\u80FD\u529B\uFF1A${value}`, {
489
+ hint: "\u53EF\u9009\uFF1Aimage | video\u3002"
490
+ });
491
+ }
492
+ async function resolveCreativeModel(auth, capability) {
493
+ const endpointType = capability === "image" ? "image-generation" : "video-generation";
494
+ const listed = await request({
495
+ baseUrl: auth.baseUrl,
496
+ path: "/v1/models",
497
+ apiKey: auth.apiKey
498
+ });
499
+ const available = new Map((listed.data ?? []).map((model) => [model.id, model]));
500
+ const ranked = RECOMMENDED_MODELS[capability].filter((id) => available.has(id));
501
+ const discovered = (listed.data ?? []).filter((model) => model.supported_endpoint_types?.includes(endpointType)).map((model) => model.id).filter((id) => !ranked.includes(id));
502
+ const candidates = [...ranked, ...discovered];
503
+ for (const id of candidates) {
504
+ const contract = await request({
505
+ baseUrl: auth.baseUrl,
506
+ path: `/v1/models/${encodeURIComponent(id)}`,
507
+ apiKey: auth.apiKey
508
+ });
509
+ if (contract.error || !contract.id || !contract.supported_endpoint_types?.includes(endpointType)) {
510
+ continue;
511
+ }
512
+ const alternatives = candidates.filter((candidate) => candidate !== id && available.has(candidate)).slice(0, 4);
513
+ return {
514
+ capability,
515
+ endpoint_type: endpointType,
516
+ model: contract,
517
+ selected_by: "focalapi-default",
518
+ next_command: capability === "image" ? `focalapi gen image "<prompt>" -m ${id} --json` : `focalapi gen video "<prompt>" -m ${id} --no-wait --json`,
519
+ alternatives
520
+ };
521
+ }
522
+ throw new ApiError("model_not_found", `\u5F53\u524D Key \u6CA1\u6709\u53EF\u76F4\u63A5\u8C03\u7528\u7684${capability === "image" ? "\u56FE\u50CF" : "\u89C6\u9891"}\u751F\u6210\u6A21\u578B`, {
523
+ hint: "\u8FD0\u884C focalapi models list --json \u67E5\u770B\u5F53\u524D\u6A21\u578B\u6C60\uFF1B\u4E0D\u8981\u731C\u6D4B\u6216\u53CD\u590D\u8BD5\u6A21\u578B ID\u3002"
524
+ });
525
+ }
526
+
468
527
  // src/commands/models.ts
469
528
  function textValue(value) {
470
529
  if (value === null || value === void 0 || value === "") return "-";
@@ -478,7 +537,7 @@ function parameterConstraint(parameter) {
478
537
  if (parameter.default !== void 0) constraints.push(`\u9ED8\u8BA4 ${String(parameter.default)}`);
479
538
  if (parameter.values?.length) constraints.push(`\u53EF\u9009 ${parameter.values.join(" / ")}`);
480
539
  if (parameter.minimum !== void 0 || parameter.maximum !== void 0) {
481
- constraints.push(`${parameter.minimum ?? "-\u221E"}\u2013${parameter.maximum ?? "\u221E"}`);
540
+ constraints.push(`${parameter.minimum ?? "-"}\u2013${parameter.maximum ?? "-"}`);
482
541
  }
483
542
  return [parameter.type, ...constraints, parameter.description].join("\uFF1B");
484
543
  }
@@ -499,38 +558,90 @@ function printModelDetails(model) {
499
558
  );
500
559
  }
501
560
  }
561
+ function filterModels(models, query, endpoint) {
562
+ const normalizedQuery = query?.trim().toLowerCase();
563
+ const normalizedEndpoint = endpoint?.trim().toLowerCase();
564
+ return models.filter((model) => {
565
+ if (normalizedQuery && !model.id.toLowerCase().includes(normalizedQuery)) return false;
566
+ return !normalizedEndpoint || (model.supported_endpoint_types ?? []).some((item) => item.toLowerCase() === normalizedEndpoint);
567
+ });
568
+ }
569
+ function printModelList(models, json) {
570
+ if (json) {
571
+ printJson({ data: models });
572
+ return;
573
+ }
574
+ printTable(
575
+ ["\u6A21\u578B ID", "\u63D0\u4F9B\u65B9", "\u652F\u6301\u7AEF\u70B9"],
576
+ models.map((model) => [model.id, model.owned_by ?? "-", textValue(model.supported_endpoint_types)])
577
+ );
578
+ }
579
+ async function fetchModels(g) {
580
+ const auth = resolveAuth(g);
581
+ const response = await request({ baseUrl: auth.baseUrl, path: "/v1/models", apiKey: auth.apiKey });
582
+ return response.data ?? [];
583
+ }
502
584
  function registerModels(program) {
503
585
  const models = program.command("models").description("\u53EF\u7528\u6A21\u578B\u67E5\u8BE2");
504
- models.command("list").description("\u5217\u51FA\u5F53\u524D Key \u53EF\u7528\u7684\u5168\u90E8\u6A21\u578B").option("--filter <keyword>", "\u6309 id \u5173\u952E\u5B57\u8FC7\u6EE4\uFF08\u4E0D\u533A\u5206\u5927\u5C0F\u5199\uFF09").action(async (opts, cmd) => {
586
+ models.command("resolve").description("\u4E3A\u521B\u4F5C\u4EFB\u52A1\u9009\u62E9\u5F53\u524D Key \u53EF\u7528\u7684\u9ED8\u8BA4\u6A21\u578B\uFF0C\u5E76\u8FD4\u56DE\u5B8C\u6574\u5B9E\u65F6\u5951\u7EA6").argument("<capability>", "\u521B\u4F5C\u80FD\u529B\uFF1Aimage | video").action(async (capability, _opts, cmd) => {
505
587
  const g = cmd.optsWithGlobals();
506
588
  const auth = resolveAuth(g);
507
- const res = await request({ baseUrl: auth.baseUrl, path: "/v1/models", apiKey: auth.apiKey });
508
- let list = res.data ?? [];
509
- if (opts.filter) {
510
- const kw = opts.filter.toLowerCase();
511
- list = list.filter((m) => m.id.toLowerCase().includes(kw));
512
- }
589
+ const resolved = await resolveCreativeModel(auth, parseCreativeCapability(capability));
513
590
  if (g.json) {
514
- printJson({ data: list });
515
- } else {
591
+ printJson(resolved);
592
+ return;
593
+ }
594
+ printTable(
595
+ ["\u5B57\u6BB5", "\u503C"],
596
+ [
597
+ ["\u80FD\u529B", resolved.capability],
598
+ ["\u6A21\u578B", resolved.model.id],
599
+ ["\u63D0\u4F9B\u65B9", resolved.model.owned_by ?? "-"],
600
+ ["\u7AEF\u70B9", resolved.endpoint_type],
601
+ ["\u4E0B\u4E00\u6B65", resolved.next_command]
602
+ ]
603
+ );
604
+ if (Array.isArray(resolved.model.supported_params) && resolved.model.supported_params.length > 0) {
605
+ process.stdout.write("\n\u5B9E\u65F6\u53C2\u6570\u5951\u7EA6\n");
516
606
  printTable(
517
- ["\u6A21\u578B ID", "\u63D0\u4F9B\u65B9", "\u652F\u6301\u7AEF\u70B9"],
518
- list.map((m) => [m.id, m.owned_by ?? "-", textValue(m.supported_endpoint_types)])
607
+ ["\u53C2\u6570", "\u7EA6\u675F"],
608
+ resolved.model.supported_params.map((parameter) => {
609
+ const item = parameter;
610
+ return [item.name, parameterConstraint(item)];
611
+ })
519
612
  );
520
613
  }
521
614
  });
522
- models.command("get").description("\u67E5\u770B\u5355\u4E2A\u6A21\u578B\u8BE6\u60C5").argument("<model>", "\u6A21\u578B ID").action(async (model, _opts, cmd) => {
615
+ models.command("list").description("\u5217\u51FA\u5F53\u524D Key \u53EF\u7528\u7684\u5168\u90E8\u6A21\u578B").option("--filter <keyword>", "\u6309 ID \u5173\u952E\u5B57\u8FC7\u6EE4\uFF08\u4E0D\u533A\u5206\u5927\u5C0F\u5199\uFF09").option("--endpoint <type>", "\u6309\u7AEF\u70B9\u7C7B\u578B\u8FC7\u6EE4\uFF0C\u5982 image-generation").action(async (opts, cmd) => {
616
+ const g = cmd.optsWithGlobals();
617
+ printModelList(filterModels(await fetchModels(g), opts.filter, opts.endpoint), Boolean(g.json));
618
+ });
619
+ models.command("search").description("\u6309\u5173\u952E\u5B57\u641C\u7D22\u5F53\u524D Key \u53EF\u7528\u7684\u6A21\u578B").argument("<query>", "\u6A21\u578B ID \u5173\u952E\u5B57").option("--endpoint <type>", "\u6309\u7AEF\u70B9\u7C7B\u578B\u8FC7\u6EE4\uFF0C\u5982 video-generation").action(async (query, opts, cmd) => {
620
+ const g = cmd.optsWithGlobals();
621
+ printModelList(filterModels(await fetchModels(g), query, opts.endpoint), Boolean(g.json));
622
+ });
623
+ models.command("get").description("\u67E5\u770B\u5355\u4E2A\u6A21\u578B\u8BE6\u60C5\u4E0E\u5B9E\u65F6\u53C2\u6570\u5951\u7EA6").argument("<model>", "\u6A21\u578B ID").action(async (model, _opts, cmd) => {
523
624
  const g = cmd.optsWithGlobals();
524
625
  const auth = resolveAuth(g);
525
- const res = await request({
626
+ const response = await request({
526
627
  baseUrl: auth.baseUrl,
527
628
  path: `/v1/models/${encodeURIComponent(model)}`,
528
629
  apiKey: auth.apiKey
529
630
  });
631
+ if (response.error) {
632
+ throw new ApiError(
633
+ "invalid_request",
634
+ response.error.message ?? `\u6A21\u578B ${model} \u4E0D\u53EF\u7528`,
635
+ { hint: "\u8FD0\u884C focalapi models list --json\uFF0C\u4F7F\u7528\u5217\u8868\u4E2D\u7684\u6A21\u578B ID\u3002" }
636
+ );
637
+ }
638
+ if (!response.id) {
639
+ throw new ApiError("bad_response", `\u6A21\u578B ${model} \u7684\u5951\u7EA6\u54CD\u5E94\u7F3A\u5C11 id`, { body: response });
640
+ }
530
641
  if (g.json) {
531
- printJson(res);
642
+ printJson(response);
532
643
  } else {
533
- printModelDetails(res);
644
+ printModelDetails(response);
534
645
  }
535
646
  });
536
647
  }
@@ -603,7 +714,7 @@ function extractText(content) {
603
714
  return "";
604
715
  }
605
716
  function registerChat(program) {
606
- program.command("chat").description("DeepSeek \u5907\u7528\u5BF9\u8BDD\u4E0E\u63D0\u793A\u8BCD\u8F85\u52A9\uFF08/v1/chat/completions\uFF09").argument("[prompt...]", "\u63D0\u793A\u8BCD\uFF1B\u7701\u7565\u4E14 stdin \u4E3A\u7BA1\u9053\u65F6\u4ECE stdin \u8BFB\u53D6").option("-m, --model <model>", "\u6A21\u578B ID\uFF08\u6216\u8BBE FOCALAPI_MODEL\uFF09").option("--system <text>", "system \u63D0\u793A\u8BCD").option("--input <file...>", "\u8F93\u5165\u6587\u4EF6\uFF08\u56FE\u7247\u8F6C data URL\uFF0C\u5982 --input @photo.jpg\uFF1B@ \u524D\u7F00\u53EF\u9009\uFF09").option("--max-tokens <n>", "max_tokens", (v) => Number.parseInt(v, 10)).option("--stream", "\u5F3A\u5236\u6D41\u5F0F\u8F93\u51FA").option("--no-stream", "\u5F3A\u5236\u975E\u6D41\u5F0F").action(
717
+ program.command("chat").description("\u8865\u5145\u6587\u672C\u8F85\u52A9\uFF08\u6A21\u578B\u5FC5\u987B\u6765\u81EA\u5F53\u524D Key \u7684\u5B9E\u65F6\u5217\u8868\uFF09").argument("[prompt...]", "\u63D0\u793A\u8BCD\uFF1B\u7701\u7565\u4E14 stdin \u4E3A\u7BA1\u9053\u65F6\u4ECE stdin \u8BFB\u53D6").option("-m, --model <model>", "\u6A21\u578B ID\uFF08\u6216\u8BBE FOCALAPI_MODEL\uFF09").option("--system <text>", "system \u63D0\u793A\u8BCD").option("--input <file...>", "\u8F93\u5165\u6587\u4EF6\uFF08\u56FE\u7247\u8F6C data URL\uFF0C\u5982 --input @photo.jpg\uFF1B@ \u524D\u7F00\u53EF\u9009\uFF09").option("--max-tokens <n>", "max_tokens", (v) => Number.parseInt(v, 10)).option("--stream", "\u5F3A\u5236\u6D41\u5F0F\u8F93\u51FA").option("--no-stream", "\u5F3A\u5236\u975E\u6D41\u5F0F").action(
607
718
  async (promptParts, opts, cmd) => {
608
719
  const g = cmd.optsWithGlobals();
609
720
  const auth = resolveAuth(g);
@@ -777,6 +888,24 @@ import { pipeline as pipeline2 } from "stream/promises";
777
888
  import { Readable as Readable2 } from "stream";
778
889
 
779
890
  // src/lib/model-capabilities.ts
891
+ var SEEDREAM_OUTPUT_FORMATS = ["png", "jpeg"];
892
+ var SEEDREAM_OPTIMIZE_PROMPT_MODES = ["auto", "enabled", "disabled"];
893
+ var GROK_IMAGE_ASPECT_RATIOS = [
894
+ "auto",
895
+ "1:1",
896
+ "2:3",
897
+ "3:2",
898
+ "3:4",
899
+ "4:3",
900
+ "9:16",
901
+ "16:9",
902
+ "9:19.5",
903
+ "19.5:9",
904
+ "1:2",
905
+ "2:1",
906
+ "1:3",
907
+ "3:1"
908
+ ];
780
909
  var IMAGE_CONSTRAINTS = {
781
910
  "gpt-image-2": {
782
911
  defaultSize: "1024x1024",
@@ -791,59 +920,148 @@ var IMAGE_CONSTRAINTS = {
791
920
  qualities: ["low", "medium", "high"],
792
921
  backgrounds: ["auto", "opaque"]
793
922
  },
794
- "doubao-seedream-4-5-251128": {
795
- defaultSize: "2048x2048",
923
+ "seedream-4-0-250828": {
924
+ defaultSize: "1k",
925
+ sizeTiers: ["1k", "2k", "4k"],
926
+ maxN: 10,
927
+ maxReferenceImages: 10,
928
+ maxTotalImages: 15,
929
+ minMegapixels: 0.92,
930
+ maxMegapixels: 16.777216,
931
+ supportsWatermark: true,
932
+ outputFormats: SEEDREAM_OUTPUT_FORMATS,
933
+ optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
934
+ },
935
+ "seedream-4-5-251128": {
936
+ defaultSize: "2k",
937
+ sizeTiers: ["2k", "4k"],
796
938
  maxN: 10,
797
939
  maxReferenceImages: 10,
798
940
  maxTotalImages: 15,
799
941
  minMegapixels: 3.6864,
800
- maxMegapixels: 16.777216
942
+ maxMegapixels: 16.777216,
943
+ supportsWatermark: true,
944
+ outputFormats: SEEDREAM_OUTPUT_FORMATS,
945
+ optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
801
946
  },
802
- "doubao-seedream-5-0-pro-260628": {
803
- defaultSize: "1024x1024",
947
+ "dola-seedream-5-0-pro-260628": {
948
+ defaultSize: "1k",
949
+ sizeTiers: ["1k", "1.5k", "2k"],
804
950
  maxN: 1,
805
951
  maxReferenceImages: 10,
806
952
  minMegapixels: 0.92,
807
- maxMegapixels: 4.194304
953
+ maxMegapixels: 4.194304,
954
+ supportsWatermark: true,
955
+ outputFormats: SEEDREAM_OUTPUT_FORMATS,
956
+ optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
808
957
  },
809
- "doubao-seedream-5-0-lite-260128": {
810
- defaultSize: "2048x2048",
958
+ "seedream-5-0-260128": {
959
+ defaultSize: "2k",
960
+ sizeTiers: ["2k", "3k", "4k"],
811
961
  maxN: 14,
812
962
  maxReferenceImages: 14,
813
963
  maxTotalImages: 15,
814
964
  minMegapixels: 3.6864,
815
- maxMegapixels: 16.777216
965
+ maxMegapixels: 16.777216,
966
+ supportsWatermark: true,
967
+ outputFormats: SEEDREAM_OUTPUT_FORMATS,
968
+ optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
969
+ },
970
+ "grok-imagine-image-quality": {
971
+ defaultSize: "1024x1024",
972
+ maxN: 10,
973
+ maxReferenceImages: 3,
974
+ aspectRatios: GROK_IMAGE_ASPECT_RATIOS,
975
+ resolutions: ["1k", "2k"],
976
+ supportsSeed: true
816
977
  },
817
- "grok-imagine-image-quality": { defaultSize: "1024x1024", maxN: 10, maxReferenceImages: 3 },
818
- "grok-imagine-image": { defaultSize: "1024x1024", maxN: 10, maxReferenceImages: 3 },
819
- "grok-imagine-image-pro": { defaultSize: "1024x1024", maxN: 10, maxReferenceImages: 1 }
978
+ "grok-imagine-image": {
979
+ defaultSize: "1024x1024",
980
+ maxN: 10,
981
+ maxReferenceImages: 3,
982
+ aspectRatios: GROK_IMAGE_ASPECT_RATIOS,
983
+ resolutions: ["1k", "2k"],
984
+ supportsSeed: true
985
+ }
820
986
  };
821
987
  var SEEDANCE_RATIOS = ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"];
988
+ var GROK_VIDEO_ASPECT_RATIOS = ["auto", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"];
822
989
  var VIDEO_CONSTRAINTS = {
823
- "doubao-seedance-2-0-260128": {
990
+ "veo-3.1-generate-preview": {
991
+ resolutions: ["720p", "1080p", "4k"],
992
+ ratios: ["16:9", "9:16"],
993
+ minSeconds: 4,
994
+ maxSeconds: 8,
995
+ allowedSeconds: [4, 6, 8],
996
+ requiredSecondsByResolution: { "1080p": 8, "4k": 8 },
997
+ supportsSeed: true
998
+ },
999
+ "veo-3.1-fast-generate-preview": {
1000
+ resolutions: ["720p", "1080p", "4k"],
1001
+ ratios: ["16:9", "9:16"],
1002
+ minSeconds: 4,
1003
+ maxSeconds: 8,
1004
+ allowedSeconds: [4, 6, 8],
1005
+ requiredSecondsByResolution: { "1080p": 8, "4k": 8 },
1006
+ supportsSeed: true
1007
+ },
1008
+ "veo-3.1-lite-generate-preview": {
1009
+ resolutions: ["720p", "1080p"],
1010
+ ratios: ["16:9", "9:16"],
1011
+ minSeconds: 4,
1012
+ maxSeconds: 8,
1013
+ allowedSeconds: [4, 6, 8],
1014
+ requiredSecondsByResolution: { "1080p": 8 },
1015
+ supportsSeed: true
1016
+ },
1017
+ "dreamina-seedance-2-0-260128": {
824
1018
  resolutions: ["480p", "720p", "1080p", "4k"],
825
1019
  ratios: SEEDANCE_RATIOS,
826
1020
  minSeconds: 4,
827
- maxSeconds: 15
1021
+ maxSeconds: 15,
1022
+ supportsPriority: true
1023
+ },
1024
+ "dreamina-seedance-2-0-fast-260128": {
1025
+ resolutions: ["480p", "720p"],
1026
+ ratios: SEEDANCE_RATIOS,
1027
+ minSeconds: 4,
1028
+ maxSeconds: 15,
1029
+ supportsPriority: true
828
1030
  },
829
- "doubao-seedance-2-0-fast-260128": {
1031
+ "dreamina-seedance-2-0-mini-260615": {
830
1032
  resolutions: ["480p", "720p"],
831
1033
  ratios: SEEDANCE_RATIOS,
832
1034
  minSeconds: 4,
833
- maxSeconds: 15
1035
+ maxSeconds: 15,
1036
+ supportsPriority: true
834
1037
  },
835
- "doubao-seedance-2-0-mini-260615": {
1038
+ "dreamina-seedance-2-5-260628": {
836
1039
  resolutions: ["480p", "720p"],
837
1040
  ratios: SEEDANCE_RATIOS,
838
1041
  minSeconds: 4,
839
- maxSeconds: 15
1042
+ maxSeconds: 30
1043
+ },
1044
+ "grok-imagine-video": {
1045
+ resolutions: ["480p", "720p", "1080p"],
1046
+ aspectRatios: GROK_VIDEO_ASPECT_RATIOS,
1047
+ minSeconds: 1,
1048
+ maxSeconds: 15,
1049
+ supportsSeed: true
1050
+ },
1051
+ "grok-imagine-video-1.5": {
1052
+ resolutions: ["480p", "720p", "1080p"],
1053
+ aspectRatios: GROK_VIDEO_ASPECT_RATIOS,
1054
+ minSeconds: 1,
1055
+ maxSeconds: 15,
1056
+ supportsSeed: true
840
1057
  }
841
1058
  };
842
1059
  var COMMON_GEMINI_RATIOS = ["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
843
1060
  var GEMINI_IMAGE_CONSTRAINTS = {
844
- "gemini-3-pro-image-preview": { aspectRatios: COMMON_GEMINI_RATIOS, imageSizes: ["1K", "2K", "4K"], supportsSampling: false },
845
- "gemini-3.1-flash-image-preview": { aspectRatios: COMMON_GEMINI_RATIOS, imageSizes: ["1K", "2K", "4K"], supportsSampling: false },
846
- "gemini-3.1-flash-lite-image-preview": {
1061
+ "gemini-2.5-flash-image": { aspectRatios: COMMON_GEMINI_RATIOS, supportsSampling: false },
1062
+ "gemini-3-pro-image": { aspectRatios: COMMON_GEMINI_RATIOS, imageSizes: ["1K", "2K", "4K"], supportsSampling: false },
1063
+ "gemini-3.1-flash-image": { aspectRatios: COMMON_GEMINI_RATIOS, imageSizes: ["1K", "2K", "4K"], supportsSampling: false },
1064
+ "gemini-3.1-flash-lite-image": {
847
1065
  aspectRatios: [...COMMON_GEMINI_RATIOS, "1:4", "4:1", "1:8", "8:1"],
848
1066
  imageSizes: ["1K"],
849
1067
  supportsSampling: true
@@ -852,7 +1070,7 @@ var GEMINI_IMAGE_CONSTRAINTS = {
852
1070
  function parseSize(size, model) {
853
1071
  const match = /^(\d+)x(\d+)$/i.exec(size.trim());
854
1072
  if (!match) {
855
- throw new ApiError("invalid_request", `${model} size must be WIDTHxHEIGHT (received: ${size})`);
1073
+ throw new ApiError("invalid_request", `${model} size must be a supported tier or WIDTHxHEIGHT (received: ${size})`);
856
1074
  }
857
1075
  return { width: Number(match[1]), height: Number(match[2]) };
858
1076
  }
@@ -862,6 +1080,15 @@ function megapixels(width, height) {
862
1080
  function formatMegapixels(value) {
863
1081
  return value.toFixed(2).replace(/\.00$/, "");
864
1082
  }
1083
+ function validateOptionalChoice(model, name, value, supported) {
1084
+ if (!value) return;
1085
+ if (!supported) {
1086
+ throw new ApiError("invalid_request", `${model} does not support ${name}`);
1087
+ }
1088
+ if (!supported.includes(value.toLowerCase())) {
1089
+ throw new ApiError("invalid_request", `${model} ${name} must be one of ${supported.join(", ")} (received: ${value})`);
1090
+ }
1091
+ }
865
1092
  function validateImageGeneration(model, input) {
866
1093
  if (input.responseFormat && input.responseFormat !== "url" && input.responseFormat !== "b64_json") {
867
1094
  throw new ApiError("invalid_request", `response_format must be url or b64_json (received: ${input.responseFormat})`);
@@ -880,13 +1107,29 @@ function validateImageGeneration(model, input) {
880
1107
  if (input.hasMask && model === "gpt-image-2" && input.imageCount !== 1) {
881
1108
  throw new ApiError("invalid_request", "gpt-image-2 mask requires exactly one reference image");
882
1109
  }
883
- if (input.quality && constraint.qualities && !constraint.qualities.includes(input.quality.toLowerCase())) {
884
- throw new ApiError("invalid_request", `${model} quality must be one of ${constraint.qualities.join(", ")} (received: ${input.quality})`);
1110
+ validateOptionalChoice(model, "quality", input.quality, constraint.qualities);
1111
+ validateOptionalChoice(model, "background", input.background, constraint.backgrounds);
1112
+ validateOptionalChoice(model, "aspect_ratio", input.aspectRatio, constraint.aspectRatios);
1113
+ validateOptionalChoice(model, "resolution", input.resolution, constraint.resolutions);
1114
+ validateOptionalChoice(model, "output_format", input.outputFormat, constraint.outputFormats);
1115
+ validateOptionalChoice(model, "optimize_prompt", input.optimizePrompt, constraint.optimizePromptModes);
1116
+ if (input.seed !== void 0) {
1117
+ if (!constraint.supportsSeed) {
1118
+ throw new ApiError("invalid_request", `${model} does not support seed`);
1119
+ }
1120
+ if (!Number.isInteger(input.seed) || input.seed < 0) {
1121
+ throw new ApiError("invalid_request", "seed must be a non-negative integer");
1122
+ }
1123
+ }
1124
+ if (input.watermark !== void 0 && !constraint.supportsWatermark) {
1125
+ throw new ApiError("invalid_request", `${model} does not support watermark`);
885
1126
  }
886
- if (input.background && constraint.backgrounds && !constraint.backgrounds.includes(input.background.toLowerCase())) {
887
- throw new ApiError("invalid_request", `${model} background must be one of ${constraint.backgrounds.join(", ")} (received: ${input.background})`);
1127
+ const suppliedSize = input.size ?? constraint.defaultSize;
1128
+ if (constraint.sizeTiers?.includes(suppliedSize.toLowerCase())) return;
1129
+ if (constraint.sizeTiers && !/^\d+x\d+$/i.test(suppliedSize)) {
1130
+ throw new ApiError("invalid_request", `${model} size must be one of ${constraint.sizeTiers.join(", ")} or WIDTHxHEIGHT (received: ${suppliedSize})`);
888
1131
  }
889
- const { width, height } = parseSize(input.size ?? constraint.defaultSize, model);
1132
+ const { width, height } = parseSize(suppliedSize, model);
890
1133
  const pixels = megapixels(width, height);
891
1134
  if (constraint.minEdge && (width < constraint.minEdge || height < constraint.minEdge) || constraint.maxEdge && (width > constraint.maxEdge || height > constraint.maxEdge) || constraint.edgeMultiple && (width % constraint.edgeMultiple !== 0 || height % constraint.edgeMultiple !== 0) || constraint.minMegapixels && pixels < constraint.minMegapixels || constraint.maxMegapixels && pixels > constraint.maxMegapixels) {
892
1135
  const edge = constraint.minEdge && constraint.maxEdge ? `${constraint.minEdge}-${constraint.maxEdge}px per edge` : "";
@@ -909,14 +1152,15 @@ function validateGeminiImageGeneration(model, input) {
909
1152
  if (input.aspectRatio && !constraint.aspectRatios.includes(input.aspectRatio)) {
910
1153
  throw new ApiError("invalid_request", `${model} aspectRatio must be one of ${constraint.aspectRatios.join(", ")} (received: ${input.aspectRatio})`);
911
1154
  }
912
- if (input.imageSize && !constraint.imageSizes.includes(input.imageSize.toUpperCase())) {
913
- throw new ApiError("invalid_request", `${model} imageSize must be one of ${constraint.imageSizes.join(", ")} (received: ${input.imageSize})`);
1155
+ if (input.imageSize && !constraint.imageSizes?.includes(input.imageSize.toUpperCase())) {
1156
+ const supported = constraint.imageSizes?.join(", ") ?? "none";
1157
+ throw new ApiError("invalid_request", `${model} imageSize must be one of ${supported} (received: ${input.imageSize})`);
914
1158
  }
915
1159
  if (input.seed !== void 0 && (!Number.isInteger(input.seed) || input.seed < 0)) {
916
1160
  throw new ApiError("invalid_request", "seed must be a non-negative integer");
917
1161
  }
918
1162
  if (!constraint.supportsSampling && (input.thinkingLevel || input.temperature !== void 0 || input.topP !== void 0)) {
919
- throw new ApiError("invalid_request", `${model} supports thinkingLevel, temperature, and topP only on gemini-3.1-flash-lite-image-preview`);
1163
+ throw new ApiError("invalid_request", `${model} supports thinkingLevel, temperature, and topP only on gemini-3.1-flash-lite-image`);
920
1164
  }
921
1165
  if (input.thinkingLevel && !["MINIMAL", "HIGH"].includes(input.thinkingLevel.toUpperCase())) {
922
1166
  throw new ApiError("invalid_request", "thinkingLevel must be MINIMAL or HIGH");
@@ -934,14 +1178,44 @@ function validateVideoGeneration(model, input) {
934
1178
  if (input.seconds !== void 0 && (input.seconds < constraint.minSeconds || input.seconds > constraint.maxSeconds)) {
935
1179
  throw new ApiError("invalid_request", `${model} seconds must be ${constraint.minSeconds}-${constraint.maxSeconds} (received: ${input.seconds})`);
936
1180
  }
1181
+ if (input.seconds !== void 0 && constraint.allowedSeconds && !constraint.allowedSeconds.includes(input.seconds)) {
1182
+ throw new ApiError("invalid_request", `${model} seconds must be one of ${constraint.allowedSeconds.join(", ")} (received: ${input.seconds})`);
1183
+ }
937
1184
  if (input.resolution && !constraint.resolutions.includes(input.resolution.toLowerCase())) {
938
1185
  throw new ApiError("invalid_request", `${model} resolution must be one of ${constraint.resolutions.join(", ")} (received: ${input.resolution})`);
939
1186
  }
940
- if (input.ratio && !constraint.ratios.includes(input.ratio)) {
941
- throw new ApiError("invalid_request", `${model} ratio must be one of ${constraint.ratios.join(", ")} (received: ${input.ratio})`);
1187
+ if (input.seconds !== void 0 && input.resolution) {
1188
+ const required = constraint.requiredSecondsByResolution?.[input.resolution.toLowerCase()];
1189
+ if (required !== void 0 && input.seconds !== required) {
1190
+ throw new ApiError("invalid_request", `${model} resolution ${input.resolution} requires ${required} seconds`);
1191
+ }
1192
+ }
1193
+ if (input.ratio) {
1194
+ if (!constraint.ratios) {
1195
+ throw new ApiError("invalid_request", `${model} uses --aspect-ratio instead of --ratio`);
1196
+ }
1197
+ if (!constraint.ratios.includes(input.ratio)) {
1198
+ throw new ApiError("invalid_request", `${model} ratio must be one of ${constraint.ratios.join(", ")} (received: ${input.ratio})`);
1199
+ }
1200
+ }
1201
+ if (input.aspectRatio) {
1202
+ if (!constraint.aspectRatios) {
1203
+ throw new ApiError("invalid_request", `${model} uses --ratio instead of --aspect-ratio`);
1204
+ }
1205
+ if (!constraint.aspectRatios.includes(input.aspectRatio)) {
1206
+ throw new ApiError("invalid_request", `${model} aspect_ratio must be one of ${constraint.aspectRatios.join(", ")} (received: ${input.aspectRatio})`);
1207
+ }
1208
+ }
1209
+ if (input.seed !== void 0) {
1210
+ if (!constraint.supportsSeed) {
1211
+ throw new ApiError("invalid_request", `${model} does not support seed`);
1212
+ }
1213
+ if (!Number.isInteger(input.seed) || input.seed < 0) {
1214
+ throw new ApiError("invalid_request", "seed must be a non-negative integer");
1215
+ }
942
1216
  }
943
- if (input.priority !== void 0 && model !== "doubao-seedance-2-0-260128") {
944
- throw new ApiError("invalid_request", `${model} does not support priority; only doubao-seedance-2-0-260128 does`);
1217
+ if (input.priority !== void 0 && !constraint.supportsPriority) {
1218
+ throw new ApiError("invalid_request", `${model} does not support priority`);
945
1219
  }
946
1220
  if (input.priority !== void 0 && (input.priority < 0 || input.priority > 9)) {
947
1221
  throw new ApiError("invalid_request", `${model} priority must be 0-9 (received: ${input.priority})`);
@@ -1153,6 +1427,14 @@ function parseJsonArray(raw, option) {
1153
1427
  throw new ApiError("invalid_request", `--${option} must be a JSON array`);
1154
1428
  }
1155
1429
  }
1430
+ function parseGeminiResponseModalities(raw) {
1431
+ const modalities = raw.split(",").map((value) => value.trim().toUpperCase()).filter(Boolean);
1432
+ const unique = new Set(modalities);
1433
+ if (unique.size !== modalities.length || !unique.has("IMAGE") || [...unique].some((value) => value !== "IMAGE" && value !== "TEXT")) {
1434
+ throw new ApiError("invalid_request", "--response-modalities must be IMAGE or IMAGE,TEXT");
1435
+ }
1436
+ return unique.has("TEXT") ? ["IMAGE", "TEXT"] : ["IMAGE"];
1437
+ }
1156
1438
  function geminiImagePart(source) {
1157
1439
  const dataUri = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(source.trim());
1158
1440
  if (dataUri) {
@@ -1161,6 +1443,24 @@ function geminiImagePart(source) {
1161
1443
  }
1162
1444
  return { fileData: { fileUri: source } };
1163
1445
  }
1446
+ function geminiOmniImageInput(source) {
1447
+ const dataUri = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(source.trim());
1448
+ if (!dataUri) {
1449
+ throw new ApiError("invalid_request", "--image for Gemini Omni must be a base64 data URI");
1450
+ }
1451
+ const [, mimeType = "", data = ""] = dataUri;
1452
+ return { type: "image", mime_type: mimeType, data: data.replace(/[\r\n]/g, "") };
1453
+ }
1454
+ function extractGeminiOmniVideo(response) {
1455
+ for (const step of response.steps ?? []) {
1456
+ for (const content of step.content ?? []) {
1457
+ if (content.type === "video" && content.data) {
1458
+ return { data: content.data, mimeType: content.mime_type };
1459
+ }
1460
+ }
1461
+ }
1462
+ return void 0;
1463
+ }
1164
1464
  function extractGeminiImageItems(response) {
1165
1465
  return (response.candidates ?? []).flatMap(
1166
1466
  (candidate) => (candidate.content?.parts ?? []).flatMap(
@@ -1170,15 +1470,23 @@ function extractGeminiImageItems(response) {
1170
1470
  }
1171
1471
  function registerGen(program) {
1172
1472
  const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
1173
- gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\u9ED8\u8BA4\u540C\u6B65\u8FD4\u56DE\u5E76\u4E0B\u8F7D\uFF1B--no-wait \u8FD4\u56DE\u53EF\u8F6E\u8BE2\u7684\u4EFB\u52A1 ID\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "\u56FE\u50CF\u6A21\u578B ID\uFF08focalapi models list \u67E5\u770B\uFF09").option("--size <size>", "\u5C3A\u5BF8\uFF0C\u5982 1024x1024").option("--quality <quality>", "\u56FE\u50CF\u8D28\u91CF\u6863\u4F4D\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--background <background>", "\u80CC\u666F\u6A21\u5F0F\uFF08\u4EC5 gpt-image-2 \u652F\u6301 auto/opaque\uFF09").option("--image <url...>", "\u53C2\u8003\u56FE\u6216\u7F16\u8F91\u56FE URL\uFF0C\u53EF\u591A\u4E2A").option("--mask <url>", "\u7F16\u8F91 mask URL\uFF08gpt-image-2 \u9700\u8981\u5355\u5F20\u53C2\u8003\u56FE\uFF09").option("--response-format <format>", "\u56FE\u50CF\u54CD\u5E94\u683C\u5F0F\uFF1Aurl \u6216 b64_json").option("--n <count>", "\u5F20\u6570\uFF081\u2013128\uFF09", (v) => Number.parseInt(v, 10), 1).option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u56FE\u50CF\u751F\u6210\u5B8C\u6210").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
1473
+ gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u56FE\u50CF\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--size <size>", "\u5C3A\u5BF8\uFF0C\u5982 1024x1024").option("--aspect-ratio <ratio>", "\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF08\u4EC5 Grok \u56FE\u50CF\u6A21\u578B\uFF09\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF08\u4EC5 Grok \u56FE\u50CF\u6A21\u578B\uFF09\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u968F\u673A\u79CD\u5B50\uFF08\u4EC5 Grok \u56FE\u50CF\u6A21\u578B\uFF0C\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--quality <quality>", "\u56FE\u50CF\u8D28\u91CF\u6863\u4F4D\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--background <background>", "\u80CC\u666F\u6A21\u5F0F\uFF08\u4EC5 gpt-image-2 \u652F\u6301 auto/opaque\uFF09").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5 Seedream\uFF0Ctrue \u6216 false\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--output-format <format>", "\u8F93\u51FA\u683C\u5F0F\uFF08\u4EC5 Seedream\uFF1Apng \u6216 jpeg\uFF09").option("--optimize-prompt <mode>", "\u63D0\u793A\u8BCD\u4F18\u5316\uFF08\u4EC5 Seedream\uFF1Aauto\u3001enabled \u6216 disabled\uFF09").option("--image <url...>", "\u53C2\u8003\u56FE\u6216\u7F16\u8F91\u56FE URL\uFF0C\u53EF\u591A\u4E2A").option("--mask <url>", "\u7F16\u8F91 mask URL\uFF08gpt-image-2 \u9700\u8981\u5355\u5F20\u53C2\u8003\u56FE\uFF09").option("--response-format <format>", "\u56FE\u50CF\u54CD\u5E94\u683C\u5F0F\uFF1Aurl \u6216 b64_json").option("--n <count>", "\u5F20\u6570\uFF081\u2013128\uFF09", (v) => Number.parseInt(v, 10), 1).option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u56FE\u50CF\u751F\u6210\u5B8C\u6210").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
1174
1474
  const g = cmd.optsWithGlobals();
1175
1475
  const auth = resolveAuth(g);
1476
+ const model = opts.model ?? (await resolveCreativeModel(auth, "image")).model.id;
1477
+ if (!opts.model && !g.json) info(`\u5DF2\u81EA\u52A8\u9009\u62E9\u56FE\u50CF\u6A21\u578B\uFF1A${model}`);
1176
1478
  const n = clampInt(opts.n, 1, MAX_IMAGE_N, "n");
1177
- validateImageGeneration(opts.model, {
1479
+ validateImageGeneration(model, {
1178
1480
  n,
1179
1481
  size: opts.size,
1180
1482
  quality: opts.quality,
1181
1483
  background: opts.background,
1484
+ aspectRatio: opts.aspectRatio,
1485
+ resolution: opts.resolution,
1486
+ seed: opts.seed,
1487
+ watermark: opts.watermark,
1488
+ outputFormat: opts.outputFormat,
1489
+ optimizePrompt: opts.optimizePrompt,
1182
1490
  responseFormat: opts.responseFormat,
1183
1491
  imageCount: opts.image?.length,
1184
1492
  hasMask: Boolean(opts.mask)
@@ -1186,10 +1494,16 @@ function registerGen(program) {
1186
1494
  if (opts.wait === false && opts.responseFormat === "b64_json") {
1187
1495
  throw new ApiError("invalid_request", "--response-format b64_json cannot be used with --no-wait; use url");
1188
1496
  }
1189
- const body = { model: opts.model, prompt: promptParts.join(" "), n };
1497
+ const body = { model, prompt: promptParts.join(" "), n };
1190
1498
  if (opts.size) body.size = opts.size;
1499
+ if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
1500
+ if (opts.resolution) body.resolution = opts.resolution.toLowerCase();
1501
+ if (opts.seed !== void 0) body.seed = opts.seed;
1191
1502
  if (opts.quality) body.quality = opts.quality;
1192
1503
  if (opts.background) body.background = opts.background;
1504
+ if (opts.watermark !== void 0) body.watermark = opts.watermark;
1505
+ if (opts.outputFormat) body.output_format = opts.outputFormat.toLowerCase();
1506
+ if (opts.optimizePrompt) body.optimize_prompt_options = { thinking: opts.optimizePrompt.toLowerCase() };
1193
1507
  if (opts.image) body.image = opts.image;
1194
1508
  if (opts.mask) body.mask = opts.mask;
1195
1509
  if (opts.responseFormat) body.response_format = opts.responseFormat;
@@ -1207,7 +1521,7 @@ function registerGen(program) {
1207
1521
  throw new ApiError("bad_response", "\u5F02\u6B65\u56FE\u50CF\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: res });
1208
1522
  }
1209
1523
  if (g.json) {
1210
- printJson({ task_id: taskId, status: res.status ?? "queued", submitted: true });
1524
+ printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, next_command: `focalapi task status ${taskId} --json` });
1211
1525
  } else {
1212
1526
  process.stdout.write(taskId + "\n");
1213
1527
  info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u67E5\u8BE2\uFF1Afocalapi task status ${taskId}`);
@@ -1226,12 +1540,12 @@ function registerGen(program) {
1226
1540
  files.push(await saveImageItem(item, dir, `image-${ts}-${i + 1}`, auth.apiKey));
1227
1541
  }
1228
1542
  if (g.json) {
1229
- printJson({ files, count: files.length });
1543
+ printJson({ model, files, count: files.length });
1230
1544
  } else {
1231
1545
  for (const f of files) info(`\u2713 ${f}`);
1232
1546
  }
1233
1547
  });
1234
- gen.command("gemini-image").description("\u4F7F\u7528 Gemini \u539F\u751F generateContent \u63A5\u53E3\u751F\u6210\u56FE\u50CF").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "Gemini \u56FE\u50CF\u6A21\u578B ID\uFF1B\u5148\u7528 focalapi models get \u786E\u8BA4").option("--aspect-ratio <ratio>", "\u753B\u9762\u6BD4\u4F8B\uFF0C\u4F8B\u5982 1:1\u300116:9\u3001auto").option("--image-size <size>", "\u8F93\u51FA\u5C3A\u5BF8\uFF0C\u4F8B\u5982 1K\u30012K\u30014K").option("--config <json>", "\u9644\u52A0 Gemini generationConfig JSON\uFF1B\u547D\u4EE4\u56FA\u5B9A responseFormat.image \u548C\u5355\u5019\u9009").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--image <url...>", "Gemini reference image URL or data URI; repeatable").option("--system <text>", "Gemini systemInstruction text").option("--seed <n>", "Non-negative Gemini generation seed", (v) => Number.parseInt(v, 10)).option("--thinking-level <level>", "Nano Banana 2 Lite: MINIMAL or HIGH").option("--temperature <n>", "Nano Banana 2 Lite: 0 through 2", (v) => Number.parseFloat(v)).option("--top-p <n>", "Nano Banana 2 Lite: 0 through 1", (v) => Number.parseFloat(v)).action(async (promptParts, opts, cmd) => {
1548
+ gen.command("gemini-image").description("\u4F7F\u7528 Gemini \u539F\u751F generateContent \u63A5\u53E3\u751F\u6210\u56FE\u50CF").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "Gemini \u56FE\u50CF\u6A21\u578B ID\uFF1B\u5148\u7528 focalapi models get \u786E\u8BA4").option("--aspect-ratio <ratio>", "\u753B\u9762\u6BD4\u4F8B\uFF0C\u4F8B\u5982 1:1\u300116:9\u3001auto").option("--image-size <size>", "\u8F93\u51FA\u5C3A\u5BF8\uFF0C\u4F8B\u5982 1K\u30012K\u30014K").option("--response-modalities <modalities>", "\u8F93\u51FA\u7C7B\u578B\uFF1AIMAGE \u6216 IMAGE,TEXT\uFF1B\u672A\u4F20\u65F6\u7531\u670D\u52A1\u7AEF\u9ED8\u8BA4 IMAGE,TEXT").option("--config <json>", "\u9644\u52A0 Gemini generationConfig JSON\uFF1B\u547D\u4EE4\u56FA\u5B9A responseFormat.image \u548C\u5355\u5019\u9009").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--image <url...>", "Gemini reference image URL or data URI; repeatable").option("--system <text>", "Gemini systemInstruction text").option("--seed <n>", "Non-negative Gemini generation seed", (v) => Number.parseInt(v, 10)).option("--thinking-level <level>", "Nano Banana 2 Lite: MINIMAL or HIGH").option("--temperature <n>", "Nano Banana 2 Lite: 0 through 2", (v) => Number.parseFloat(v)).option("--top-p <n>", "Nano Banana 2 Lite: 0 through 1", (v) => Number.parseFloat(v)).action(async (promptParts, opts, cmd) => {
1235
1549
  const g = cmd.optsWithGlobals();
1236
1550
  const auth = resolveAuth(g);
1237
1551
  validateGeminiImageGeneration(opts.model, {
@@ -1254,6 +1568,7 @@ function registerGen(program) {
1254
1568
  ...suppliedConfig,
1255
1569
  candidateCount: 1,
1256
1570
  responseFormat: { image: imageConfig },
1571
+ ...opts.responseModalities ? { responseModalities: parseGeminiResponseModalities(opts.responseModalities) } : {},
1257
1572
  ...opts.seed !== void 0 ? { seed: opts.seed } : {},
1258
1573
  ...opts.thinkingLevel ? { thinkingConfig: { ...suppliedConfig.thinkingConfig ?? {}, thinkingLevel: opts.thinkingLevel.toUpperCase() } } : {},
1259
1574
  ...opts.temperature !== void 0 ? { temperature: opts.temperature } : {},
@@ -1287,11 +1602,55 @@ function registerGen(program) {
1287
1602
  for (const file of files) info(`\u2713 ${file}`);
1288
1603
  }
1289
1604
  });
1290
- gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u4EFB\u52A1\u5236\uFF1A\u9ED8\u8BA4\u8F6E\u8BE2\u81F3\u5B8C\u6210\u5E76\u4E0B\u8F7D\uFF1B--no-wait \u53EA\u53D6 task_id\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF08focalapi models list \u67E5\u770B\uFF09").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF081\u20133600\uFF09", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--image <url...>", "\u56FE\u751F\u89C6\u9891\u7684\u6E90\u56FE\u50CF URL\uFF0C\u53EF\u591A\u4E2A").option("--generate-audio <boolean>", "\u662F\u5426\u751F\u6210\u97F3\u9891\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "generate-audio")).option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--service-tier <tier>", "\u670D\u52A1\u5C42\u7EA7\uFF08Seedance 2.0 \u9ED8\u8BA4 default\uFF09").option("--priority <n>", "\u4EFB\u52A1\u4F18\u5148\u7EA7\uFF08\u4EC5 Seedance 2.0 \u6807\u51C6\u7248\uFF09", (v) => Number.parseInt(v, 10)).option("--callback-url <url>", "\u4EFB\u52A1\u5B8C\u6210\u56DE\u8C03 URL").option("--return-last-frame <boolean>", "\u662F\u5426\u8FD4\u56DE\u6700\u540E\u4E00\u5E27\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "return-last-frame")).option("--execution-expires-after <seconds>", "\u4EFB\u52A1\u8FC7\u671F\u79D2\u6570\uFF083600\u2013259200\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-identifier <identifier>", "Seedance \u5B89\u5168\u6807\u8BC6\u7B26\uFF081\u201364 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09").option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u5B8C\u6210").option("--poll-interval <ms>", "\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--timeout <minutes>", "\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--content <json>", "Ark-compatible content JSON array; overrides prompt/image facade fields").action(
1605
+ gen.command("omni-video").description("\u4F7F\u7528 Gemini Omni Flash \u539F\u751F Interactions API \u751F\u6210\u6216\u7F16\u8F91\u89C6\u9891").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("--image <data-uri...>", "\u56FE\u751F\u89C6\u9891\u53C2\u8003\u56FE\uFF1B\u4EC5\u652F\u6301 base64 data URI\uFF0C\u53EF\u591A\u4E2A").option("--previous-interaction-id <id>", "\u4E0A\u4E00\u6B21\u4EA4\u4E92 ID\uFF0C\u7528\u4E8E\u8FDE\u7EED\u89C6\u9891\u7F16\u8F91").option("--aspect-ratio <ratio>", "\u753B\u9762\u6BD4\u4F8B\uFF1A16:9 \u6216 9:16").option("--task <task>", "\u89C6\u9891\u4EFB\u52A1\uFF1Atext_to_video\u3001image_to_video\u3001reference_to_video \u6216 edit").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
1606
+ const g = cmd.optsWithGlobals();
1607
+ const auth = resolveAuth(g);
1608
+ if (opts.aspectRatio && !["16:9", "9:16"].includes(opts.aspectRatio)) {
1609
+ throw new ApiError("invalid_request", "--aspect-ratio must be 16:9 or 9:16");
1610
+ }
1611
+ if (opts.task && !["text_to_video", "image_to_video", "reference_to_video", "edit"].includes(opts.task)) {
1612
+ throw new ApiError("invalid_request", "--task must be text_to_video, image_to_video, reference_to_video, or edit");
1613
+ }
1614
+ const prompt = promptParts.join(" ");
1615
+ const imageInputs = (opts.image ?? []).map(geminiOmniImageInput);
1616
+ const input = imageInputs.length === 0 ? prompt : [...imageInputs, { type: "text", text: prompt }];
1617
+ const res = await withProgress("\u6B63\u5728\u751F\u6210 Gemini Omni \u89C6\u9891", () => request({
1618
+ baseUrl: auth.baseUrl,
1619
+ path: "/v1beta/interactions",
1620
+ apiKey: auth.apiKey,
1621
+ body: {
1622
+ model: "gemini-omni-flash-preview",
1623
+ input,
1624
+ ...opts.previousInteractionId ? { previous_interaction_id: opts.previousInteractionId } : {},
1625
+ ...opts.aspectRatio ? { response_format: { type: "video", aspect_ratio: opts.aspectRatio } } : {},
1626
+ ...opts.task ? { generation_config: { video_config: { task: opts.task } } } : {}
1627
+ },
1628
+ timeoutMs: 6e5
1629
+ }));
1630
+ const video = extractGeminiOmniVideo(res);
1631
+ if (!video) {
1632
+ throw new ApiError("bad_response", "Gemini Omni \u54CD\u5E94\u4E2D\u672A\u627E\u5230\u89C6\u9891\u6570\u636E", { body: res });
1633
+ }
1634
+ const dir = resolve2(opts.out);
1635
+ await mkdir2(dir, { recursive: true });
1636
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1637
+ const extension = video.mimeType?.includes("webm") ? ".webm" : ".mp4";
1638
+ const file = join3(dir, `gemini-omni-${timestamp}${extension}`);
1639
+ await writeFile(file, Buffer.from(video.data, "base64"));
1640
+ if (g.json) {
1641
+ printJson({ interaction_id: res.id, file });
1642
+ } else {
1643
+ info(`\u2713 ${file}`);
1644
+ if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
1645
+ }
1646
+ });
1647
+ gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF1B\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "Grok \u89C6\u9891\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "Grok \u89C6\u9891\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u56FE\u751F\u89C6\u9891\u7684\u6E90\u56FE\u50CF URL\uFF0C\u53EF\u591A\u4E2A").option("--generate-audio <boolean>", "\u662F\u5426\u751F\u6210\u97F3\u9891\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "generate-audio")).option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--service-tier <tier>", "\u670D\u52A1\u5C42\u7EA7\uFF08Seedance 2.0 \u9ED8\u8BA4 default\uFF09").option("--priority <n>", "\u4EFB\u52A1\u4F18\u5148\u7EA7\uFF08\u4EC5 Seedance 2.0 \u7CFB\u5217\uFF09", (v) => Number.parseInt(v, 10)).option("--callback-url <url>", "\u4EFB\u52A1\u5B8C\u6210\u56DE\u8C03 URL").option("--return-last-frame <boolean>", "\u662F\u5426\u8FD4\u56DE\u6700\u540E\u4E00\u5E27\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "return-last-frame")).option("--execution-expires-after <seconds>", "\u4EFB\u52A1\u8FC7\u671F\u79D2\u6570\uFF083600\u2013259200\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-identifier <identifier>", "Seedance \u5B89\u5168\u6807\u8BC6\u7B26\uFF081\u201364 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09").option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u5B8C\u6210").option("--poll-interval <ms>", "\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--timeout <minutes>", "\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--content <json>", "Ark-compatible content JSON array; overrides prompt/image facade fields").action(
1291
1648
  async (promptParts, opts, cmd) => {
1292
1649
  const g = cmd.optsWithGlobals();
1293
1650
  const auth = resolveAuth(g);
1294
- const body = { model: opts.model, prompt: promptParts.join(" ") };
1651
+ const model = opts.model ?? (await resolveCreativeModel(auth, "video")).model.id;
1652
+ if (!opts.model && !g.json) info(`\u5DF2\u81EA\u52A8\u9009\u62E9\u89C6\u9891\u6A21\u578B\uFF1A${model}`);
1653
+ const body = { model, prompt: promptParts.join(" ") };
1295
1654
  const metadata = {};
1296
1655
  if (opts.seconds !== void 0) {
1297
1656
  const seconds = clampInt(opts.seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds");
@@ -1301,6 +1660,8 @@ function registerGen(program) {
1301
1660
  if (opts.image) body.images = opts.image;
1302
1661
  if (opts.resolution) metadata.resolution = opts.resolution.toLowerCase();
1303
1662
  if (opts.ratio) metadata.ratio = opts.ratio;
1663
+ if (opts.aspectRatio) metadata.ratio = opts.aspectRatio;
1664
+ if (opts.seed !== void 0) metadata.seed = opts.seed;
1304
1665
  if (opts.generateAudio !== void 0) metadata.generate_audio = opts.generateAudio;
1305
1666
  if (opts.watermark !== void 0) metadata.watermark = opts.watermark;
1306
1667
  if (opts.serviceTier) metadata.service_tier = opts.serviceTier;
@@ -1310,10 +1671,12 @@ function registerGen(program) {
1310
1671
  if (opts.executionExpiresAfter !== void 0) metadata.execution_expires_after = opts.executionExpiresAfter;
1311
1672
  if (opts.safetyIdentifier) metadata.safety_identifier = opts.safetyIdentifier;
1312
1673
  if (opts.content) metadata.content = parseJsonArray(opts.content, "content");
1313
- validateVideoGeneration(opts.model, {
1674
+ validateVideoGeneration(model, {
1314
1675
  seconds: opts.seconds,
1315
1676
  resolution: opts.resolution,
1316
1677
  ratio: opts.ratio,
1678
+ aspectRatio: opts.aspectRatio,
1679
+ seed: opts.seed,
1317
1680
  serviceTier: opts.serviceTier,
1318
1681
  priority: opts.priority,
1319
1682
  executionExpiresAfter: opts.executionExpiresAfter,
@@ -1333,7 +1696,7 @@ function registerGen(program) {
1333
1696
  }
1334
1697
  if (opts.wait === false) {
1335
1698
  if (g.json) {
1336
- printJson({ task_id: taskId, submitted: true });
1699
+ printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
1337
1700
  } else {
1338
1701
  process.stdout.write(taskId + "\n");
1339
1702
  info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}`);
@@ -1352,7 +1715,7 @@ function registerGen(program) {
1352
1715
  });
1353
1716
  const filePath = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
1354
1717
  if (g.json) {
1355
- printJson({ task_id: taskId, status: final.status, file: filePath });
1718
+ printJson({ model, task_id: taskId, status: final.status, file: filePath });
1356
1719
  } else {
1357
1720
  info(`\u2713 ${filePath}`);
1358
1721
  }
@@ -1495,219 +1858,433 @@ function registerUsage(program) {
1495
1858
  }
1496
1859
 
1497
1860
  // src/commands/connect.ts
1498
- import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
1861
+ import { createHash, randomUUID } from "crypto";
1862
+ import {
1863
+ cpSync,
1864
+ existsSync as existsSync2,
1865
+ lstatSync,
1866
+ mkdirSync as mkdirSync2,
1867
+ readFileSync as readFileSync3,
1868
+ readdirSync,
1869
+ readlinkSync,
1870
+ renameSync,
1871
+ rmSync,
1872
+ writeFileSync as writeFileSync2
1873
+ } from "fs";
1499
1874
  import { homedir as homedir2, platform } from "os";
1500
- import { dirname, join as join4 } from "path";
1875
+ import { dirname, join as join4, relative, resolve as resolve4 } from "path";
1501
1876
  import { fileURLToPath } from "url";
1502
- var MANIFEST_NAME = ".focalapi-connect-manifest.json";
1877
+ var MANIFEST_NAME = ".focalapi-managed-skills.json";
1878
+ var LEGACY_MANIFEST_NAME = ".focalapi-connect-manifest.json";
1879
+ var AGENTS = [
1880
+ { id: "adal", name: "ADAL", skillsPath: [".adal", "skills"] },
1881
+ { id: "amp", name: "Amp", skillsPath: [".config", "agents", "skills"], detectPaths: [[".amp"], [".config", "amp"]] },
1882
+ { id: "antigravity", name: "Antigravity", skillsPath: [".gemini", "antigravity", "skills"] },
1883
+ { id: "augment", name: "Augment", skillsPath: [".augment", "skills"] },
1884
+ { id: "bob", name: "Bob", skillsPath: [".bob", "skills"] },
1885
+ { id: "claude-code", name: "Claude Code", skillsPath: [".claude", "skills"] },
1886
+ { id: "cline", name: "Cline", skillsPath: [".agents", "skills"], detectPaths: [[".cline"]] },
1887
+ { id: "codebuddy", name: "CodeBuddy", skillsPath: [".codebuddy", "skills"] },
1888
+ { id: "codex", name: "Codex", skillsPath: [".agents", "skills"], detectPaths: [[".codex"]] },
1889
+ { id: "command-code", name: "Command Code", skillsPath: [".commandcode", "skills"] },
1890
+ { id: "continue", name: "Continue", skillsPath: [".continue", "skills"] },
1891
+ { id: "cortex", name: "Snowflake Cortex", skillsPath: [".snowflake", "cortex", "skills"] },
1892
+ { id: "crush", name: "Crush", skillsPath: [".config", "crush", "skills"] },
1893
+ { id: "cursor", name: "Cursor", skillsPath: [".cursor", "skills"] },
1894
+ { id: "deepagents", name: "Deep Agents", skillsPath: [".deepagents", "agent", "skills"] },
1895
+ { id: "droid", name: "Factory Droid", skillsPath: [".factory", "skills"] },
1896
+ { id: "firebender", name: "Firebender", skillsPath: [".firebender", "skills"] },
1897
+ { id: "gemini-cli", name: "Gemini CLI", skillsPath: [".gemini", "skills"] },
1898
+ { id: "github-copilot", name: "GitHub Copilot", skillsPath: [".copilot", "skills"] },
1899
+ { id: "goose", name: "Goose", skillsPath: [".config", "goose", "skills"] },
1900
+ { id: "iflow-cli", name: "iFlow CLI", skillsPath: [".iflow", "skills"] },
1901
+ { id: "junie", name: "Junie", skillsPath: [".junie", "skills"] },
1902
+ { id: "kilo", name: "Kilo Code", skillsPath: [".kilocode", "skills"] },
1903
+ { id: "kimi-cli", name: "Kimi CLI", skillsPath: [".config", "agents", "skills"], detectPaths: [[".kimi"], [".config", "kimi"]] },
1904
+ { id: "kiro-cli", name: "Kiro CLI", skillsPath: [".kiro", "skills"] },
1905
+ { id: "kode", name: "Kode", skillsPath: [".kode", "skills"] },
1906
+ { id: "mcpjam", name: "MCPJam", skillsPath: [".mcpjam", "skills"] },
1907
+ { id: "mistral-vibe", name: "Mistral Vibe", skillsPath: [".vibe", "skills"] },
1908
+ { id: "mux", name: "Mux", skillsPath: [".mux", "skills"] },
1909
+ { id: "neovate", name: "Neovate", skillsPath: [".neovate", "skills"] },
1910
+ { id: "openclaw", name: "OpenClaw", skillsPath: [".openclaw", "skills"] },
1911
+ { id: "opencode", name: "OpenCode", skillsPath: [".config", "opencode", "skills"] },
1912
+ { id: "openhands", name: "OpenHands", skillsPath: [".openhands", "skills"] },
1913
+ { id: "pi", name: "Pi", skillsPath: [".agents", "skills"], detectPaths: [[".pi", "agent"]] },
1914
+ { id: "pochi", name: "Pochi", skillsPath: [".pochi", "skills"] },
1915
+ { id: "qoder", name: "Qoder", skillsPath: [".qoder", "skills"] },
1916
+ { id: "qwen-code", name: "Qwen Code", skillsPath: [".qwen", "skills"] },
1917
+ { id: "roo", name: "Roo Code", skillsPath: [".roo", "skills"] },
1918
+ { id: "trae", name: "Trae", skillsPath: [".trae", "skills"] },
1919
+ { id: "trae-cn", name: "Trae CN", skillsPath: [".trae-cn", "skills"] },
1920
+ { id: "warp", name: "Warp", skillsPath: [".agents", "skills"], detectPaths: [[".warp"]] },
1921
+ { id: "windsurf", name: "Windsurf", skillsPath: [".codeium", "windsurf", "skills"] },
1922
+ { id: "zencoder", name: "Zencoder", skillsPath: [".zencoder", "skills"] }
1923
+ ];
1503
1924
  function homeDir() {
1504
1925
  return normalizeHomePath(process.env.FOCALAPI_HOME ?? homedir2());
1505
1926
  }
1506
1927
  function getTargets() {
1507
1928
  const home = homeDir();
1508
- const claudeRoot = join4(home, ".claude");
1509
- const codexRoot = join4(home, ".codex");
1510
- const opencodeRoot = join4(home, ".config", "opencode");
1511
- const hermesRoot = platform() === "win32" ? join4(home, "AppData", "Local", "hermes") : join4(home, ".config", "hermes");
1512
- return [
1513
- {
1514
- id: "claude-code",
1515
- name: "Claude Code",
1516
- skillsDir: join4(claudeRoot, "skills"),
1517
- detected: existsSync2(claudeRoot),
1518
- providerHint: [
1519
- "Claude Code provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1520
- " \u5728 ~/.claude/settings.json \u7684 env \u6BB5\u52A0\u5165\uFF1A",
1521
- ' "ANTHROPIC_BASE_URL": "https://api.focalapi.com",',
1522
- ' "ANTHROPIC_AUTH_TOKEN": "<\u4F60\u7684 sk- key>"',
1523
- " \uFF08focalapi \u5DF2\u517C\u5BB9 /v1/messages \u534F\u8BAE\uFF09"
1524
- ].join("\n")
1525
- },
1526
- {
1527
- id: "codex",
1528
- name: "Codex",
1529
- skillsDir: join4(codexRoot, "skills"),
1530
- detected: existsSync2(codexRoot),
1531
- providerHint: [
1532
- "Codex provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1533
- " \u5728 ~/.codex/config.toml \u52A0\u5165\uFF1A",
1534
- " [model_providers.focalapi]",
1535
- ' name = "focalapi"',
1536
- ' base_url = "https://api.focalapi.com/v1"',
1537
- ' env_key = "FOCALAPI_API_KEY"',
1538
- " \uFF08focalapi \u5DF2\u517C\u5BB9 /v1/responses \u4E0E /v1/chat/completions\uFF09"
1539
- ].join("\n")
1540
- },
1541
- {
1542
- id: "opencode",
1543
- name: "OpenCode",
1544
- skillsDir: join4(opencodeRoot, "skills"),
1545
- detected: existsSync2(opencodeRoot),
1546
- providerHint: [
1547
- "OpenCode provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1548
- " \u5728 opencode.json \u7684 provider \u6BB5\u52A0\u5165 openai-compatible \u63D0\u4F9B\u5546\uFF0C",
1549
- ' baseURL = "https://api.focalapi.com/v1"\uFF0CapiKey \u6307\u5411\u4F60\u7684 sk- key\u3002'
1550
- ].join("\n")
1551
- },
1552
- {
1553
- id: "hermes",
1554
- name: "Hermes",
1555
- skillsDir: join4(hermesRoot, "skills"),
1556
- detected: existsSync2(hermesRoot),
1557
- providerHint: [
1558
- "Hermes \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1559
- " \u6280\u80FD\u5DF2\u88C5\u5165\u9ED8\u8BA4 profile \u7684 skills \u76EE\u5F55\uFF1B\u975E\u9ED8\u8BA4 profile \u8BF7\u628A\u6280\u80FD\u76EE\u5F55\u590D\u5236\u5230",
1560
- " hermes/profiles/<name>/skills/\u3002provider \u5728 config.yaml \u52A0 openai-compatible",
1561
- ' \u63D0\u4F9B\u5546\uFF0Cbase_url = "https://api.focalapi.com/v1"\u3002'
1562
- ].join("\n")
1563
- }
1564
- ];
1929
+ const definitions = [...AGENTS];
1930
+ definitions.push({
1931
+ id: "hermes",
1932
+ name: "Hermes",
1933
+ skillsPath: platform() === "win32" ? ["AppData", "Local", "hermes", "skills"] : [".config", "hermes", "skills"]
1934
+ });
1935
+ return definitions.map((agent) => {
1936
+ const skillsDir = join4(home, ...agent.skillsPath);
1937
+ const detectPaths = agent.detectPaths ?? [agent.skillsPath.slice(0, -1)];
1938
+ return {
1939
+ id: agent.id,
1940
+ name: agent.name,
1941
+ skillsDir,
1942
+ detected: detectPaths.some((parts) => existsSync2(join4(home, ...parts)))
1943
+ };
1944
+ });
1565
1945
  }
1566
1946
  function bundledSkillsDir() {
1567
- if (process.env.FOCALAPI_SKILLS_DIR) {
1568
- return process.env.FOCALAPI_SKILLS_DIR;
1569
- }
1947
+ if (process.env.FOCALAPI_SKILLS_DIR) return normalizeHomePath(process.env.FOCALAPI_SKILLS_DIR);
1570
1948
  const here = dirname(fileURLToPath(import.meta.url));
1571
1949
  const candidates = [join4(here, "..", "skills"), join4(here, "..", "..", "skills")];
1572
1950
  for (const dir of candidates) {
1573
- if (existsSync2(join4(dir, "focalapi", "SKILL.md"))) {
1574
- return dir;
1575
- }
1951
+ if (existsSync2(join4(dir, "focalapi", "SKILL.md"))) return dir;
1576
1952
  }
1577
- throw new ApiError("internal_error", "\u672A\u627E\u5230\u5185\u7F6E skills \u76EE\u5F55\uFF08focalapi/SKILL.md \u7F3A\u5931\uFF09");
1953
+ throw new ApiError("internal_error", "\u672A\u627E\u5230\u5185\u7F6E Skills\uFF08focalapi/SKILL.md \u7F3A\u5931\uFF09");
1578
1954
  }
1579
1955
  function listBundledSkills(srcDir) {
1580
1956
  const dir = srcDir ?? bundledSkillsDir();
1581
- return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("focalapi") && existsSync2(join4(dir, d.name, "SKILL.md"))).map((d) => d.name).sort();
1957
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("focalapi") && existsSync2(join4(dir, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
1582
1958
  }
1583
1959
  function manifestPath(skillsDir) {
1584
1960
  return join4(skillsDir, MANIFEST_NAME);
1585
1961
  }
1586
1962
  function readManifest(skillsDir) {
1587
- const path = manifestPath(skillsDir);
1588
- if (!existsSync2(path)) return void 0;
1963
+ for (const name of [MANIFEST_NAME, LEGACY_MANIFEST_NAME]) {
1964
+ const path = join4(skillsDir, name);
1965
+ if (!existsSync2(path)) continue;
1966
+ try {
1967
+ const manifest = JSON.parse(readFileSync3(path, "utf8"));
1968
+ if (manifest.tool === "focalapi-cli" && Array.isArray(manifest.skills)) return manifest;
1969
+ } catch {
1970
+ }
1971
+ }
1972
+ return void 0;
1973
+ }
1974
+ function digestDirectory(root) {
1975
+ const hash = createHash("sha256");
1976
+ const walk = (dir) => {
1977
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1978
+ for (const entry of entries) {
1979
+ const path = join4(dir, entry.name);
1980
+ const name = relative(root, path).replaceAll("\\", "/");
1981
+ const stat = lstatSync(path);
1982
+ if (stat.isSymbolicLink()) {
1983
+ hash.update(`link\0${name}\0${readlinkSync(path)}\0`);
1984
+ } else if (stat.isDirectory()) {
1985
+ hash.update(`dir\0${name}\0`);
1986
+ walk(path);
1987
+ } else if (stat.isFile()) {
1988
+ hash.update(`file\0${name}\0`);
1989
+ hash.update(readFileSync3(path));
1990
+ hash.update("\0");
1991
+ }
1992
+ }
1993
+ };
1994
+ walk(root);
1995
+ return hash.digest("hex");
1996
+ }
1997
+ function isAuthConfigured() {
1998
+ if (process.env.FOCALAPI_API_KEY) return true;
1589
1999
  try {
1590
- return JSON.parse(readFileSync3(path, "utf-8"));
2000
+ return Object.values(loadConfig().profiles).some((profile) => Boolean(profile.apiKey));
1591
2001
  } catch {
1592
- return void 0;
2002
+ return false;
2003
+ }
2004
+ }
2005
+ function groupTargets(targets) {
2006
+ const grouped = /* @__PURE__ */ new Map();
2007
+ for (const target of targets) {
2008
+ const key = resolve4(target.skillsDir).toLowerCase();
2009
+ grouped.set(key, [...grouped.get(key) ?? [], target]);
2010
+ }
2011
+ return [...grouped.values()].map((agents) => ({ agents, skillsDir: agents[0].skillsDir }));
2012
+ }
2013
+ function installTo(skillsDir, agents, skills, srcDir) {
2014
+ const sourceRoot = resolve4(srcDir).toLowerCase();
2015
+ if (resolve4(skillsDir).toLowerCase() === sourceRoot) {
2016
+ throw new ApiError("invalid_request", "\u5B89\u88C5\u76EE\u6807\u4E0D\u80FD\u662F focalapi-cli \u81EA\u5E26 Skills \u6E90\u76EE\u5F55");
2017
+ }
2018
+ mkdirSync2(skillsDir, { recursive: true });
2019
+ const oldManifest = readManifest(skillsDir);
2020
+ const transactionRoot = join4(skillsDir, `.focalapi-install-${randomUUID()}`);
2021
+ const stageRoot = join4(transactionRoot, "stage");
2022
+ const backupRoot = join4(transactionRoot, "backup");
2023
+ mkdirSync2(stageRoot, { recursive: true });
2024
+ mkdirSync2(backupRoot, { recursive: true });
2025
+ const digests = {};
2026
+ const touched = [];
2027
+ let oldManifestBackup;
2028
+ let oldLegacyManifestBackup;
2029
+ try {
2030
+ for (const skill of skills) {
2031
+ const staged = join4(stageRoot, skill);
2032
+ cpSync(join4(srcDir, skill), staged, { recursive: true, force: true });
2033
+ digests[skill] = digestDirectory(staged);
2034
+ }
2035
+ const currentManifest = manifestPath(skillsDir);
2036
+ if (existsSync2(currentManifest)) {
2037
+ oldManifestBackup = join4(backupRoot, MANIFEST_NAME);
2038
+ renameSync(currentManifest, oldManifestBackup);
2039
+ }
2040
+ const legacyManifest = join4(skillsDir, LEGACY_MANIFEST_NAME);
2041
+ if (existsSync2(legacyManifest)) {
2042
+ oldLegacyManifestBackup = join4(backupRoot, LEGACY_MANIFEST_NAME);
2043
+ renameSync(legacyManifest, oldLegacyManifestBackup);
2044
+ }
2045
+ for (const skill of skills) {
2046
+ const destination = join4(skillsDir, skill);
2047
+ const backup = join4(backupRoot, skill);
2048
+ if (existsSync2(destination)) {
2049
+ renameSync(destination, backup);
2050
+ touched.push({ destination, backup });
2051
+ } else {
2052
+ touched.push({ destination });
2053
+ }
2054
+ renameSync(join4(stageRoot, skill), destination);
2055
+ }
2056
+ for (const retired of oldManifest?.skills ?? []) {
2057
+ if (skills.includes(retired) || !retired.startsWith("focalapi")) continue;
2058
+ const destination = join4(skillsDir, retired);
2059
+ const expected = oldManifest?.digests?.[retired];
2060
+ if (!expected || !existsSync2(destination) || digestDirectory(destination) !== expected) continue;
2061
+ const backup = join4(backupRoot, `retired-${retired}`);
2062
+ renameSync(destination, backup);
2063
+ touched.push({ destination, backup });
2064
+ }
2065
+ const manifest = {
2066
+ tool: "focalapi-cli",
2067
+ schema: 2,
2068
+ version: VERSION,
2069
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2070
+ agents,
2071
+ skills,
2072
+ digests
2073
+ };
2074
+ const stagedManifest = join4(transactionRoot, MANIFEST_NAME);
2075
+ writeFileSync2(stagedManifest, JSON.stringify(manifest, null, 2) + "\n", "utf8");
2076
+ renameSync(stagedManifest, currentManifest);
2077
+ } catch (error) {
2078
+ rmSync(manifestPath(skillsDir), { force: true });
2079
+ for (const item of touched.reverse()) {
2080
+ rmSync(item.destination, { recursive: true, force: true });
2081
+ if (item.backup && existsSync2(item.backup)) renameSync(item.backup, item.destination);
2082
+ }
2083
+ if (oldManifestBackup && existsSync2(oldManifestBackup)) renameSync(oldManifestBackup, manifestPath(skillsDir));
2084
+ if (oldLegacyManifestBackup && existsSync2(oldLegacyManifestBackup)) {
2085
+ renameSync(oldLegacyManifestBackup, join4(skillsDir, LEGACY_MANIFEST_NAME));
2086
+ }
2087
+ throw error;
2088
+ } finally {
2089
+ rmSync(transactionRoot, { recursive: true, force: true });
1593
2090
  }
2091
+ return { agents, skillsDir, skills, version: VERSION };
1594
2092
  }
1595
- function installTo(target, skills, srcDir) {
1596
- mkdirSync2(target.skillsDir, { recursive: true });
1597
- for (const skill of skills) {
1598
- cpSync(join4(srcDir, skill), join4(target.skillsDir, skill), { recursive: true });
2093
+ function uninstallFrom(skillsDir, srcDir) {
2094
+ const manifest = readManifest(skillsDir);
2095
+ if (!manifest) return { removed: [], preserved: [], hadManifest: false };
2096
+ const removed = [];
2097
+ const preserved = [];
2098
+ for (const skill of manifest.skills ?? []) {
2099
+ if (!skill.startsWith("focalapi")) continue;
2100
+ const dir = join4(skillsDir, skill);
2101
+ if (!existsSync2(dir)) continue;
2102
+ const expected = manifest.digests?.[skill] ?? (existsSync2(join4(srcDir, skill)) ? digestDirectory(join4(srcDir, skill)) : void 0);
2103
+ if (!expected || digestDirectory(dir) !== expected) {
2104
+ preserved.push(skill);
2105
+ continue;
2106
+ }
2107
+ rmSync(dir, { recursive: true, force: true });
2108
+ removed.push(skill);
2109
+ }
2110
+ rmSync(manifestPath(skillsDir), { force: true });
2111
+ rmSync(join4(skillsDir, LEGACY_MANIFEST_NAME), { force: true });
2112
+ return { removed, preserved, hadManifest: true };
2113
+ }
2114
+ function verifyTarget(skillsDir) {
2115
+ const manifest = readManifest(skillsDir);
2116
+ if (!manifest) return { installed: false, valid: false, missing: [], modified: [] };
2117
+ const missing = [];
2118
+ const modified = [];
2119
+ for (const skill of manifest.skills ?? []) {
2120
+ const dir = join4(skillsDir, skill);
2121
+ if (!existsSync2(dir)) {
2122
+ missing.push(skill);
2123
+ continue;
2124
+ }
2125
+ const expected = manifest.digests?.[skill];
2126
+ if (!expected || digestDirectory(dir) !== expected) modified.push(skill);
1599
2127
  }
1600
- const manifest = {
1601
- tool: "focalapi-cli",
1602
- version: VERSION,
1603
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1604
- skills
2128
+ return {
2129
+ installed: true,
2130
+ valid: missing.length === 0 && modified.length === 0,
2131
+ version: manifest.version,
2132
+ missing,
2133
+ modified
1605
2134
  };
1606
- writeFileSync2(manifestPath(target.skillsDir), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
1607
- return manifest;
1608
2135
  }
1609
- function uninstallFrom(target) {
1610
- const manifest = readManifest(target.skillsDir);
1611
- if (!manifest) {
1612
- return { removed: [], hadManifest: false };
2136
+ function customTarget(path) {
2137
+ const skillsDir = resolve4(normalizeHomePath(path));
2138
+ if (dirname(skillsDir) === skillsDir || skillsDir.toLowerCase() === resolve4(homeDir()).toLowerCase()) {
2139
+ throw new ApiError("invalid_request", "--path \u5FC5\u987B\u6307\u5411\u5177\u4F53\u7684 Skills \u76EE\u5F55\uFF0C\u4E0D\u80FD\u662F\u78C1\u76D8\u6839\u76EE\u5F55\u6216\u7528\u6237\u4E3B\u76EE\u5F55");
1613
2140
  }
1614
- const removed = [];
1615
- for (const skill of manifest.skills) {
1616
- const dir = join4(target.skillsDir, skill);
1617
- if (skill.startsWith("focalapi") && existsSync2(dir)) {
1618
- rmSync(dir, { recursive: true, force: true });
1619
- removed.push(skill);
2141
+ return {
2142
+ id: "custom",
2143
+ name: "Custom Skills Directory",
2144
+ skillsDir,
2145
+ detected: true
2146
+ };
2147
+ }
2148
+ function resolveTargets(targetIds, path, g) {
2149
+ if (path) {
2150
+ if (targetIds.length > 0) {
2151
+ throw new ApiError("invalid_request", "--path \u4E0E Agent ID \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
1620
2152
  }
2153
+ return [customTarget(path)];
1621
2154
  }
1622
- rmSync(manifestPath(target.skillsDir), { force: true });
1623
- return { removed, hadManifest: true };
1624
- }
1625
- function resolveTargets(targetIds, g) {
1626
2155
  const all = getTargets();
1627
- if (targetIds && targetIds.length > 0) {
1628
- const unknown = targetIds.filter((id) => !all.some((t) => t.id === id));
2156
+ if (targetIds.length > 0) {
2157
+ const unknown = targetIds.filter((id) => !all.some((target) => target.id === id));
1629
2158
  if (unknown.length > 0) {
1630
2159
  throw new ApiError("invalid_request", `\u672A\u77E5 Agent\uFF1A${unknown.join(", ")}`, {
1631
- hint: `\u53EF\u9009\uFF1A${all.map((t) => t.id).join(" | ")}\u3002`
2160
+ hint: `\u53EF\u9009\uFF1A${all.map((target) => target.id).join(" | ")}\uFF1B\u672A\u77E5 Agent \u53EF\u4F7F\u7528 --path <skills-dir>\u3002`
1632
2161
  });
1633
2162
  }
1634
- return all.filter((t) => targetIds.includes(t.id));
2163
+ return all.filter((target) => targetIds.includes(target.id));
1635
2164
  }
1636
- const detected = all.filter((t) => t.detected);
2165
+ const detected = all.filter((target) => target.detected);
1637
2166
  if (detected.length === 0) {
1638
- throw new ApiError("invalid_request", "\u672A\u68C0\u6D4B\u5230\u4EFB\u4F55\u672C\u673A Agent", {
1639
- hint: `\u652F\u6301\uFF1A${all.map((t) => `${t.id}\uFF08${t.name}\uFF09`).join("\u3001")}\u3002\u53EF\u5148\u663E\u5F0F\u6307\u5B9A\uFF1Afocalapi connect install <agent>\u3002`
2167
+ throw new ApiError("invalid_request", "\u672A\u68C0\u6D4B\u5230\u672C\u673A Agent", {
2168
+ hint: "\u8FD0\u884C focalapi connect list \u67E5\u770B\u652F\u6301\u5217\u8868\uFF0C\u6216\u4F7F\u7528 focalapi connect install --path <skills-dir>\u3002"
1640
2169
  });
1641
2170
  }
1642
- if (!g.json) {
1643
- info(`\u68C0\u6D4B\u5230 ${detected.length} \u4E2A Agent\uFF1A${detected.map((t) => t.name).join("\u3001")}`);
1644
- }
2171
+ if (!g.json) info(`\u68C0\u6D4B\u5230 ${detected.length} \u4E2A Agent\uFF0C\u5C06\u6309 ${groupTargets(detected).length} \u4E2A\u6280\u80FD\u76EE\u5F55\u5B89\u88C5\u3002`);
1645
2172
  return detected;
1646
2173
  }
2174
+ function installCommand(targetIds, path, g) {
2175
+ const targets = resolveTargets(targetIds, path, g);
2176
+ const srcDir = bundledSkillsDir();
2177
+ const skills = listBundledSkills(srcDir);
2178
+ const results = groupTargets(targets).map((group) => installTo(
2179
+ group.skillsDir,
2180
+ group.agents.map((agent) => agent.id),
2181
+ skills,
2182
+ srcDir
2183
+ ));
2184
+ const authConfigured = isAuthConfigured();
2185
+ const nextSteps = [
2186
+ ...!authConfigured ? ["focalapi auth login --key <sk-...>"] : [],
2187
+ "\u91CD\u542F Agent \u4F1A\u8BDD",
2188
+ "\u76F4\u63A5\u63CF\u8FF0\u521B\u4F5C\u4EFB\u52A1\uFF0C\u4F8B\u5982\u201C\u751F\u6210\u4E00\u5F20\u4EA7\u54C1\u4E3B\u89C6\u89C9\u201D"
2189
+ ];
2190
+ if (g.json) {
2191
+ printJson({
2192
+ installed: results,
2193
+ auth_configured: authConfigured,
2194
+ ready: authConfigured,
2195
+ restart_required: true,
2196
+ next_steps: nextSteps
2197
+ });
2198
+ return;
2199
+ }
2200
+ for (const result of results) {
2201
+ info(`\u2713 ${result.agents.join(" / ")}\uFF1A${result.skills.length} \u4E2A Skills \u5DF2\u88C5\u5165 ${result.skillsDir}`);
2202
+ }
2203
+ info(authConfigured ? "\u63A5\u5165\u5DF2\u95ED\u73AF\uFF1A\u91CD\u542F Agent \u540E\uFF0C\u76F4\u63A5\u63CF\u8FF0\u56FE\u7247\u6216\u89C6\u9891\u4EFB\u52A1\u5373\u53EF\uFF1B\u65E0\u9700\u70B9\u540D focalapi \u6216\u5148\u8BD5\u6A21\u578B\u3002" : "Skills \u5DF2\u5B89\u88C5\uFF1B\u8FD8\u9700\u8FD0\u884C focalapi auth login --key <sk-...>\uFF0C\u7136\u540E\u91CD\u542F Agent\u3002");
2204
+ }
1647
2205
  function registerConnect(program) {
1648
- const connect = program.command("connect").description("\u628A focalapi \u80FD\u529B\u6CE8\u5165\u672C\u673A AI Agent\uFF08skills \u5B89\u88C5/\u5378\u8F7D\uFF09");
1649
- connect.command("list").description("\u5217\u51FA\u652F\u6301\u7684 Agent \u53CA\u68C0\u6D4B/\u5B89\u88C5\u72B6\u6001").action(async (_opts, cmd) => {
2206
+ const connect = program.command("connect").description("\u8BA9\u672C\u673A AI Agent \u81EA\u52A8\u8C03\u7528 focalapi \u521B\u4F5C\u6A21\u578B\uFF08\u4E0D\u6539 Agent \u7684\u4E3B\u6A21\u578B/provider\uFF09").option("--path <skills-dir>", "\u5B89\u88C5\u5230\u6307\u5B9A Skills \u76EE\u5F55\uFF0C\u4E0D\u626B\u63CF\u5168\u5C40 Agent").action(async (opts, cmd) => {
2207
+ installCommand([], opts.path, cmd.optsWithGlobals());
2208
+ });
2209
+ connect.command("list").description("\u5217\u51FA\u652F\u6301\u7684 Agent \u53CA\u68C0\u6D4B/\u5B89\u88C5\u72B6\u6001\uFF08\u53EA\u8BFB\uFF09").action(async (_opts, cmd) => {
1650
2210
  const g = cmd.optsWithGlobals();
1651
- const rows = getTargets().map((t) => {
1652
- const manifest = readManifest(t.skillsDir);
2211
+ const rows = getTargets().map((target) => {
2212
+ const status = verifyTarget(target.skillsDir);
1653
2213
  return {
1654
- id: t.id,
1655
- name: t.name,
1656
- detected: t.detected,
1657
- skillsDir: t.skillsDir,
1658
- installed: manifest ? `${manifest.skills.length} \u4E2A\u6280\u80FD\uFF08v${manifest.version}\uFF09` : ""
2214
+ id: target.id,
2215
+ name: target.name,
2216
+ detected: target.detected,
2217
+ skills_dir: target.skillsDir,
2218
+ installed: status.installed,
2219
+ valid: status.valid,
2220
+ version: status.version
1659
2221
  };
1660
2222
  });
1661
2223
  if (g.json) {
1662
- printJson({ agents: rows });
2224
+ printJson({ supported: rows.length, agents: rows });
1663
2225
  } else {
1664
2226
  printTable(
1665
- ["Agent", "ID", "\u68C0\u6D4B\u5230", "\u5DF2\u5B89\u88C5", "\u6280\u80FD\u76EE\u5F55"],
1666
- rows.map((r) => [r.name, r.id, r.detected ? "\u2713" : "-", r.installed || "-", r.skillsDir])
2227
+ ["Agent", "ID", "\u68C0\u6D4B\u5230", "\u5B89\u88C5\u72B6\u6001", "\u6280\u80FD\u76EE\u5F55"],
2228
+ rows.map((row) => [
2229
+ row.name,
2230
+ row.id,
2231
+ row.detected ? "\u2713" : "-",
2232
+ row.valid ? `\u2713 v${row.version}` : row.installed ? "\u9700\u4FEE\u590D" : "-",
2233
+ row.skills_dir
2234
+ ])
1667
2235
  );
1668
2236
  }
1669
2237
  });
1670
- connect.command("install").description("\u5411\u6307\u5B9A\uFF08\u6216\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684\uFF09Agent \u5B89\u88C5 focalapi \u6280\u80FD\u5305").argument("[targets...]", `Agent ID\uFF0C\u5982 claude-code codex\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684`).action(async (targetIds, _opts, cmd) => {
2238
+ connect.command("install").description("\u4E8B\u52A1\u5F0F\u5B89\u88C5/\u4FEE\u590D Skills\uFF1B\u7701\u7565 Agent ID \u65F6\u5904\u7406\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684 Agent").argument("[targets...]", "Agent ID\uFF0C\u5982 claude-code codex").option("--path <skills-dir>", "\u5B89\u88C5\u5230\u6307\u5B9A Skills \u76EE\u5F55\uFF0C\u4E0D\u626B\u63CF\u5168\u5C40 Agent").action(async (targetIds, opts, cmd) => {
2239
+ installCommand(targetIds, opts.path ?? cmd.parent?.opts().path, cmd.optsWithGlobals());
2240
+ });
2241
+ connect.command("verify").description("\u9A8C\u8BC1 Skills \u5B8C\u6574\u6027\u3001\u8BA4\u8BC1\u5C31\u7EEA\u72B6\u6001\u4E0E\u9700\u8981\u91CD\u542F\u7684 Agent").argument("[targets...]", "Agent ID\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684").option("--path <skills-dir>", "\u9A8C\u8BC1\u6307\u5B9A Skills \u76EE\u5F55").action(async (targetIds, opts, cmd) => {
1671
2242
  const g = cmd.optsWithGlobals();
1672
- const targets = resolveTargets(targetIds, g);
1673
- const srcDir = bundledSkillsDir();
1674
- const skills = listBundledSkills(srcDir);
1675
- const results = targets.map((t) => ({ target: t, manifest: installTo(t, skills, srcDir) }));
2243
+ const targets = resolveTargets(targetIds, opts.path ?? cmd.parent?.opts().path, g);
2244
+ const rows = groupTargets(targets).map((group) => ({
2245
+ agents: group.agents.map((agent) => agent.id),
2246
+ skills_dir: group.skillsDir,
2247
+ ...verifyTarget(group.skillsDir)
2248
+ }));
2249
+ const authConfigured = isAuthConfigured();
2250
+ const valid = rows.every((row) => row.valid);
2251
+ const result = {
2252
+ valid,
2253
+ auth_configured: authConfigured,
2254
+ ready: valid && authConfigured,
2255
+ targets: rows,
2256
+ next_steps: [
2257
+ ...!valid ? ["focalapi connect install"] : [],
2258
+ ...!authConfigured ? ["focalapi auth login --key <sk-...>"] : [],
2259
+ ...valid && authConfigured ? ["\u91CD\u542F Agent \u4F1A\u8BDD\u540E\u76F4\u63A5\u63CF\u8FF0\u521B\u4F5C\u4EFB\u52A1"] : []
2260
+ ]
2261
+ };
1676
2262
  if (g.json) {
1677
- printJson({
1678
- installed: results.map((r) => ({ agent: r.target.id, skillsDir: r.target.skillsDir, skills: r.manifest.skills }))
1679
- });
1680
- return;
1681
- }
1682
- for (const { target, manifest } of results) {
1683
- info(`\u2713 ${target.name}\uFF1A${manifest.skills.length} \u4E2A\u6280\u80FD\u5DF2\u88C5\u5165 ${target.skillsDir}`);
1684
- }
1685
- info("");
1686
- info("\u6280\u80FD\u5DF2\u5C31\u7EEA\u3002\u8981\u8BA9 Agent \u76F4\u63A5\u4EE5 focalapi \u4E3A\u6A21\u578B\u540E\u7AEF\uFF0C\u8FD8\u9700\u914D\u7F6E provider\uFF08v1 \u8BF7\u624B\u52A8\uFF09\uFF1A");
1687
- for (const { target } of results) {
1688
- info("");
1689
- info(target.providerHint);
2263
+ printJson(result);
2264
+ } else {
2265
+ printTable(
2266
+ ["Agent", "Skills", "\u8BA4\u8BC1", "\u5C31\u7EEA"],
2267
+ rows.map((row) => [row.agents.join(" / "), row.valid ? "\u2713" : "\u9700\u4FEE\u590D", authConfigured ? "\u2713" : "\u672A\u914D\u7F6E", row.valid && authConfigured ? "\u2713" : "-"])
2268
+ );
2269
+ for (const step of result.next_steps) info(`\u4E0B\u4E00\u6B65\uFF1A${step}`);
1690
2270
  }
1691
- info("");
1692
- info("\u5B8C\u6210\u540E\u91CD\u542F Agent \u4F1A\u8BDD\uFF0C\u5373\u53EF\u7528\u81EA\u7136\u8BED\u8A00\u8BA9\u5B83\u8C03\u7528 focalapi\uFF08\u5982\u300C\u7528 focalapi \u753B\u4E00\u5F20\u2026\u2026\u300D\uFF09\u3002");
1693
- info("\u5378\u8F7D\uFF1Afocalapi connect uninstall");
2271
+ if (!result.ready) process.exitCode = 1;
1694
2272
  });
1695
- connect.command("uninstall").description("\u6309 manifest \u7CBE\u786E\u5378\u8F7D\u6CE8\u5165\u7684\u6280\u80FD\uFF08\u4E0D\u78B0\u5176\u4ED6\u6587\u4EF6\uFF09").argument("[targets...]", "Agent ID\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684").action(async (targetIds, _opts, cmd) => {
2273
+ connect.command("uninstall").description("\u5378\u8F7D\u672A\u88AB\u7528\u6237\u4FEE\u6539\u7684\u6258\u7BA1 Skills\uFF1B\u5176\u4ED6\u6587\u4EF6\u4E00\u5F8B\u4FDD\u7559").argument("[targets...]", "Agent ID\uFF1B\u7701\u7565=\u5168\u90E8\u5DF2\u68C0\u6D4B\u5230\u7684").option("--path <skills-dir>", "\u4ECE\u6307\u5B9A Skills \u76EE\u5F55\u5378\u8F7D").action(async (targetIds, opts, cmd) => {
1696
2274
  const g = cmd.optsWithGlobals();
1697
- const targets = resolveTargets(targetIds, g);
1698
- const results = targets.map((t) => ({ target: t, ...uninstallFrom(t) }));
2275
+ const targets = resolveTargets(targetIds, opts.path ?? cmd.parent?.opts().path, g);
2276
+ const srcDir = bundledSkillsDir();
2277
+ const results = groupTargets(targets).map((group) => ({
2278
+ agents: group.agents.map((agent) => agent.id),
2279
+ skills_dir: group.skillsDir,
2280
+ ...uninstallFrom(group.skillsDir, srcDir)
2281
+ }));
1699
2282
  if (g.json) {
1700
- printJson({
1701
- uninstalled: results.map((r) => ({ agent: r.target.id, removed: r.removed, hadManifest: r.hadManifest }))
1702
- });
2283
+ printJson({ uninstalled: results });
1703
2284
  return;
1704
2285
  }
1705
- for (const r of results) {
1706
- if (!r.hadManifest) {
1707
- info(`- ${r.target.name}\uFF1A\u65E0 focalapi \u5B89\u88C5\u8BB0\u5F55\uFF0C\u8DF3\u8FC7`);
1708
- } else {
1709
- info(`\u2713 ${r.target.name}\uFF1A\u5DF2\u79FB\u9664 ${r.removed.length} \u4E2A\u6280\u80FD`);
1710
- }
2286
+ for (const result of results) {
2287
+ info(`\u2713 ${result.agents.join(" / ")}\uFF1A\u79FB\u9664 ${result.removed.length} \u4E2A\uFF0C\u4FDD\u7559\u7528\u6237\u4FEE\u6539 ${result.preserved.length} \u4E2A`);
1711
2288
  }
1712
2289
  });
1713
2290
  }
@@ -1810,7 +2387,7 @@ function registerRequest(program) {
1810
2387
  // src/cli.ts
1811
2388
  function buildProgram() {
1812
2389
  const program = new Command();
1813
- program.name("focalapi").description("\u9762\u5411\u521B\u4F5C\u5DE5\u4F5C\u6D41\u7684 focalapi CLI\uFF1A\u56FE\u50CF\u3001\u89C6\u9891\u3001\u97F3\u9891\u4E0E DeepSeek \u5907\u7528\u6587\u672C\u80FD\u529B\uFF0C\u4E00\u6761\u547D\u4EE4\u76F4\u8FBE").version(VERSION, "-v, --version", "\u663E\u793A\u7248\u672C\u53F7").option("--json", "\u4EE5 JSON \u8F93\u51FA\uFF08\u9762\u5411 Agent \u4E0E\u811A\u672C\uFF0Cstdout \u7EAF\u51C0\uFF09").option("--base-url <url>", "\u8986\u76D6 API \u5730\u5740\uFF08\u9ED8\u8BA4 https://api.focalapi.com\uFF0C\u53EF\u7528 FOCALAPI_BASE_URL\uFF09").option("--key <key>", "\u8986\u76D6 API Key\uFF08\u53EF\u7528 FOCALAPI_API_KEY\uFF09").option("--profile <name>", "\u4F7F\u7528\u6307\u5B9A\u914D\u7F6E\u6863\u6848");
2390
+ program.name("focalapi").description("\u8BA9 AI Agent \u76F4\u63A5\u8C03\u7528 focalapi \u521B\u4F5C\u6A21\u578B\uFF1A\u81EA\u52A8\u9009\u6A21\u3001\u751F\u6210\u3001\u4EFB\u52A1\u7EED\u53D6\u4E0E\u7528\u91CF\u8BCA\u65AD").version(VERSION, "-v, --version", "\u663E\u793A\u7248\u672C\u53F7").option("--json", "\u4EE5 JSON \u8F93\u51FA\uFF08\u9762\u5411 Agent \u4E0E\u811A\u672C\uFF0Cstdout \u7EAF\u51C0\uFF09").option("--base-url <url>", "\u8986\u76D6 API \u5730\u5740\uFF08\u9ED8\u8BA4 https://api.focalapi.com\uFF0C\u53EF\u7528 FOCALAPI_BASE_URL\uFF09").option("--key <key>", "\u8986\u76D6 API Key\uFF08\u53EF\u7528 FOCALAPI_API_KEY\uFF09").option("--profile <name>", "\u4F7F\u7528\u6307\u5B9A\u914D\u7F6E\u6863\u6848");
1814
2391
  registerAuth(program);
1815
2392
  registerModels(program);
1816
2393
  registerChat(program);
@@ -1825,10 +2402,11 @@ function buildProgram() {
1825
2402
  return program;
1826
2403
  }
1827
2404
  async function main(argv = process.argv) {
2405
+ process.exitCode = 0;
1828
2406
  const program = buildProgram();
1829
2407
  try {
1830
2408
  await program.parseAsync(argv);
1831
- return 0;
2409
+ return typeof process.exitCode === "number" ? process.exitCode : 0;
1832
2410
  } catch (err) {
1833
2411
  let json = false;
1834
2412
  try {