focalapi-cli 0.1.0 → 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
@@ -8,6 +8,10 @@ var ApiError = class extends Error {
8
8
  code;
9
9
  status;
10
10
  hint;
11
+ /** 上游返回的稳定错误代码或类型;不包含密钥等敏感请求信息。 */
12
+ upstreamCode;
13
+ /** 可用于向服务方关联日志的请求 ID。 */
14
+ requestId;
11
15
  /** 上游原始响应体(已截断),仅调试用途,打印前需脱敏。 */
12
16
  body;
13
17
  constructor(code, message, opts) {
@@ -17,6 +21,8 @@ var ApiError = class extends Error {
17
21
  this.status = opts?.status;
18
22
  this.hint = opts?.hint;
19
23
  this.body = opts?.body;
24
+ this.upstreamCode = opts?.upstreamCode;
25
+ this.requestId = opts?.requestId;
20
26
  }
21
27
  toJSON() {
22
28
  return {
@@ -24,12 +30,14 @@ var ApiError = class extends Error {
24
30
  code: this.code,
25
31
  message: this.message,
26
32
  ...this.hint ? { hint: this.hint } : {},
27
- ...this.status !== void 0 ? { status: this.status } : {}
33
+ ...this.status !== void 0 ? { status: this.status } : {},
34
+ ...this.upstreamCode ? { upstream_code: this.upstreamCode } : {},
35
+ ...this.requestId ? { request_id: this.requestId } : {}
28
36
  }
29
37
  };
30
38
  }
31
39
  };
32
- function refineErrorCode(status, message) {
40
+ function refineErrorCode(status, message, opts) {
33
41
  const m = message.toLowerCase();
34
42
  if (m.includes("quota") || m.includes("\u989D\u5EA6") || m.includes("insufficient")) {
35
43
  return "insufficient_quota";
@@ -38,14 +46,17 @@ function refineErrorCode(status, message) {
38
46
  return "model_not_found";
39
47
  }
40
48
  if (m.includes("key") || m.includes("token") || m.includes("auth")) {
41
- return status === 401 || status === 403 ? "invalid_api_key" : "invalid_request";
49
+ if (status === 401 || status === 403) {
50
+ return opts?.authFailureIsInvalidApiKey ? "invalid_api_key" : "upstream_auth_failed";
51
+ }
52
+ return "invalid_request";
42
53
  }
43
54
  switch (status) {
44
55
  case 400:
45
56
  return "invalid_request";
46
57
  case 401:
47
58
  case 403:
48
- return "invalid_api_key";
59
+ return opts?.authFailureIsInvalidApiKey ? "invalid_api_key" : "authentication_failed";
49
60
  case 404:
50
61
  return "model_not_found";
51
62
  case 429:
@@ -103,6 +114,14 @@ function printError(err, opts) {
103
114
  const hint = err.hint;
104
115
  if (hint) {
105
116
  process.stderr.write(`\u63D0\u793A\uFF1A${hint}
117
+ `);
118
+ }
119
+ if (err.upstreamCode) {
120
+ process.stderr.write(`\u4E0A\u6E38\u4EE3\u7801\uFF1A${err.upstreamCode}
121
+ `);
122
+ }
123
+ if (err.requestId) {
124
+ process.stderr.write(`\u8BF7\u6C42 ID\uFF1A${err.requestId}
106
125
  `);
107
126
  }
108
127
  }
@@ -136,7 +155,7 @@ function displayWidth(s) {
136
155
  }
137
156
 
138
157
  // src/lib/version.ts
139
- var VERSION = true ? "0.1.0" : "0.0.0-dev";
158
+ var VERSION = true ? "0.2.0" : "0.0.0-dev";
140
159
 
141
160
  // src/commands/auth.ts
142
161
  import { createInterface } from "readline/promises";
@@ -248,7 +267,8 @@ function extractErrorMessage(raw) {
248
267
  const obj = parsed;
249
268
  const errObj = obj?.error;
250
269
  const message = typeof errObj?.message === "string" && errObj.message || typeof obj?.message === "string" && obj.message || JSON.stringify(parsed).slice(0, 500);
251
- return { message, body: parsed };
270
+ const upstreamCode = [errObj?.code, errObj?.type, obj?.code, obj?.type].find((value) => typeof value === "string");
271
+ return { message, body: parsed, upstreamCode: typeof upstreamCode === "string" ? upstreamCode : void 0 };
252
272
  }
253
273
  async function request(opts) {
254
274
  const res = await rawRequest(opts);
@@ -298,9 +318,10 @@ async function rawRequest(opts) {
298
318
  }
299
319
  if (!res.ok) {
300
320
  const text = await res.text().catch(() => "");
301
- const { message, body } = extractErrorMessage(text);
302
- const code = refineErrorCode(res.status, message);
303
- throw new ApiError(code, message, { status: res.status, body });
321
+ const { message, body, upstreamCode } = extractErrorMessage(text);
322
+ const code = refineErrorCode(res.status, message, { authFailureIsInvalidApiKey: opts.authFailureIsInvalidApiKey });
323
+ const requestId = res.headers.get("x-request-id") ?? res.headers.get("request-id") ?? res.headers.get("x-requestid") ?? void 0;
324
+ throw new ApiError(code, message, { status: res.status, body, upstreamCode, requestId });
304
325
  }
305
326
  return res;
306
327
  }
@@ -342,7 +363,8 @@ async function fetchTokenUsage(baseUrl, apiKey) {
342
363
  baseUrl,
343
364
  path: "/api/usage/token/",
344
365
  apiKey,
345
- timeoutMs: 15e3
366
+ timeoutMs: 15e3,
367
+ authFailureIsInvalidApiKey: true
346
368
  });
347
369
  if (!res.data) {
348
370
  throw new ApiError("bad_response", "\u7528\u91CF\u63A5\u53E3\u54CD\u5E94\u7F3A\u5C11 data \u5B57\u6BB5");
@@ -443,42 +465,183 @@ function registerAuth(program) {
443
465
  });
444
466
  }
445
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
+
446
527
  // src/commands/models.ts
528
+ function textValue(value) {
529
+ if (value === null || value === void 0 || value === "") return "-";
530
+ if (Array.isArray(value)) return value.length === 0 ? "-" : value.map(textValue).join(", ");
531
+ if (typeof value === "object") return JSON.stringify(value);
532
+ return String(value);
533
+ }
534
+ function parameterConstraint(parameter) {
535
+ const constraints = [];
536
+ if (parameter.required) constraints.push("\u5FC5\u586B");
537
+ if (parameter.default !== void 0) constraints.push(`\u9ED8\u8BA4 ${String(parameter.default)}`);
538
+ if (parameter.values?.length) constraints.push(`\u53EF\u9009 ${parameter.values.join(" / ")}`);
539
+ if (parameter.minimum !== void 0 || parameter.maximum !== void 0) {
540
+ constraints.push(`${parameter.minimum ?? "-"}\u2013${parameter.maximum ?? "-"}`);
541
+ }
542
+ return [parameter.type, ...constraints, parameter.description].join("\uFF1B");
543
+ }
544
+ function printModelDetails(model) {
545
+ const { supported_params: supportedParams, ...summary } = model;
546
+ printTable(
547
+ ["\u5B57\u6BB5", "\u503C"],
548
+ Object.entries(summary).map(([key, value]) => [key, textValue(value)])
549
+ );
550
+ if (Array.isArray(supportedParams) && supportedParams.length > 0) {
551
+ process.stdout.write("\n\u652F\u6301\u53C2\u6570\n");
552
+ printTable(
553
+ ["\u53C2\u6570", "\u7EA6\u675F"],
554
+ supportedParams.map((parameter) => {
555
+ const item = parameter;
556
+ return [item.name, parameterConstraint(item)];
557
+ })
558
+ );
559
+ }
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
+ }
447
584
  function registerModels(program) {
448
585
  const models = program.command("models").description("\u53EF\u7528\u6A21\u578B\u67E5\u8BE2");
449
- 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) => {
450
587
  const g = cmd.optsWithGlobals();
451
588
  const auth = resolveAuth(g);
452
- const res = await request({ baseUrl: auth.baseUrl, path: "/v1/models", apiKey: auth.apiKey });
453
- let list = res.data ?? [];
454
- if (opts.filter) {
455
- const kw = opts.filter.toLowerCase();
456
- list = list.filter((m) => m.id.toLowerCase().includes(kw));
457
- }
589
+ const resolved = await resolveCreativeModel(auth, parseCreativeCapability(capability));
458
590
  if (g.json) {
459
- printJson({ data: list });
460
- } 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");
461
606
  printTable(
462
- ["\u6A21\u578B ID", "\u63D0\u4F9B\u65B9"],
463
- list.map((m) => [m.id, m.owned_by ?? "-"])
607
+ ["\u53C2\u6570", "\u7EA6\u675F"],
608
+ resolved.model.supported_params.map((parameter) => {
609
+ const item = parameter;
610
+ return [item.name, parameterConstraint(item)];
611
+ })
464
612
  );
465
613
  }
466
614
  });
467
- 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) => {
468
624
  const g = cmd.optsWithGlobals();
469
625
  const auth = resolveAuth(g);
470
- const res = await request({
626
+ const response = await request({
471
627
  baseUrl: auth.baseUrl,
472
628
  path: `/v1/models/${encodeURIComponent(model)}`,
473
629
  apiKey: auth.apiKey
474
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
+ }
475
641
  if (g.json) {
476
- printJson(res);
642
+ printJson(response);
477
643
  } else {
478
- printTable(
479
- ["\u5B57\u6BB5", "\u503C"],
480
- Object.entries(res).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)])
481
- );
644
+ printModelDetails(response);
482
645
  }
483
646
  });
484
647
  }
@@ -551,7 +714,7 @@ function extractText(content) {
551
714
  return "";
552
715
  }
553
716
  function registerChat(program) {
554
- program.command("chat").description("\u5BF9\u8BDD\u4E0E\u591A\u6A21\u6001\u63A8\u7406\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(
555
718
  async (promptParts, opts, cmd) => {
556
719
  const g = cmd.optsWithGlobals();
557
720
  const auth = resolveAuth(g);
@@ -724,6 +887,350 @@ import { join as join3, resolve as resolve2 } from "path";
724
887
  import { pipeline as pipeline2 } from "stream/promises";
725
888
  import { Readable as Readable2 } from "stream";
726
889
 
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
+ ];
909
+ var IMAGE_CONSTRAINTS = {
910
+ "gpt-image-2": {
911
+ defaultSize: "1024x1024",
912
+ maxN: 8,
913
+ maxReferenceImages: 16,
914
+ minMegapixels: 0.65536,
915
+ maxMegapixels: 8.2944,
916
+ minEdge: 1024,
917
+ maxEdge: 3840,
918
+ edgeMultiple: 16,
919
+ maxAspectRatio: 3,
920
+ qualities: ["low", "medium", "high"],
921
+ backgrounds: ["auto", "opaque"]
922
+ },
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"],
938
+ maxN: 10,
939
+ maxReferenceImages: 10,
940
+ maxTotalImages: 15,
941
+ minMegapixels: 3.6864,
942
+ maxMegapixels: 16.777216,
943
+ supportsWatermark: true,
944
+ outputFormats: SEEDREAM_OUTPUT_FORMATS,
945
+ optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
946
+ },
947
+ "dola-seedream-5-0-pro-260628": {
948
+ defaultSize: "1k",
949
+ sizeTiers: ["1k", "1.5k", "2k"],
950
+ maxN: 1,
951
+ maxReferenceImages: 10,
952
+ minMegapixels: 0.92,
953
+ maxMegapixels: 4.194304,
954
+ supportsWatermark: true,
955
+ outputFormats: SEEDREAM_OUTPUT_FORMATS,
956
+ optimizePromptModes: SEEDREAM_OPTIMIZE_PROMPT_MODES
957
+ },
958
+ "seedream-5-0-260128": {
959
+ defaultSize: "2k",
960
+ sizeTiers: ["2k", "3k", "4k"],
961
+ maxN: 14,
962
+ maxReferenceImages: 14,
963
+ maxTotalImages: 15,
964
+ minMegapixels: 3.6864,
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
977
+ },
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
+ }
986
+ };
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"];
989
+ var VIDEO_CONSTRAINTS = {
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": {
1018
+ resolutions: ["480p", "720p", "1080p", "4k"],
1019
+ ratios: SEEDANCE_RATIOS,
1020
+ minSeconds: 4,
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
1030
+ },
1031
+ "dreamina-seedance-2-0-mini-260615": {
1032
+ resolutions: ["480p", "720p"],
1033
+ ratios: SEEDANCE_RATIOS,
1034
+ minSeconds: 4,
1035
+ maxSeconds: 15,
1036
+ supportsPriority: true
1037
+ },
1038
+ "dreamina-seedance-2-5-260628": {
1039
+ resolutions: ["480p", "720p"],
1040
+ ratios: SEEDANCE_RATIOS,
1041
+ minSeconds: 4,
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
1057
+ }
1058
+ };
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"];
1060
+ var GEMINI_IMAGE_CONSTRAINTS = {
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": {
1065
+ aspectRatios: [...COMMON_GEMINI_RATIOS, "1:4", "4:1", "1:8", "8:1"],
1066
+ imageSizes: ["1K"],
1067
+ supportsSampling: true
1068
+ }
1069
+ };
1070
+ function parseSize(size, model) {
1071
+ const match = /^(\d+)x(\d+)$/i.exec(size.trim());
1072
+ if (!match) {
1073
+ throw new ApiError("invalid_request", `${model} size must be a supported tier or WIDTHxHEIGHT (received: ${size})`);
1074
+ }
1075
+ return { width: Number(match[1]), height: Number(match[2]) };
1076
+ }
1077
+ function megapixels(width, height) {
1078
+ return width * height / 1e6;
1079
+ }
1080
+ function formatMegapixels(value) {
1081
+ return value.toFixed(2).replace(/\.00$/, "");
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
+ }
1092
+ function validateImageGeneration(model, input) {
1093
+ if (input.responseFormat && input.responseFormat !== "url" && input.responseFormat !== "b64_json") {
1094
+ throw new ApiError("invalid_request", `response_format must be url or b64_json (received: ${input.responseFormat})`);
1095
+ }
1096
+ const constraint = IMAGE_CONSTRAINTS[model.trim()];
1097
+ if (!constraint) return;
1098
+ if (input.n < 1 || input.n > constraint.maxN) {
1099
+ throw new ApiError("invalid_request", `${model} n must be 1-${constraint.maxN} (received: ${input.n})`);
1100
+ }
1101
+ if (input.imageCount !== void 0 && constraint.maxReferenceImages !== void 0 && input.imageCount > constraint.maxReferenceImages) {
1102
+ throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxReferenceImages} reference images`);
1103
+ }
1104
+ if (input.imageCount !== void 0 && constraint.maxTotalImages !== void 0 && input.imageCount + input.n > constraint.maxTotalImages) {
1105
+ throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxTotalImages} input plus generated images`);
1106
+ }
1107
+ if (input.hasMask && model === "gpt-image-2" && input.imageCount !== 1) {
1108
+ throw new ApiError("invalid_request", "gpt-image-2 mask requires exactly one reference image");
1109
+ }
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`);
1126
+ }
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})`);
1131
+ }
1132
+ const { width, height } = parseSize(suppliedSize, model);
1133
+ const pixels = megapixels(width, height);
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) {
1135
+ const edge = constraint.minEdge && constraint.maxEdge ? `${constraint.minEdge}-${constraint.maxEdge}px per edge` : "";
1136
+ const step = constraint.edgeMultiple ? `, edge multiples of ${constraint.edgeMultiple}` : "";
1137
+ const mp = constraint.minMegapixels && constraint.maxMegapixels ? `, ${formatMegapixels(constraint.minMegapixels)}-${formatMegapixels(constraint.maxMegapixels)} MP` : "";
1138
+ throw new ApiError("invalid_request", `${model} does not support size=${width}x${height}; supported: ${edge}${step}${mp}`);
1139
+ }
1140
+ if (constraint.maxAspectRatio) {
1141
+ const ratio = Math.max(width, height) / Math.min(width, height);
1142
+ if (ratio > constraint.maxAspectRatio) {
1143
+ throw new ApiError("invalid_request", `${model} longest-to-shortest edge ratio must not exceed ${constraint.maxAspectRatio}:1 (received: ${width}x${height})`);
1144
+ }
1145
+ }
1146
+ }
1147
+ function validateGeminiImageGeneration(model, input) {
1148
+ const constraint = GEMINI_IMAGE_CONSTRAINTS[model.trim()];
1149
+ if (!constraint) {
1150
+ throw new ApiError("invalid_request", `${model} is not a supported Gemini image model; run focalapi models get ${model} first`);
1151
+ }
1152
+ if (input.aspectRatio && !constraint.aspectRatios.includes(input.aspectRatio)) {
1153
+ throw new ApiError("invalid_request", `${model} aspectRatio must be one of ${constraint.aspectRatios.join(", ")} (received: ${input.aspectRatio})`);
1154
+ }
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})`);
1158
+ }
1159
+ if (input.seed !== void 0 && (!Number.isInteger(input.seed) || input.seed < 0)) {
1160
+ throw new ApiError("invalid_request", "seed must be a non-negative integer");
1161
+ }
1162
+ if (!constraint.supportsSampling && (input.thinkingLevel || input.temperature !== void 0 || input.topP !== void 0)) {
1163
+ throw new ApiError("invalid_request", `${model} supports thinkingLevel, temperature, and topP only on gemini-3.1-flash-lite-image`);
1164
+ }
1165
+ if (input.thinkingLevel && !["MINIMAL", "HIGH"].includes(input.thinkingLevel.toUpperCase())) {
1166
+ throw new ApiError("invalid_request", "thinkingLevel must be MINIMAL or HIGH");
1167
+ }
1168
+ if (input.temperature !== void 0 && (!Number.isFinite(input.temperature) || input.temperature < 0 || input.temperature > 2)) {
1169
+ throw new ApiError("invalid_request", "temperature must be between 0 and 2");
1170
+ }
1171
+ if (input.topP !== void 0 && (!Number.isFinite(input.topP) || input.topP < 0 || input.topP > 1)) {
1172
+ throw new ApiError("invalid_request", "topP must be between 0 and 1");
1173
+ }
1174
+ }
1175
+ function validateVideoGeneration(model, input) {
1176
+ const constraint = VIDEO_CONSTRAINTS[model.trim()];
1177
+ if (!constraint) return;
1178
+ if (input.seconds !== void 0 && (input.seconds < constraint.minSeconds || input.seconds > constraint.maxSeconds)) {
1179
+ throw new ApiError("invalid_request", `${model} seconds must be ${constraint.minSeconds}-${constraint.maxSeconds} (received: ${input.seconds})`);
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
+ }
1184
+ if (input.resolution && !constraint.resolutions.includes(input.resolution.toLowerCase())) {
1185
+ throw new ApiError("invalid_request", `${model} resolution must be one of ${constraint.resolutions.join(", ")} (received: ${input.resolution})`);
1186
+ }
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
+ }
1216
+ }
1217
+ if (input.priority !== void 0 && !constraint.supportsPriority) {
1218
+ throw new ApiError("invalid_request", `${model} does not support priority`);
1219
+ }
1220
+ if (input.priority !== void 0 && (input.priority < 0 || input.priority > 9)) {
1221
+ throw new ApiError("invalid_request", `${model} priority must be 0-9 (received: ${input.priority})`);
1222
+ }
1223
+ if (input.serviceTier && input.serviceTier !== "default") {
1224
+ throw new ApiError("invalid_request", `${model} service_tier must be default (received: ${input.serviceTier})`);
1225
+ }
1226
+ if (input.executionExpiresAfter !== void 0 && (input.executionExpiresAfter < 3600 || input.executionExpiresAfter > 259200)) {
1227
+ throw new ApiError("invalid_request", `${model} execution_expires_after must be 3600-259200 (received: ${input.executionExpiresAfter})`);
1228
+ }
1229
+ if (input.safetyIdentifier !== void 0 && !/^[\x21-\x7e]{1,64}$/.test(input.safetyIdentifier)) {
1230
+ throw new ApiError("invalid_request", `${model} safety_identifier must contain 1-64 printable ASCII characters`);
1231
+ }
1232
+ }
1233
+
727
1234
  // src/lib/tasks.ts
728
1235
  import { createWriteStream } from "fs";
729
1236
  import { mkdir } from "fs/promises";
@@ -767,11 +1274,21 @@ function extractProgress(body) {
767
1274
  return void 0;
768
1275
  }
769
1276
  async function fetchTask(baseUrl, apiKey, taskId) {
770
- const raw = await request({
771
- baseUrl,
772
- path: `/v1/video/generations/${encodeURIComponent(taskId)}`,
773
- apiKey
774
- });
1277
+ let raw;
1278
+ try {
1279
+ raw = await request({
1280
+ baseUrl,
1281
+ path: `/v1/video/generations/${encodeURIComponent(taskId)}`,
1282
+ apiKey
1283
+ });
1284
+ } catch (err) {
1285
+ if (!(err instanceof ApiError) || err.status !== 404) throw err;
1286
+ raw = await request({
1287
+ baseUrl,
1288
+ path: `/v1/images/generations/${encodeURIComponent(taskId)}`,
1289
+ apiKey
1290
+ });
1291
+ }
775
1292
  const obj = raw;
776
1293
  const rawStatus = String(obj?.status ?? obj?.data?.status ?? "");
777
1294
  return {
@@ -843,9 +1360,34 @@ function clampInt(value, min, max, name) {
843
1360
  }
844
1361
  return value;
845
1362
  }
1363
+ function parseBooleanOption(value, name) {
1364
+ switch (value.trim().toLowerCase()) {
1365
+ case "true":
1366
+ return true;
1367
+ case "false":
1368
+ return false;
1369
+ default:
1370
+ throw new ApiError("invalid_request", `--${name} must be true or false (received: ${value})`);
1371
+ }
1372
+ }
1373
+ async function withProgress(label, operation) {
1374
+ const startedAt = Date.now();
1375
+ info(`${label}\u2026`);
1376
+ const timer = setInterval(() => {
1377
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1e3));
1378
+ info(`${label}\uFF0C\u5DF2\u7B49\u5F85 ${elapsedSeconds} \u79D2\u2026`);
1379
+ }, 1e4);
1380
+ timer.unref();
1381
+ try {
1382
+ return await operation();
1383
+ } finally {
1384
+ clearInterval(timer);
1385
+ }
1386
+ }
846
1387
  async function saveImageItem(item, dir, base, apiKey) {
847
1388
  if (item.b64_json) {
848
- const filePath = join3(dir, `${base}.png`);
1389
+ const ext = item.mime_type?.includes("jpeg") ? ".jpg" : item.mime_type?.includes("webp") ? ".webp" : ".png";
1390
+ const filePath = join3(dir, `${base}${ext}`);
849
1391
  await writeFile(filePath, Buffer.from(item.b64_json, "base64"));
850
1392
  return filePath;
851
1393
  }
@@ -864,21 +1406,128 @@ async function saveImageItem(item, dir, base, apiKey) {
864
1406
  }
865
1407
  throw new ApiError("bad_response", "\u56FE\u50CF\u7ED3\u679C\u65E2\u6CA1\u6709 url \u4E5F\u6CA1\u6709 b64_json");
866
1408
  }
1409
+ function parseGenerationConfig(raw) {
1410
+ if (!raw) return {};
1411
+ try {
1412
+ const parsed = JSON.parse(raw);
1413
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
1414
+ throw new Error("not an object");
1415
+ }
1416
+ return parsed;
1417
+ } catch {
1418
+ throw new ApiError("invalid_request", "--config \u5FC5\u987B\u662F\u5408\u6CD5\u7684 generationConfig JSON \u5BF9\u8C61\u3002");
1419
+ }
1420
+ }
1421
+ function parseJsonArray(raw, option) {
1422
+ try {
1423
+ const parsed = JSON.parse(raw);
1424
+ if (!Array.isArray(parsed)) throw new Error("not an array");
1425
+ return parsed;
1426
+ } catch {
1427
+ throw new ApiError("invalid_request", `--${option} must be a JSON array`);
1428
+ }
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
+ }
1438
+ function geminiImagePart(source) {
1439
+ const dataUri = /^data:([^;,]+);base64,([a-z0-9+/=\r\n]+)$/i.exec(source.trim());
1440
+ if (dataUri) {
1441
+ const [, mimeType = "", data = ""] = dataUri;
1442
+ return { inlineData: { mimeType, data: data.replace(/[\r\n]/g, "") } };
1443
+ }
1444
+ return { fileData: { fileUri: source } };
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
+ }
1464
+ function extractGeminiImageItems(response) {
1465
+ return (response.candidates ?? []).flatMap(
1466
+ (candidate) => (candidate.content?.parts ?? []).flatMap(
1467
+ (part) => part.inlineData?.data ? [{ b64_json: part.inlineData.data, mime_type: part.inlineData.mimeType }] : []
1468
+ )
1469
+ );
1470
+ }
867
1471
  function registerGen(program) {
868
1472
  const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
869
- gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\u540C\u6B65\u8FD4\u56DE\uFF0C\u4EA7\u7269\u81EA\u52A8\u4E0B\u8F7D\u5230\u672C\u5730\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("--n <count>", "\u5F20\u6570\uFF081\u2013128\uFF09", (v) => Number.parseInt(v, 10), 1).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) => {
870
1474
  const g = cmd.optsWithGlobals();
871
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}`);
872
1478
  const n = clampInt(opts.n, 1, MAX_IMAGE_N, "n");
873
- const body = { model: opts.model, prompt: promptParts.join(" "), n };
1479
+ validateImageGeneration(model, {
1480
+ n,
1481
+ size: opts.size,
1482
+ quality: opts.quality,
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,
1490
+ responseFormat: opts.responseFormat,
1491
+ imageCount: opts.image?.length,
1492
+ hasMask: Boolean(opts.mask)
1493
+ });
1494
+ if (opts.wait === false && opts.responseFormat === "b64_json") {
1495
+ throw new ApiError("invalid_request", "--response-format b64_json cannot be used with --no-wait; use url");
1496
+ }
1497
+ const body = { model, prompt: promptParts.join(" "), n };
874
1498
  if (opts.size) body.size = opts.size;
875
- const res = await request({
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;
1502
+ if (opts.quality) body.quality = opts.quality;
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() };
1507
+ if (opts.image) body.image = opts.image;
1508
+ if (opts.mask) body.mask = opts.mask;
1509
+ if (opts.responseFormat) body.response_format = opts.responseFormat;
1510
+ const res = await withProgress(opts.wait === false ? "\u6B63\u5728\u63D0\u4EA4\u56FE\u50CF\u4EFB\u52A1" : "\u6B63\u5728\u751F\u6210\u56FE\u50CF", () => request({
876
1511
  baseUrl: auth.baseUrl,
877
1512
  path: "/v1/images/generations",
878
1513
  apiKey: auth.apiKey,
879
1514
  body,
1515
+ headers: opts.wait === false ? { Prefer: "respond-async" } : void 0,
880
1516
  timeoutMs: 6e5
881
- });
1517
+ }));
1518
+ if (opts.wait === false) {
1519
+ const taskId = extractTaskId(res);
1520
+ if (!taskId) {
1521
+ throw new ApiError("bad_response", "\u5F02\u6B65\u56FE\u50CF\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: res });
1522
+ }
1523
+ if (g.json) {
1524
+ printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, next_command: `focalapi task status ${taskId} --json` });
1525
+ } else {
1526
+ process.stdout.write(taskId + "\n");
1527
+ info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u67E5\u8BE2\uFF1Afocalapi task status ${taskId}`);
1528
+ }
1529
+ return;
1530
+ }
882
1531
  const items = res.data ?? [];
883
1532
  if (items.length === 0) {
884
1533
  throw new ApiError("bad_response", "\u56FE\u50CF\u751F\u6210\u54CD\u5E94\u4E3A\u7A7A", { body: res });
@@ -891,34 +1540,163 @@ function registerGen(program) {
891
1540
  files.push(await saveImageItem(item, dir, `image-${ts}-${i + 1}`, auth.apiKey));
892
1541
  }
893
1542
  if (g.json) {
894
- printJson({ files, count: files.length });
1543
+ printJson({ model, files, count: files.length });
895
1544
  } else {
896
1545
  for (const f of files) info(`\u2713 ${f}`);
897
1546
  }
898
1547
  });
899
- 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("--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).action(
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) => {
1549
+ const g = cmd.optsWithGlobals();
1550
+ const auth = resolveAuth(g);
1551
+ validateGeminiImageGeneration(opts.model, {
1552
+ aspectRatio: opts.aspectRatio,
1553
+ imageSize: opts.imageSize,
1554
+ seed: opts.seed,
1555
+ thinkingLevel: opts.thinkingLevel,
1556
+ temperature: opts.temperature,
1557
+ topP: opts.topP
1558
+ });
1559
+ const suppliedConfig = parseGenerationConfig(opts.config);
1560
+ const suppliedResponseFormat = suppliedConfig.responseFormat;
1561
+ const suppliedImageConfig = suppliedResponseFormat !== null && typeof suppliedResponseFormat === "object" && !Array.isArray(suppliedResponseFormat) ? suppliedResponseFormat.image : void 0;
1562
+ const imageConfig = {
1563
+ ...suppliedImageConfig !== null && typeof suppliedImageConfig === "object" && !Array.isArray(suppliedImageConfig) ? suppliedImageConfig : {},
1564
+ ...opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {},
1565
+ ...opts.imageSize ? { imageSize: opts.imageSize.toUpperCase() } : {}
1566
+ };
1567
+ const generationConfig = {
1568
+ ...suppliedConfig,
1569
+ candidateCount: 1,
1570
+ responseFormat: { image: imageConfig },
1571
+ ...opts.responseModalities ? { responseModalities: parseGeminiResponseModalities(opts.responseModalities) } : {},
1572
+ ...opts.seed !== void 0 ? { seed: opts.seed } : {},
1573
+ ...opts.thinkingLevel ? { thinkingConfig: { ...suppliedConfig.thinkingConfig ?? {}, thinkingLevel: opts.thinkingLevel.toUpperCase() } } : {},
1574
+ ...opts.temperature !== void 0 ? { temperature: opts.temperature } : {},
1575
+ ...opts.topP !== void 0 ? { topP: opts.topP } : {}
1576
+ };
1577
+ const res = await withProgress("\u6B63\u5728\u751F\u6210 Gemini \u56FE\u50CF", () => request({
1578
+ baseUrl: auth.baseUrl,
1579
+ path: `/v1beta/models/${encodeURIComponent(opts.model)}:generateContent`,
1580
+ apiKey: auth.apiKey,
1581
+ body: {
1582
+ contents: [{ role: "user", parts: [{ text: promptParts.join(" ") }, ...(opts.image ?? []).map(geminiImagePart)] }],
1583
+ ...opts.system ? { systemInstruction: { parts: [{ text: opts.system }] } } : {},
1584
+ generationConfig
1585
+ },
1586
+ timeoutMs: 6e5
1587
+ }));
1588
+ const items = extractGeminiImageItems(res);
1589
+ if (items.length === 0) {
1590
+ throw new ApiError("bad_response", "Gemini \u56FE\u50CF\u54CD\u5E94\u4E2D\u672A\u627E\u5230 inlineData \u56FE\u50CF\u3002", { body: res });
1591
+ }
1592
+ const dir = resolve2(opts.out);
1593
+ await mkdir2(dir, { recursive: true });
1594
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1595
+ const files = [];
1596
+ for (const [i, item] of items.entries()) {
1597
+ files.push(await saveImageItem(item, dir, `gemini-image-${ts}-${i + 1}`, auth.apiKey));
1598
+ }
1599
+ if (g.json) {
1600
+ printJson({ files, count: files.length });
1601
+ } else {
1602
+ for (const file of files) info(`\u2713 ${file}`);
1603
+ }
1604
+ });
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(
900
1648
  async (promptParts, opts, cmd) => {
901
1649
  const g = cmd.optsWithGlobals();
902
1650
  const auth = resolveAuth(g);
903
- 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(" ") };
1654
+ const metadata = {};
904
1655
  if (opts.seconds !== void 0) {
905
- body.seconds = String(clampInt(opts.seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds"));
1656
+ const seconds = clampInt(opts.seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds");
1657
+ body.duration = seconds;
906
1658
  }
907
1659
  if (opts.size) body.size = opts.size;
908
- const created = await request({
1660
+ if (opts.image) body.images = opts.image;
1661
+ if (opts.resolution) metadata.resolution = opts.resolution.toLowerCase();
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;
1665
+ if (opts.generateAudio !== void 0) metadata.generate_audio = opts.generateAudio;
1666
+ if (opts.watermark !== void 0) metadata.watermark = opts.watermark;
1667
+ if (opts.serviceTier) metadata.service_tier = opts.serviceTier;
1668
+ if (opts.priority !== void 0) metadata.priority = opts.priority;
1669
+ if (opts.callbackUrl) metadata.callback_url = opts.callbackUrl;
1670
+ if (opts.returnLastFrame !== void 0) metadata.return_last_frame = opts.returnLastFrame;
1671
+ if (opts.executionExpiresAfter !== void 0) metadata.execution_expires_after = opts.executionExpiresAfter;
1672
+ if (opts.safetyIdentifier) metadata.safety_identifier = opts.safetyIdentifier;
1673
+ if (opts.content) metadata.content = parseJsonArray(opts.content, "content");
1674
+ validateVideoGeneration(model, {
1675
+ seconds: opts.seconds,
1676
+ resolution: opts.resolution,
1677
+ ratio: opts.ratio,
1678
+ aspectRatio: opts.aspectRatio,
1679
+ seed: opts.seed,
1680
+ serviceTier: opts.serviceTier,
1681
+ priority: opts.priority,
1682
+ executionExpiresAfter: opts.executionExpiresAfter,
1683
+ safetyIdentifier: opts.safetyIdentifier
1684
+ });
1685
+ if (Object.keys(metadata).length > 0) body.metadata = metadata;
1686
+ const created = await withProgress("\u6B63\u5728\u63D0\u4EA4\u89C6\u9891\u4EFB\u52A1", () => request({
909
1687
  baseUrl: auth.baseUrl,
910
1688
  path: "/v1/video/generations",
911
1689
  apiKey: auth.apiKey,
912
1690
  body,
913
1691
  timeoutMs: 12e4
914
- });
1692
+ }));
915
1693
  const taskId = extractTaskId(created);
916
1694
  if (!taskId) {
917
1695
  throw new ApiError("bad_response", "\u89C6\u9891\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: created });
918
1696
  }
919
1697
  if (opts.wait === false) {
920
1698
  if (g.json) {
921
- printJson({ task_id: taskId, submitted: true });
1699
+ printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
922
1700
  } else {
923
1701
  process.stdout.write(taskId + "\n");
924
1702
  info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}`);
@@ -937,7 +1715,7 @@ function registerGen(program) {
937
1715
  });
938
1716
  const filePath = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
939
1717
  if (g.json) {
940
- printJson({ task_id: taskId, status: final.status, file: filePath });
1718
+ printJson({ model, task_id: taskId, status: final.status, file: filePath });
941
1719
  } else {
942
1720
  info(`\u2713 ${filePath}`);
943
1721
  }
@@ -980,63 +1758,6 @@ function registerTask(program) {
980
1758
  });
981
1759
  }
982
1760
 
983
- // src/commands/search.ts
984
- import { readFileSync as readFileSync3 } from "fs";
985
- function extractResults(raw) {
986
- if (!raw || typeof raw !== "object") return [];
987
- const obj = raw;
988
- const candidates = [obj.results, obj.data, obj.items, obj.output];
989
- for (const c of candidates) {
990
- if (!Array.isArray(c)) continue;
991
- const out = [];
992
- for (const item of c) {
993
- if (!item || typeof item !== "object") continue;
994
- const it = item;
995
- const url = typeof it.url === "string" ? it.url : typeof it.link === "string" ? it.link : "";
996
- const title = typeof it.title === "string" ? it.title : url;
997
- const snippet = typeof it.snippet === "string" ? it.snippet : typeof it.content === "string" ? it.content.slice(0, 200) : void 0;
998
- if (url || title) out.push({ title, url, ...snippet ? { snippet } : {} });
999
- }
1000
- if (out.length > 0) return out;
1001
- }
1002
- return [];
1003
- }
1004
- function registerSearch(program) {
1005
- program.command("search").description("\u8054\u7F51\u641C\u7D22\uFF08/v1/alpha/search\uFF0Calpha \u7EA7\u63A5\u53E3\uFF09").argument("<query...>", "\u641C\u7D22\u5185\u5BB9").requiredOption("-m, --model <model>", "\u641C\u7D22\u6A21\u578B ID\uFF08focalapi models list --filter search \u67E5\u770B\uFF09").option("--raw <json|@file>", "\u5B8C\u6574\u81EA\u5B9A\u4E49\u8BF7\u6C42\u4F53\uFF08JSON \u5B57\u7B26\u4E32\u6216 @\u6587\u4EF6\uFF09\uFF0C\u4E0E query \u5408\u5E76").action(async (queryParts, opts, cmd) => {
1006
- const g = cmd.optsWithGlobals();
1007
- const auth = resolveAuth(g);
1008
- let body = { model: opts.model, query: queryParts.join(" ") };
1009
- if (opts.raw) {
1010
- const text = opts.raw.startsWith("@") ? readFileSync3(opts.raw.slice(1), "utf-8") : opts.raw;
1011
- try {
1012
- body = { ...JSON.parse(text), model: opts.model };
1013
- } catch {
1014
- throw new ApiError("invalid_request", "--raw \u4E0D\u662F\u5408\u6CD5 JSON");
1015
- }
1016
- }
1017
- const res = await request({
1018
- baseUrl: auth.baseUrl,
1019
- path: "/v1/alpha/search",
1020
- apiKey: auth.apiKey,
1021
- body,
1022
- timeoutMs: 12e4
1023
- });
1024
- if (g.json) {
1025
- printJson(res);
1026
- return;
1027
- }
1028
- const results = extractResults(res);
1029
- if (results.length === 0) {
1030
- printJson(res);
1031
- return;
1032
- }
1033
- printTable(
1034
- ["#", "\u6807\u9898", "\u94FE\u63A5"],
1035
- results.map((r, i) => [String(i + 1), r.title.slice(0, 60), r.url])
1036
- );
1037
- });
1038
- }
1039
-
1040
1761
  // src/commands/audio.ts
1041
1762
  import { writeFile as writeFile2 } from "fs/promises";
1042
1763
  import { resolve as resolve3 } from "path";
@@ -1087,77 +1808,15 @@ function registerAudio(program) {
1087
1808
  });
1088
1809
  }
1089
1810
 
1090
- // src/commands/embed.ts
1091
- function registerEmbed(program) {
1092
- program.command("embed").description("\u6587\u672C\u5411\u91CF\u5316\uFF08/v1/embeddings\uFF09").argument("[text...]", "\u6587\u672C\uFF1B\u7701\u7565\u4E14 stdin \u4E3A\u7BA1\u9053\u65F6\u4ECE stdin \u8BFB\u53D6").requiredOption("-m, --model <model>", "\u5411\u91CF\u6A21\u578B ID").option("--input <file>", "\u4ECE\u6587\u4EF6\u8BFB\u53D6\u6587\u672C\uFF08@ \u524D\u7F00\u53EF\u9009\uFF09").action(async (textParts, opts, cmd) => {
1093
- const g = cmd.optsWithGlobals();
1094
- const auth = resolveAuth(g);
1095
- let text = textParts.join(" ").trim();
1096
- if (opts.input) {
1097
- text = readInputFile(opts.input.replace(/^@/, "")).data.toString("utf-8");
1098
- } else if (!text && !process.stdin.isTTY) {
1099
- text = await readStdin();
1100
- }
1101
- if (!text) {
1102
- throw new ApiError("invalid_request", "\u7F3A\u5C11\u5F85\u5411\u91CF\u5316\u6587\u672C", {
1103
- hint: 'focalapi embed "\u6587\u672C" -m <model>\uFF0C\u6216 focalapi embed -m <model> --input @file.txt\u3002'
1104
- });
1105
- }
1106
- const res = await request({
1107
- baseUrl: auth.baseUrl,
1108
- path: "/v1/embeddings",
1109
- apiKey: auth.apiKey,
1110
- body: { model: opts.model, input: text },
1111
- timeoutMs: 12e4
1112
- });
1113
- if (g.json) {
1114
- printJson(res);
1115
- } else {
1116
- const vec = res.data?.[0]?.embedding ?? [];
1117
- info(`\u7EF4\u5EA6\uFF1A${vec.length}\uFF1Btokens\uFF1A${res.usage?.total_tokens ?? "-"}`);
1118
- info("\u5B8C\u6574\u5411\u91CF\u8BF7\u7528 --json \u8F93\u51FA\u3002");
1119
- }
1120
- });
1121
- }
1122
-
1123
- // src/commands/rerank.ts
1124
- import { readFileSync as readFileSync4 } from "fs";
1125
- function registerRerank(program) {
1126
- program.command("rerank").description("\u6309\u67E5\u8BE2\u5BF9\u6587\u6863\u91CD\u6392\u5E8F\uFF08/v1/rerank\uFF09").requiredOption("-m, --model <model>", "rerank \u6A21\u578B ID").requiredOption("--query <text>", "\u67E5\u8BE2").requiredOption("--docs <json|@file>", "\u6587\u6863\u6570\u7EC4\uFF08JSON \u5B57\u7B26\u4E32\u6216 @file.json\uFF09").option("--top-n <n>", "\u53EA\u8FD4\u56DE\u524D N \u6761", (v) => Number.parseInt(v, 10)).action(async (opts, cmd) => {
1127
- const g = cmd.optsWithGlobals();
1128
- const auth = resolveAuth(g);
1129
- const text = opts.docs.startsWith("@") ? readFileSync4(opts.docs.slice(1), "utf-8") : opts.docs;
1130
- let documents;
1131
- try {
1132
- documents = JSON.parse(text);
1133
- } catch {
1134
- throw new ApiError("invalid_request", "--docs \u4E0D\u662F\u5408\u6CD5 JSON \u6570\u7EC4");
1135
- }
1136
- if (!Array.isArray(documents) || documents.length === 0) {
1137
- throw new ApiError("invalid_request", "--docs \u5FC5\u987B\u662F\u975E\u7A7A JSON \u6570\u7EC4");
1138
- }
1139
- const body = { model: opts.model, query: opts.query, documents };
1140
- if (opts.topN !== void 0) body.top_n = opts.topN;
1141
- const res = await request({
1142
- baseUrl: auth.baseUrl,
1143
- path: "/v1/rerank",
1144
- apiKey: auth.apiKey,
1145
- body,
1146
- timeoutMs: 12e4
1147
- });
1148
- if (g.json) {
1149
- printJson(res);
1150
- return;
1151
- }
1152
- const rows = (res.results ?? []).map((r) => {
1153
- const doc = typeof r.document === "string" ? r.document : r.document?.text ?? "";
1154
- return [String(r.index ?? "-"), String(r.relevance_score ?? "-"), doc.slice(0, 60)];
1155
- });
1156
- printTable(["\u539F\u6587\u6863\u5E8F\u53F7", "\u76F8\u5173\u5EA6", "\u6587\u6863\u9884\u89C8"], rows);
1157
- });
1158
- }
1159
-
1160
1811
  // src/commands/usage.ts
1812
+ function formatBillingUsage(billing) {
1813
+ const value = billing.total_usage;
1814
+ if (typeof value === "number" && Number.isFinite(value)) {
1815
+ return value.toLocaleString("zh-CN", { maximumFractionDigits: 4 });
1816
+ }
1817
+ if (typeof value === "string" && value.trim() !== "") return value;
1818
+ return "-";
1819
+ }
1161
1820
  function defaultStartDate() {
1162
1821
  const now = /* @__PURE__ */ new Date();
1163
1822
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
@@ -1191,226 +1850,441 @@ function registerUsage(program) {
1191
1850
  ["\u5DF2\u7528", String(token.total_used)],
1192
1851
  ["\u5269\u4F59", token.unlimited_quota ? "\u65E0\u9650" : String(token.total_available)],
1193
1852
  ["\u8FC7\u671F\u65F6\u95F4", token.expires_at > 0 ? new Date(token.expires_at * 1e3).toLocaleString() : "\u6C38\u4E0D\u8FC7\u671F"],
1194
- [`\u5468\u671F\u7528\u91CF\uFF08${start} ~ ${end}\uFF09`, JSON.stringify(billing)]
1853
+ [`\u5468\u671F\u7528\u91CF\uFF08${start} ~ ${end}\uFF09`, formatBillingUsage(billing)],
1854
+ ["\u8D26\u5355\u5BF9\u8C61", typeof billing.object === "string" ? billing.object : "-"]
1195
1855
  ]
1196
1856
  );
1197
1857
  });
1198
1858
  }
1199
1859
 
1200
1860
  // src/commands/connect.ts
1201
- import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, 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";
1202
1874
  import { homedir as homedir2, platform } from "os";
1203
- import { dirname, join as join4 } from "path";
1875
+ import { dirname, join as join4, relative, resolve as resolve4 } from "path";
1204
1876
  import { fileURLToPath } from "url";
1205
- 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
+ ];
1206
1924
  function homeDir() {
1207
1925
  return normalizeHomePath(process.env.FOCALAPI_HOME ?? homedir2());
1208
1926
  }
1209
1927
  function getTargets() {
1210
1928
  const home = homeDir();
1211
- const claudeRoot = join4(home, ".claude");
1212
- const codexRoot = join4(home, ".codex");
1213
- const opencodeRoot = join4(home, ".config", "opencode");
1214
- const hermesRoot = platform() === "win32" ? join4(home, "AppData", "Local", "hermes") : join4(home, ".config", "hermes");
1215
- return [
1216
- {
1217
- id: "claude-code",
1218
- name: "Claude Code",
1219
- skillsDir: join4(claudeRoot, "skills"),
1220
- detected: existsSync2(claudeRoot),
1221
- providerHint: [
1222
- "Claude Code provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1223
- " \u5728 ~/.claude/settings.json \u7684 env \u6BB5\u52A0\u5165\uFF1A",
1224
- ' "ANTHROPIC_BASE_URL": "https://api.focalapi.com",',
1225
- ' "ANTHROPIC_AUTH_TOKEN": "<\u4F60\u7684 sk- key>"',
1226
- " \uFF08focalapi \u5DF2\u517C\u5BB9 /v1/messages \u534F\u8BAE\uFF09"
1227
- ].join("\n")
1228
- },
1229
- {
1230
- id: "codex",
1231
- name: "Codex",
1232
- skillsDir: join4(codexRoot, "skills"),
1233
- detected: existsSync2(codexRoot),
1234
- providerHint: [
1235
- "Codex provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1236
- " \u5728 ~/.codex/config.toml \u52A0\u5165\uFF1A",
1237
- " [model_providers.focalapi]",
1238
- ' name = "focalapi"',
1239
- ' base_url = "https://api.focalapi.com/v1"',
1240
- ' env_key = "FOCALAPI_API_KEY"',
1241
- " \uFF08focalapi \u5DF2\u517C\u5BB9 /v1/responses \u4E0E /v1/chat/completions\uFF09"
1242
- ].join("\n")
1243
- },
1244
- {
1245
- id: "opencode",
1246
- name: "OpenCode",
1247
- skillsDir: join4(opencodeRoot, "skills"),
1248
- detected: existsSync2(opencodeRoot),
1249
- providerHint: [
1250
- "OpenCode provider \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1251
- " \u5728 opencode.json \u7684 provider \u6BB5\u52A0\u5165 openai-compatible \u63D0\u4F9B\u5546\uFF0C",
1252
- ' baseURL = "https://api.focalapi.com/v1"\uFF0CapiKey \u6307\u5411\u4F60\u7684 sk- key\u3002'
1253
- ].join("\n")
1254
- },
1255
- {
1256
- id: "hermes",
1257
- name: "Hermes",
1258
- skillsDir: join4(hermesRoot, "skills"),
1259
- detected: existsSync2(hermesRoot),
1260
- providerHint: [
1261
- "Hermes \u914D\u7F6E\uFF08\u624B\u52A8\uFF09\uFF1A",
1262
- " \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",
1263
- " hermes/profiles/<name>/skills/\u3002provider \u5728 config.yaml \u52A0 openai-compatible",
1264
- ' \u63D0\u4F9B\u5546\uFF0Cbase_url = "https://api.focalapi.com/v1"\u3002'
1265
- ].join("\n")
1266
- }
1267
- ];
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
+ });
1268
1945
  }
1269
1946
  function bundledSkillsDir() {
1270
- if (process.env.FOCALAPI_SKILLS_DIR) {
1271
- return process.env.FOCALAPI_SKILLS_DIR;
1272
- }
1947
+ if (process.env.FOCALAPI_SKILLS_DIR) return normalizeHomePath(process.env.FOCALAPI_SKILLS_DIR);
1273
1948
  const here = dirname(fileURLToPath(import.meta.url));
1274
1949
  const candidates = [join4(here, "..", "skills"), join4(here, "..", "..", "skills")];
1275
1950
  for (const dir of candidates) {
1276
- if (existsSync2(join4(dir, "focalapi", "SKILL.md"))) {
1277
- return dir;
1278
- }
1951
+ if (existsSync2(join4(dir, "focalapi", "SKILL.md"))) return dir;
1279
1952
  }
1280
- 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");
1281
1954
  }
1282
1955
  function listBundledSkills(srcDir) {
1283
1956
  const dir = srcDir ?? bundledSkillsDir();
1284
- 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();
1285
1958
  }
1286
1959
  function manifestPath(skillsDir) {
1287
1960
  return join4(skillsDir, MANIFEST_NAME);
1288
1961
  }
1289
1962
  function readManifest(skillsDir) {
1290
- const path = manifestPath(skillsDir);
1291
- 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;
1292
1999
  try {
1293
- return JSON.parse(readFileSync5(path, "utf-8"));
2000
+ return Object.values(loadConfig().profiles).some((profile) => Boolean(profile.apiKey));
1294
2001
  } catch {
1295
- return void 0;
2002
+ return false;
1296
2003
  }
1297
2004
  }
1298
- function installTo(target, skills, srcDir) {
1299
- mkdirSync2(target.skillsDir, { recursive: true });
1300
- for (const skill of skills) {
1301
- cpSync(join4(srcDir, skill), join4(target.skillsDir, skill), { recursive: true });
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]);
1302
2010
  }
1303
- const manifest = {
1304
- tool: "focalapi-cli",
1305
- version: VERSION,
1306
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1307
- skills
1308
- };
1309
- writeFileSync2(manifestPath(target.skillsDir), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
1310
- return manifest;
2011
+ return [...grouped.values()].map((agents) => ({ agents, skillsDir: agents[0].skillsDir }));
1311
2012
  }
1312
- function uninstallFrom(target) {
1313
- const manifest = readManifest(target.skillsDir);
1314
- if (!manifest) {
1315
- return { removed: [], hadManifest: false };
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");
1316
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 });
2090
+ }
2091
+ return { agents, skillsDir, skills, version: VERSION };
2092
+ }
2093
+ function uninstallFrom(skillsDir, srcDir) {
2094
+ const manifest = readManifest(skillsDir);
2095
+ if (!manifest) return { removed: [], preserved: [], hadManifest: false };
1317
2096
  const removed = [];
1318
- for (const skill of manifest.skills) {
1319
- const dir = join4(target.skillsDir, skill);
1320
- if (skill.startsWith("focalapi") && existsSync2(dir)) {
1321
- rmSync(dir, { recursive: true, force: true });
1322
- removed.push(skill);
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;
1323
2124
  }
2125
+ const expected = manifest.digests?.[skill];
2126
+ if (!expected || digestDirectory(dir) !== expected) modified.push(skill);
2127
+ }
2128
+ return {
2129
+ installed: true,
2130
+ valid: missing.length === 0 && modified.length === 0,
2131
+ version: manifest.version,
2132
+ missing,
2133
+ modified
2134
+ };
2135
+ }
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");
1324
2140
  }
1325
- rmSync(manifestPath(target.skillsDir), { force: true });
1326
- return { removed, hadManifest: true };
2141
+ return {
2142
+ id: "custom",
2143
+ name: "Custom Skills Directory",
2144
+ skillsDir,
2145
+ detected: true
2146
+ };
1327
2147
  }
1328
- function resolveTargets(targetIds, g) {
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");
2152
+ }
2153
+ return [customTarget(path)];
2154
+ }
1329
2155
  const all = getTargets();
1330
- if (targetIds && targetIds.length > 0) {
1331
- 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));
1332
2158
  if (unknown.length > 0) {
1333
2159
  throw new ApiError("invalid_request", `\u672A\u77E5 Agent\uFF1A${unknown.join(", ")}`, {
1334
- 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`
1335
2161
  });
1336
2162
  }
1337
- return all.filter((t) => targetIds.includes(t.id));
2163
+ return all.filter((target) => targetIds.includes(target.id));
1338
2164
  }
1339
- const detected = all.filter((t) => t.detected);
2165
+ const detected = all.filter((target) => target.detected);
1340
2166
  if (detected.length === 0) {
1341
- throw new ApiError("invalid_request", "\u672A\u68C0\u6D4B\u5230\u4EFB\u4F55\u672C\u673A Agent", {
1342
- 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"
1343
2169
  });
1344
2170
  }
1345
- if (!g.json) {
1346
- info(`\u68C0\u6D4B\u5230 ${detected.length} \u4E2A Agent\uFF1A${detected.map((t) => t.name).join("\u3001")}`);
1347
- }
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`);
1348
2172
  return detected;
1349
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
+ }
1350
2205
  function registerConnect(program) {
1351
- const connect = program.command("connect").description("\u628A focalapi \u80FD\u529B\u6CE8\u5165\u672C\u673A AI Agent\uFF08skills \u5B89\u88C5/\u5378\u8F7D\uFF09");
1352
- 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) => {
1353
2210
  const g = cmd.optsWithGlobals();
1354
- const rows = getTargets().map((t) => {
1355
- const manifest = readManifest(t.skillsDir);
2211
+ const rows = getTargets().map((target) => {
2212
+ const status = verifyTarget(target.skillsDir);
1356
2213
  return {
1357
- id: t.id,
1358
- name: t.name,
1359
- detected: t.detected,
1360
- skillsDir: t.skillsDir,
1361
- 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
1362
2221
  };
1363
2222
  });
1364
2223
  if (g.json) {
1365
- printJson({ agents: rows });
2224
+ printJson({ supported: rows.length, agents: rows });
1366
2225
  } else {
1367
2226
  printTable(
1368
- ["Agent", "ID", "\u68C0\u6D4B\u5230", "\u5DF2\u5B89\u88C5", "\u6280\u80FD\u76EE\u5F55"],
1369
- 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
+ ])
1370
2235
  );
1371
2236
  }
1372
2237
  });
1373
- 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) => {
1374
2242
  const g = cmd.optsWithGlobals();
1375
- const targets = resolveTargets(targetIds, g);
1376
- const srcDir = bundledSkillsDir();
1377
- const skills = listBundledSkills(srcDir);
1378
- 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
+ };
1379
2262
  if (g.json) {
1380
- printJson({
1381
- installed: results.map((r) => ({ agent: r.target.id, skillsDir: r.target.skillsDir, skills: r.manifest.skills }))
1382
- });
1383
- return;
1384
- }
1385
- for (const { target, manifest } of results) {
1386
- info(`\u2713 ${target.name}\uFF1A${manifest.skills.length} \u4E2A\u6280\u80FD\u5DF2\u88C5\u5165 ${target.skillsDir}`);
1387
- }
1388
- info("");
1389
- 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");
1390
- for (const { target } of results) {
1391
- info("");
1392
- 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}`);
1393
2270
  }
1394
- info("");
1395
- 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");
1396
- info("\u5378\u8F7D\uFF1Afocalapi connect uninstall");
2271
+ if (!result.ready) process.exitCode = 1;
1397
2272
  });
1398
- 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) => {
1399
2274
  const g = cmd.optsWithGlobals();
1400
- const targets = resolveTargets(targetIds, g);
1401
- 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
+ }));
1402
2282
  if (g.json) {
1403
- printJson({
1404
- uninstalled: results.map((r) => ({ agent: r.target.id, removed: r.removed, hadManifest: r.hadManifest }))
1405
- });
2283
+ printJson({ uninstalled: results });
1406
2284
  return;
1407
2285
  }
1408
- for (const r of results) {
1409
- if (!r.hadManifest) {
1410
- info(`- ${r.target.name}\uFF1A\u65E0 focalapi \u5B89\u88C5\u8BB0\u5F55\uFF0C\u8DF3\u8FC7`);
1411
- } else {
1412
- info(`\u2713 ${r.target.name}\uFF1A\u5DF2\u79FB\u9664 ${r.removed.length} \u4E2A\u6280\u80FD`);
1413
- }
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`);
1414
2288
  }
1415
2289
  });
1416
2290
  }
@@ -1513,16 +2387,13 @@ function registerRequest(program) {
1513
2387
  // src/cli.ts
1514
2388
  function buildProgram() {
1515
2389
  const program = new Command();
1516
- program.name("focalapi").description("\u8FDE\u63A5 focalapi \u4E0E AI Agent \u7684\u547D\u4EE4\u884C\u5DE5\u5177\uFF1A\u5BF9\u8BDD\u3001\u56FE\u50CF\u3001\u89C6\u9891\u3001\u641C\u7D22\u3001\u97F3\u9891\u3001\u7528\u91CF\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");
1517
2391
  registerAuth(program);
1518
2392
  registerModels(program);
1519
2393
  registerChat(program);
1520
2394
  registerGen(program);
1521
2395
  registerTask(program);
1522
- registerSearch(program);
1523
2396
  registerAudio(program);
1524
- registerEmbed(program);
1525
- registerRerank(program);
1526
2397
  registerUsage(program);
1527
2398
  registerDoctor(program);
1528
2399
  registerConnect(program);
@@ -1531,10 +2402,11 @@ function buildProgram() {
1531
2402
  return program;
1532
2403
  }
1533
2404
  async function main(argv = process.argv) {
2405
+ process.exitCode = 0;
1534
2406
  const program = buildProgram();
1535
2407
  try {
1536
2408
  await program.parseAsync(argv);
1537
- return 0;
2409
+ return typeof process.exitCode === "number" ? process.exitCode : 0;
1538
2410
  } catch (err) {
1539
2411
  let json = false;
1540
2412
  try {