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/CHANGELOG.md +10 -0
- package/LICENSE +202 -202
- package/README.md +155 -209
- package/dist/cli.js +795 -217
- package/package.json +53 -50
- package/scripts/postinstall.cjs +41 -0
- package/skills/focalapi/SKILL.md +54 -43
- package/skills/focalapi-auth/SKILL.md +30 -50
- package/skills/focalapi-chat/SKILL.md +32 -29
- package/skills/focalapi-gen/SKILL.md +53 -79
- package/skills/focalapi-models/SKILL.md +49 -0
- package/skills/focalapi-task/SKILL.md +25 -0
- package/skills/focalapi-usage/SKILL.md +27 -36
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.
|
|
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 ?? "
|
|
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("
|
|
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
|
|
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(
|
|
515
|
-
|
|
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
|
-
["\
|
|
518
|
-
|
|
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("
|
|
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
|
|
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(
|
|
642
|
+
printJson(response);
|
|
532
643
|
} else {
|
|
533
|
-
printModelDetails(
|
|
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("
|
|
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
|
-
"
|
|
795
|
-
defaultSize: "
|
|
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
|
-
"
|
|
803
|
-
defaultSize: "
|
|
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
|
-
"
|
|
810
|
-
defaultSize: "
|
|
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
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1038
|
+
"dreamina-seedance-2-5-260628": {
|
|
836
1039
|
resolutions: ["480p", "720p"],
|
|
837
1040
|
ratios: SEEDANCE_RATIOS,
|
|
838
1041
|
minSeconds: 4,
|
|
839
|
-
maxSeconds:
|
|
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-
|
|
845
|
-
"gemini-3
|
|
846
|
-
"gemini-3.1-flash-
|
|
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
|
-
|
|
884
|
-
|
|
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
|
-
|
|
887
|
-
|
|
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(
|
|
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
|
|
913
|
-
|
|
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
|
|
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.
|
|
941
|
-
|
|
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 &&
|
|
944
|
-
throw new ApiError("invalid_request", `${model} does not support priority
|
|
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\
|
|
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(
|
|
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
|
|
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("\
|
|
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
|
|
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(
|
|
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 {
|
|
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-
|
|
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
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
|
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((
|
|
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
|
|
1588
|
-
|
|
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
|
|
2000
|
+
return Object.values(loadConfig().profiles).some((profile) => Boolean(profile.apiKey));
|
|
1591
2001
|
} catch {
|
|
1592
|
-
return
|
|
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
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
|
1610
|
-
const
|
|
1611
|
-
if (
|
|
1612
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
|
1628
|
-
const unknown = targetIds.filter((id) => !all.some((
|
|
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((
|
|
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((
|
|
2163
|
+
return all.filter((target) => targetIds.includes(target.id));
|
|
1635
2164
|
}
|
|
1636
|
-
const detected = all.filter((
|
|
2165
|
+
const detected = all.filter((target) => target.detected);
|
|
1637
2166
|
if (detected.length === 0) {
|
|
1638
|
-
throw new ApiError("invalid_request", "\u672A\u68C0\u6D4B\u5230\
|
|
1639
|
-
hint:
|
|
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("\
|
|
1649
|
-
|
|
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((
|
|
1652
|
-
const
|
|
2211
|
+
const rows = getTargets().map((target) => {
|
|
2212
|
+
const status = verifyTarget(target.skillsDir);
|
|
1653
2213
|
return {
|
|
1654
|
-
id:
|
|
1655
|
-
name:
|
|
1656
|
-
detected:
|
|
1657
|
-
|
|
1658
|
-
installed:
|
|
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", "\
|
|
1666
|
-
rows.map((
|
|
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("\
|
|
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
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
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("\
|
|
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
|
|
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
|
|
1706
|
-
|
|
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("\
|
|
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 {
|