focalapi-cli 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/cli.js +131 -17
- package/package.json +1 -1
- package/skills/focalapi/SKILL.md +3 -3
- package/skills/focalapi-task/SKILL.md +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-08-18
|
|
4
|
+
|
|
5
|
+
- Added `task status --wait [--download]`: built-in bounded polling with elapsed-time reporting and an optional auto-download, so agents never need to write their own poller (the audited agent session's custom script crashed on the Windows `.cmd` shim and stalled blind).
|
|
6
|
+
- Added `task list [--status] [--action] [--limit] [--offset]` backed by `GET /v1/tasks`, so a caller that lost a submission output can reconcile recent tasks instead of resubmitting and double-charging.
|
|
7
|
+
- Added `--idempotency-key` to `gen video` and async `gen image`: same-key retries replay the original task server-side without a second charge. Keys are auto-generated when omitted, breadcrumbed on stderr (`idempotency_key=...`), and the server's `idempotent_replay` marker is surfaced in both JSON output and stderr.
|
|
8
|
+
- Aligned `task download --json` output with `gen image` by exposing `files[]` alongside the legacy `file`.
|
|
9
|
+
- Added a stderr warning for double-encoded reference URLs (`%25XX`) before submission — the top cause of 403 `invalid_reference_url` from presigned URLs.
|
|
10
|
+
|
|
3
11
|
## 0.3.1 - 2026-08-18
|
|
4
12
|
|
|
5
13
|
- Added `--duration` as an alias of `--seconds` on `gen video` so the API contract field name works directly; conflicting values are rejected locally.
|
package/dist/cli.js
CHANGED
|
@@ -176,7 +176,7 @@ function displayWidth(s) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/lib/version.ts
|
|
179
|
-
var VERSION = true ? "0.
|
|
179
|
+
var VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
180
180
|
|
|
181
181
|
// src/commands/auth.ts
|
|
182
182
|
import { createInterface } from "readline/promises";
|
|
@@ -916,6 +916,7 @@ import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
|
916
916
|
import { join as join3, resolve as resolve2 } from "path";
|
|
917
917
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
918
918
|
import { Readable as Readable2 } from "stream";
|
|
919
|
+
import { randomUUID } from "crypto";
|
|
919
920
|
|
|
920
921
|
// src/lib/model-capabilities.ts
|
|
921
922
|
var GEMINI_IMAGE_MAX_SEED = 9007199254740991;
|
|
@@ -1526,6 +1527,19 @@ function extractProgress(body) {
|
|
|
1526
1527
|
}
|
|
1527
1528
|
return void 0;
|
|
1528
1529
|
}
|
|
1530
|
+
function extractCreatedAt(raw) {
|
|
1531
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1532
|
+
const obj = raw;
|
|
1533
|
+
const candidates = [obj.created_at, obj.data?.created_at];
|
|
1534
|
+
for (const candidate of candidates) {
|
|
1535
|
+
if (typeof candidate === "number" && candidate > 0) return candidate;
|
|
1536
|
+
if (typeof candidate === "string") {
|
|
1537
|
+
const parsed = Number.parseInt(candidate, 10);
|
|
1538
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return void 0;
|
|
1542
|
+
}
|
|
1529
1543
|
async function cancelTask(baseUrl, apiKey, taskId) {
|
|
1530
1544
|
try {
|
|
1531
1545
|
await request({
|
|
@@ -1588,9 +1602,24 @@ async function fetchTask(baseUrl, apiKey, taskId) {
|
|
|
1588
1602
|
status: normalizeTaskStatus(rawStatus),
|
|
1589
1603
|
rawStatus,
|
|
1590
1604
|
progress: extractProgress(raw),
|
|
1605
|
+
createdAt: extractCreatedAt(raw),
|
|
1591
1606
|
raw
|
|
1592
1607
|
};
|
|
1593
1608
|
}
|
|
1609
|
+
async function listTasks(baseUrl, apiKey, opts) {
|
|
1610
|
+
const raw = await request({
|
|
1611
|
+
baseUrl,
|
|
1612
|
+
path: "/v1/tasks",
|
|
1613
|
+
apiKey,
|
|
1614
|
+
query: {
|
|
1615
|
+
status: opts?.status,
|
|
1616
|
+
action: opts?.action,
|
|
1617
|
+
limit: opts?.limit,
|
|
1618
|
+
offset: opts?.offset
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
return raw.data ?? [];
|
|
1622
|
+
}
|
|
1594
1623
|
async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
1595
1624
|
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
1596
1625
|
const timeoutMs = opts?.timeoutMs ?? 30 * 6e4;
|
|
@@ -1615,7 +1644,7 @@ async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
|
1615
1644
|
}
|
|
1616
1645
|
if (Date.now() > deadline) {
|
|
1617
1646
|
throw new ApiError("timeout", `\u4EFB\u52A1 ${taskId} \u7B49\u5F85\u8D85\u65F6\uFF08${Math.round(timeoutMs / 6e4)} \u5206\u949F\uFF09`, {
|
|
1618
|
-
hint: `\u53EF\
|
|
1647
|
+
hint: `\u53EF\u8FD0\u884C focalapi task status ${taskId} --wait \u7EE7\u7EED\u7B49\u5F85\uFF0C\u6216 focalapi task status ${taskId} \u67E5\u770B\u5F53\u524D\u72B6\u6001\u3002`
|
|
1619
1648
|
});
|
|
1620
1649
|
}
|
|
1621
1650
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
@@ -1653,6 +1682,20 @@ async function downloadTaskContent(baseUrl, apiKey, taskId, outDir, filenameBase
|
|
|
1653
1682
|
var MAX_IMAGE_N = 128;
|
|
1654
1683
|
var MAX_TASK_DURATION_SECONDS = 3600;
|
|
1655
1684
|
var DEFAULT_OUT_DIR = "focalapi-out";
|
|
1685
|
+
function resolveIdempotencyKey(provided) {
|
|
1686
|
+
const key = provided?.trim() || randomUUID();
|
|
1687
|
+
if (!/^[\x21-\x7e]{8,128}$/.test(key)) {
|
|
1688
|
+
throw new ApiError("invalid_request", "--idempotency-key \u5FC5\u987B\u662F 8\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF08\u6536\u5230\uFF1A" + provided + "\uFF09");
|
|
1689
|
+
}
|
|
1690
|
+
return key;
|
|
1691
|
+
}
|
|
1692
|
+
function warnDoubleEncodedReferenceURLs(label, urls) {
|
|
1693
|
+
for (const url of urls) {
|
|
1694
|
+
if (url && /%25[0-9a-f]{2}/i.test(url)) {
|
|
1695
|
+
info(`\u8B66\u544A\uFF1A${label} \u7684 URL \u7591\u4F3C\u88AB\u4E8C\u6B21\u7F16\u7801\uFF08\u5305\u542B %25XX\uFF09\uFF1A${url.slice(0, 100)} \u2014\u2014 \u9884\u7B7E\u540D URL \u5FC5\u987B\u539F\u6837\u4F20\u9012\uFF0C\u5426\u5219\u4E0A\u6E38\u4F1A\u8FD4\u56DE 403 invalid_reference_url`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1656
1699
|
function clampInt(value, min, max, name) {
|
|
1657
1700
|
if (!Number.isInteger(value) || value < min || value > max) {
|
|
1658
1701
|
throw new ApiError("invalid_request", `${name} \u5FC5\u987B\u662F ${min}\u2013${max} \u7684\u6574\u6570\uFF08\u6536\u5230\uFF1A${value}\uFF09`);
|
|
@@ -1769,7 +1812,7 @@ function extractGeminiImageItems(response) {
|
|
|
1769
1812
|
}
|
|
1770
1813
|
function registerGen(program) {
|
|
1771
1814
|
const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
|
|
1772
|
-
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>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u6A21\u578B\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\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("--negative-prompt <text>", "\u8D1F\u9762\u63D0\u793A\u8BCD\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--creativity <level>", "\u63D0\u793A\u8BCD\u6269\u5C55\u5F3A\u5EA6\uFF0C\u5982 raw\u3001low\u3001medium\u3001high").option("--prompt-extend <boolean>", "\u662F\u5426\u6269\u5C55\u63D0\u793A\u8BCD\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "prompt-extend")).option("--style-references <json>", "Krea image_style_references JSON \u6570\u7EC4").option("--moodboards <json>", "Krea moodboards JSON \u6570\u7EC4").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\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) => {
|
|
1815
|
+
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>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u6A21\u578B\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\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("--negative-prompt <text>", "\u8D1F\u9762\u63D0\u793A\u8BCD\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--creativity <level>", "\u63D0\u793A\u8BCD\u6269\u5C55\u5F3A\u5EA6\uFF0C\u5982 raw\u3001low\u3001medium\u3001high").option("--prompt-extend <boolean>", "\u662F\u5426\u6269\u5C55\u63D0\u793A\u8BCD\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "prompt-extend")).option("--style-references <json>", "Krea image_style_references JSON \u6570\u7EC4").option("--moodboards <json>", "Krea moodboards JSON \u6570\u7EC4").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\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("--idempotency-key <key>", "\u5E42\u7B49\u952E\uFF088\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09\u3002\u540C\u4E00 key \u7684\u91CD\u590D\u63D0\u4EA4\u62FF\u56DE\u539F\u4EFB\u52A1\u800C\u4E0D\u91CD\u590D\u8BA1\u8D39\uFF1B\u7701\u7565\u65F6\u81EA\u52A8\u751F\u6210\u3002\u91CD\u8BD5\u4E0D\u786E\u5B9A\u7684\u63D0\u4EA4\u65F6\u52A1\u5FC5\u590D\u7528\u539F key").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
|
|
1773
1816
|
const g = cmd.optsWithGlobals();
|
|
1774
1817
|
const auth = resolveAuth(g);
|
|
1775
1818
|
const model = opts.model ?? (await resolveCreativeModel(auth, "image")).model.id;
|
|
@@ -1818,12 +1861,17 @@ function registerGen(program) {
|
|
|
1818
1861
|
if (opts.image) body.image = opts.image;
|
|
1819
1862
|
if (opts.mask) body.mask = opts.mask;
|
|
1820
1863
|
if (opts.responseFormat) body.response_format = opts.responseFormat;
|
|
1864
|
+
const idempotencyKey = opts.wait === false ? resolveIdempotencyKey(opts.idempotencyKey) : void 0;
|
|
1865
|
+
warnDoubleEncodedReferenceURLs("--image", opts.image ?? []);
|
|
1821
1866
|
const res = await withProgress(opts.wait === false ? "\u6B63\u5728\u63D0\u4EA4\u56FE\u50CF\u4EFB\u52A1" : "\u6B63\u5728\u751F\u6210\u56FE\u50CF", () => request({
|
|
1822
1867
|
baseUrl: auth.baseUrl,
|
|
1823
1868
|
path: "/v1/images/generations",
|
|
1824
1869
|
apiKey: auth.apiKey,
|
|
1825
1870
|
body,
|
|
1826
|
-
headers:
|
|
1871
|
+
headers: {
|
|
1872
|
+
...opts.wait === false ? { Prefer: "respond-async" } : {},
|
|
1873
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
1874
|
+
},
|
|
1827
1875
|
timeoutMs: 6e5
|
|
1828
1876
|
}));
|
|
1829
1877
|
if (opts.wait === false) {
|
|
@@ -1832,8 +1880,9 @@ function registerGen(program) {
|
|
|
1832
1880
|
throw new ApiError("bad_response", "\u5F02\u6B65\u56FE\u50CF\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: res });
|
|
1833
1881
|
}
|
|
1834
1882
|
info(`task_id=${taskId}`);
|
|
1883
|
+
if (idempotencyKey) info(`idempotency_key=${idempotencyKey}`);
|
|
1835
1884
|
if (g.json) {
|
|
1836
|
-
printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
1885
|
+
printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, ...res.idempotent_replay ? { idempotent_replay: true } : {}, next_command: `focalapi task status ${taskId} --json` });
|
|
1837
1886
|
} else {
|
|
1838
1887
|
process.stdout.write(taskId + "\n");
|
|
1839
1888
|
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u67E5\u8BE2\uFF1Afocalapi task status ${taskId}`);
|
|
@@ -1958,7 +2007,7 @@ function registerGen(program) {
|
|
|
1958
2007
|
if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
|
|
1959
2008
|
}
|
|
1960
2009
|
});
|
|
1961
|
-
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--duration \u4E3A\u540C\u4E49\u522B\u540D\u3002\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--duration <n>", "\u65F6\u957F\u79D2\u6570\uFF08--seconds \u7684\u522B\u540D\uFF0C\u4E0E API \u5951\u7EA6\u5B57\u6BB5\u540C\u540D\uFF09", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u53C2\u8003\u56FE URL\uFF08Grok \u89C6\u9891\u4E3A reference-to-video \u6A21\u5F0F\uFF0C\u53EF\u591A\u4E2A\uFF09").option("--first-frame <url>", "\u56FE\u751F\u89C6\u9891\u9996\u5E27\u56FE URL\uFF08image-to-video \u6A21\u5F0F\uFF1B\u4E0E --image \u4E92\u65A5\uFF09").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(
|
|
2010
|
+
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--duration \u4E3A\u540C\u4E49\u522B\u540D\u3002\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--duration <n>", "\u65F6\u957F\u79D2\u6570\uFF08--seconds \u7684\u522B\u540D\uFF0C\u4E0E API \u5951\u7EA6\u5B57\u6BB5\u540C\u540D\uFF09", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u53C2\u8003\u56FE URL\uFF08Grok \u89C6\u9891\u4E3A reference-to-video \u6A21\u5F0F\uFF0C\u53EF\u591A\u4E2A\uFF09").option("--first-frame <url>", "\u56FE\u751F\u89C6\u9891\u9996\u5E27\u56FE URL\uFF08image-to-video \u6A21\u5F0F\uFF1B\u4E0E --image \u4E92\u65A5\uFF09").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("--idempotency-key <key>", "\u5E42\u7B49\u952E\uFF088\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09\u3002\u540C\u4E00 key \u7684\u91CD\u590D\u63D0\u4EA4\u62FF\u56DE\u539F\u4EFB\u52A1\u800C\u4E0D\u91CD\u590D\u8BA1\u8D39\uFF1B\u7701\u7565\u65F6\u81EA\u52A8\u751F\u6210\u3002\u91CD\u8BD5\u4E0D\u786E\u5B9A\u7684\u63D0\u4EA4\u65F6\u52A1\u5FC5\u590D\u7528\u539F key").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(
|
|
1962
2011
|
async (promptParts, opts, cmd) => {
|
|
1963
2012
|
const g = cmd.optsWithGlobals();
|
|
1964
2013
|
const auth = resolveAuth(g);
|
|
@@ -2010,11 +2059,14 @@ function registerGen(program) {
|
|
|
2010
2059
|
watermark: opts.watermark
|
|
2011
2060
|
});
|
|
2012
2061
|
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
2062
|
+
const idempotencyKey = resolveIdempotencyKey(opts.idempotencyKey);
|
|
2063
|
+
warnDoubleEncodedReferenceURLs("--image/--first-frame", [...opts.image ?? [], opts.firstFrame]);
|
|
2013
2064
|
const created = await withProgress("\u6B63\u5728\u63D0\u4EA4\u89C6\u9891\u4EFB\u52A1", () => request({
|
|
2014
2065
|
baseUrl: auth.baseUrl,
|
|
2015
2066
|
path: "/v1/video/generations",
|
|
2016
2067
|
apiKey: auth.apiKey,
|
|
2017
2068
|
body,
|
|
2069
|
+
headers: { "Idempotency-Key": idempotencyKey },
|
|
2018
2070
|
timeoutMs: 12e4
|
|
2019
2071
|
}));
|
|
2020
2072
|
const taskId = extractTaskId(created);
|
|
@@ -2023,8 +2075,10 @@ function registerGen(program) {
|
|
|
2023
2075
|
}
|
|
2024
2076
|
if (opts.wait === false) {
|
|
2025
2077
|
info(`task_id=${taskId}`);
|
|
2078
|
+
info(`idempotency_key=${idempotencyKey}`);
|
|
2079
|
+
if (created.idempotent_replay) info("\u5DF2\u56DE\u653E\u539F\u4EFB\u52A1\uFF08idempotent_replay\uFF0C\u672A\u91CD\u590D\u8BA1\u8D39\uFF09");
|
|
2026
2080
|
if (g.json) {
|
|
2027
|
-
printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
2081
|
+
printJson({ model, task_id: taskId, submitted: true, ...created.idempotent_replay ? { idempotent_replay: true } : {}, next_command: `focalapi task status ${taskId} --json` });
|
|
2028
2082
|
} else {
|
|
2029
2083
|
process.stdout.write(taskId + "\n");
|
|
2030
2084
|
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}\uFF1B\u6392\u961F\u4E2D\u53EF\u53D6\u6D88\uFF1Afocalapi task cancel ${taskId}`);
|
|
@@ -2052,42 +2106,102 @@ function registerGen(program) {
|
|
|
2052
2106
|
}
|
|
2053
2107
|
|
|
2054
2108
|
// src/commands/task.ts
|
|
2055
|
-
var TASK_ID_HINT = "task_id \u533A\u5206\u5927\u5C0F\u5199\u4E14\u6DF7\u6709\u5C0F\u5199 l / \u6570\u5B57 1 / \u5927\u5199 I\uFF08O \u4E0E 0 \u540C\u7406\uFF09\uFF0C\u5FC5\u987B\u9010\u5B57\u590D\u5236\u3002\u82E5\u63D0\u4EA4\u65F6\u7684\u8F93\u51FA\u5DF2\u4E22\u5931\uFF0C\u5148\
|
|
2109
|
+
var TASK_ID_HINT = "task_id \u533A\u5206\u5927\u5C0F\u5199\u4E14\u6DF7\u6709\u5C0F\u5199 l / \u6570\u5B57 1 / \u5927\u5199 I\uFF08O \u4E0E 0 \u540C\u7406\uFF09\uFF0C\u5FC5\u987B\u9010\u5B57\u590D\u5236\u3002\u82E5\u63D0\u4EA4\u65F6\u7684\u8F93\u51FA\u5DF2\u4E22\u5931\uFF0C\u5148\u8FD0\u884C focalapi task list \u627E\u56DE\u6700\u8FD1\u7684\u4EFB\u52A1\uFF0C\u4E0D\u8981\u76F2\u76EE\u91CD\u65B0\u63D0\u4EA4\u2014\u2014\u91CD\u590D\u63D0\u4EA4\u4F1A\u91CD\u590D\u6263\u8D39\u3002";
|
|
2056
2110
|
function withTaskIdHint(err) {
|
|
2057
2111
|
if (err instanceof ApiError && err.status === 404) {
|
|
2058
2112
|
return new ApiError(err.code, err.message, { status: err.status, hint: TASK_ID_HINT, body: err.body, upstreamCode: err.upstreamCode, requestId: err.requestId });
|
|
2059
2113
|
}
|
|
2060
2114
|
return err;
|
|
2061
2115
|
}
|
|
2116
|
+
function formatElapsed(seconds) {
|
|
2117
|
+
if (!seconds || seconds < 0) return "-";
|
|
2118
|
+
if (seconds < 90) return `${seconds} \u79D2`;
|
|
2119
|
+
return `${Math.floor(seconds / 60)} \u5206 ${seconds % 60} \u79D2`;
|
|
2120
|
+
}
|
|
2121
|
+
function taskAgeSeconds(createdAt) {
|
|
2122
|
+
if (!createdAt) return void 0;
|
|
2123
|
+
return Math.max(0, Math.floor(Date.now() / 1e3 - createdAt));
|
|
2124
|
+
}
|
|
2062
2125
|
function registerTask(program) {
|
|
2063
2126
|
const task = program.command("task").description("\u4EFB\u52A1\u67E5\u8BE2\u3001\u53D6\u6D88\u4E0E\u4EA7\u7269\u4E0B\u8F7D\uFF08\u89C6\u9891\u7B49\u4EFB\u52A1\u5236\u80FD\u529B\uFF09");
|
|
2064
|
-
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId,
|
|
2127
|
+
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001\uFF1B--wait \u5185\u7F6E\u8F6E\u8BE2\u7B49\u5F85\u7EC8\u6001\uFF0C\u65E0\u9700\u81EA\u5199\u8F6E\u8BE2\u811A\u672C").argument("<task_id>", "\u4EFB\u52A1 ID").option("--wait", "\u7B49\u5F85\u4EFB\u52A1\u5230\u8FBE\u7EC8\u6001\uFF08\u6210\u529F / \u5931\u8D25 / \u53D6\u6D88\uFF09\u540E\u518D\u8FD4\u56DE").option("--timeout <minutes>", "--wait \u7684\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("--poll-interval <ms>", "--wait \u7684\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--download", "--wait \u6210\u529F\u540E\u81EA\u52A8\u4E0B\u8F7D\u4EA7\u7269\uFF08\u7B49\u4EF7\u4E8E\u63A5\u7740\u6267\u884C task download\uFF09").option("-o, --out <dir>", "--download \u7684\u8F93\u51FA\u76EE\u5F55", "focalapi-out").action(async (taskId, opts, cmd) => {
|
|
2065
2128
|
const g = cmd.optsWithGlobals();
|
|
2066
2129
|
const auth = resolveAuth(g);
|
|
2067
2130
|
try {
|
|
2068
|
-
|
|
2131
|
+
let info_ = await fetchTask(auth.baseUrl, auth.apiKey, taskId);
|
|
2132
|
+
if (opts.wait && info_.status !== "success" && info_.status !== "failed" && info_.status !== "cancelled") {
|
|
2133
|
+
info_ = await pollTask(auth.baseUrl, auth.apiKey, taskId, {
|
|
2134
|
+
intervalMs: opts.pollInterval,
|
|
2135
|
+
timeoutMs: opts.timeout * 6e4,
|
|
2136
|
+
onUpdate: (t) => {
|
|
2137
|
+
if (!g.json) {
|
|
2138
|
+
const elapsed2 = formatElapsed(taskAgeSeconds(t.createdAt));
|
|
2139
|
+
info(` \u72B6\u6001\uFF1A${t.rawStatus || t.status}${t.progress !== void 0 ? `\uFF08${t.progress}%\uFF09` : ""}\uFF0C\u5DF2\u8017\u65F6 ${elapsed2}`);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
const elapsed = taskAgeSeconds(info_.createdAt);
|
|
2145
|
+
let file;
|
|
2146
|
+
if (opts.download && info_.status === "success") {
|
|
2147
|
+
file = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
|
|
2148
|
+
}
|
|
2069
2149
|
if (g.json) {
|
|
2070
|
-
printJson({
|
|
2150
|
+
printJson({
|
|
2151
|
+
task_id: taskId,
|
|
2152
|
+
status: info_.status,
|
|
2153
|
+
raw_status: info_.rawStatus,
|
|
2154
|
+
progress: info_.progress,
|
|
2155
|
+
...elapsed !== void 0 ? { elapsed_seconds: elapsed } : {},
|
|
2156
|
+
...file ? { file } : {},
|
|
2157
|
+
raw: info_.raw
|
|
2158
|
+
});
|
|
2071
2159
|
} else {
|
|
2072
2160
|
printTable(
|
|
2073
2161
|
["\u5B57\u6BB5", "\u503C"],
|
|
2074
2162
|
[
|
|
2075
2163
|
["\u4EFB\u52A1 ID", taskId],
|
|
2076
2164
|
["\u72B6\u6001", `${info_.status}${info_.rawStatus && info_.rawStatus !== info_.status ? `\uFF08\u4E0A\u6E38\uFF1A${info_.rawStatus}\uFF09` : ""}`],
|
|
2077
|
-
["\u8FDB\u5EA6", info_.progress !== void 0 ? `${info_.progress}%` : "-"]
|
|
2165
|
+
["\u8FDB\u5EA6", info_.progress !== void 0 ? `${info_.progress}%` : "-"],
|
|
2166
|
+
["\u5DF2\u8017\u65F6", formatElapsed(elapsed)]
|
|
2078
2167
|
]
|
|
2079
2168
|
);
|
|
2080
|
-
if (
|
|
2169
|
+
if (file) {
|
|
2170
|
+
info(`\u2713 ${file}`);
|
|
2171
|
+
} else if (info_.status === "success") {
|
|
2081
2172
|
info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
|
|
2082
2173
|
}
|
|
2083
2174
|
if (info_.status === "pending" || info_.status === "running") {
|
|
2084
|
-
info(`\
|
|
2175
|
+
info(`\u7B49\u5F85\u5B8C\u6210\uFF1Afocalapi task status ${taskId} --wait\uFF1B\u6392\u961F\u4E2D\u53EF\u53D6\u6D88\uFF1Afocalapi task cancel ${taskId}`);
|
|
2085
2176
|
}
|
|
2086
2177
|
}
|
|
2087
2178
|
} catch (err) {
|
|
2088
2179
|
throw withTaskIdHint(err);
|
|
2089
2180
|
}
|
|
2090
2181
|
});
|
|
2182
|
+
task.command("list").description("\u5217\u51FA\u5F53\u524D Key \u7684\u8FD1\u671F\u4EFB\u52A1\uFF08\u63D0\u4EA4\u8F93\u51FA\u4E22\u5931\u65F6\u7528\u5B83\u627E\u56DE task_id\uFF09").option("--status <status>", "\u6309\u72B6\u6001\u8FC7\u6EE4\uFF1Aqueued\u3001in_progress\u3001completed\u3001failed\u3001cancelled").option("--action <action>", "\u6309\u4EFB\u52A1\u7C7B\u578B\u8FC7\u6EE4\uFF0C\u5982 generate\u3001image_generation").option("--limit <n>", "\u6BCF\u9875\u6570\u91CF\uFF081\u2013100\uFF09", (v) => Number.parseInt(v, 10), 20).option("--offset <n>", "\u504F\u79FB\u91CF", (v) => Number.parseInt(v, 10), 0).action(async (opts, cmd) => {
|
|
2183
|
+
const g = cmd.optsWithGlobals();
|
|
2184
|
+
const auth = resolveAuth(g);
|
|
2185
|
+
const items = await listTasks(auth.baseUrl, auth.apiKey, opts);
|
|
2186
|
+
if (g.json) {
|
|
2187
|
+
printJson({ object: "list", data: items });
|
|
2188
|
+
} else if (items.length === 0) {
|
|
2189
|
+
info("\u5F53\u524D Key \u6682\u65E0\u4EFB\u52A1\u8BB0\u5F55\u3002");
|
|
2190
|
+
} else {
|
|
2191
|
+
printTable(
|
|
2192
|
+
["\u4EFB\u52A1 ID", "\u6A21\u578B", "\u72B6\u6001", "\u8FDB\u5EA6", "\u5DF2\u8017\u65F6", "\u989D\u5EA6"],
|
|
2193
|
+
items.map((item) => [
|
|
2194
|
+
item.task_id,
|
|
2195
|
+
item.model ?? "-",
|
|
2196
|
+
item.status ?? "-",
|
|
2197
|
+
item.progress !== void 0 ? `${item.progress}%` : "-",
|
|
2198
|
+
formatElapsed(item.created_at ? Math.max(0, Math.floor(Date.now() / 1e3 - item.created_at)) : void 0),
|
|
2199
|
+
item.quota !== void 0 ? String(item.quota) : "-"
|
|
2200
|
+
])
|
|
2201
|
+
);
|
|
2202
|
+
info("\u7EED\u53D6\uFF1Afocalapi task status <task_id> --wait");
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2091
2205
|
task.command("cancel").description("\u53D6\u6D88\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF08\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4E0D\u53EF\u53D6\u6D88\uFF1B\u53D6\u6D88\u540E\u8D39\u7528\u81EA\u52A8\u9000\u8FD8\uFF09").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId, _opts, cmd) => {
|
|
2092
2206
|
const g = cmd.optsWithGlobals();
|
|
2093
2207
|
const auth = resolveAuth(g);
|
|
@@ -2103,7 +2217,7 @@ function registerTask(program) {
|
|
|
2103
2217
|
const auth = resolveAuth(g);
|
|
2104
2218
|
const filePath = await downloadTaskContent(auth.baseUrl, auth.apiKey, taskId, opts.out);
|
|
2105
2219
|
if (g.json) {
|
|
2106
|
-
printJson({ task_id: taskId, file: filePath });
|
|
2220
|
+
printJson({ task_id: taskId, file: filePath, files: [filePath] });
|
|
2107
2221
|
} else {
|
|
2108
2222
|
info(`\u2713 ${filePath}`);
|
|
2109
2223
|
}
|
|
@@ -2210,7 +2324,7 @@ function registerUsage(program) {
|
|
|
2210
2324
|
}
|
|
2211
2325
|
|
|
2212
2326
|
// src/commands/connect.ts
|
|
2213
|
-
import { createHash, randomUUID } from "crypto";
|
|
2327
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2214
2328
|
import {
|
|
2215
2329
|
cpSync,
|
|
2216
2330
|
existsSync as existsSync2,
|
|
@@ -2369,7 +2483,7 @@ function installTo(skillsDir, agents, skills, srcDir) {
|
|
|
2369
2483
|
}
|
|
2370
2484
|
mkdirSync2(skillsDir, { recursive: true });
|
|
2371
2485
|
const oldManifest = readManifest(skillsDir);
|
|
2372
|
-
const transactionRoot = join4(skillsDir, `.focalapi-install-${
|
|
2486
|
+
const transactionRoot = join4(skillsDir, `.focalapi-install-${randomUUID2()}`);
|
|
2373
2487
|
const stageRoot = join4(transactionRoot, "stage");
|
|
2374
2488
|
const backupRoot = join4(transactionRoot, "backup");
|
|
2375
2489
|
mkdirSync2(stageRoot, { recursive: true });
|
package/package.json
CHANGED
package/skills/focalapi/SKILL.md
CHANGED
|
@@ -26,8 +26,8 @@ focalapi gen image "<user prompt>" -o ./focalapi-out --json
|
|
|
26
26
|
focalapi gen video "<user prompt>" --no-wait -o ./focalapi-out --json
|
|
27
27
|
|
|
28
28
|
# Continue an asynchronous video task.
|
|
29
|
-
focalapi task status <task-id> --json
|
|
30
|
-
focalapi task
|
|
29
|
+
focalapi task status <task-id> --wait --json # built-in polling; never write your own poll script
|
|
30
|
+
focalapi task list --json # recover recent task IDs after a lost output
|
|
31
31
|
focalapi task cancel <task-id> --json # queued tasks only; cancelled tasks are refunded
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -38,7 +38,7 @@ focalapi task cancel <task-id> --json # queued tasks only; cancelled tasks are
|
|
|
38
38
|
| Generate or edit images; create from reference images | `focalapi gen image` | focalapi-gen |
|
|
39
39
|
| Generate video; animate images or reference media | `focalapi gen video` | focalapi-gen |
|
|
40
40
|
| Select, compare, or inspect model parameters | `focalapi models resolve/get/search` | focalapi-models |
|
|
41
|
-
| Inspect progress or failures; cancel queued tasks; download results | `focalapi task status/cancel/download` | focalapi-task |
|
|
41
|
+
| Inspect progress or failures; wait, list, cancel queued tasks; download results | `focalapi task status/wait/list/cancel/download` | focalapi-task |
|
|
42
42
|
| Resolve key, sign-in, or 401 issues | `focalapi auth status/login` | focalapi-auth |
|
|
43
43
|
| Inspect quota, usage, or service failures | `focalapi usage/doctor` | focalapi-usage |
|
|
44
44
|
| Provide text assistance explicitly requested by the user | `focalapi chat` | focalapi-chat |
|
|
@@ -13,8 +13,10 @@ metadata:
|
|
|
13
13
|
After a generation command returns `task_id`, prefer the response's `next_command`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
focalapi task status <task-id> --json
|
|
17
|
-
focalapi task
|
|
16
|
+
focalapi task status <task-id> --json # one-shot status check
|
|
17
|
+
focalapi task status <task-id> --wait # built-in polling until a terminal state — do not write your own poller
|
|
18
|
+
focalapi task status <task-id> --wait --download -o ./focalapi-out --json
|
|
19
|
+
focalapi task list --status in_progress --json # reconcile recent tasks after losing a submission output
|
|
18
20
|
focalapi task cancel <task-id> --json
|
|
19
21
|
```
|
|
20
22
|
|
|
@@ -24,7 +26,9 @@ focalapi task cancel <task-id> --json
|
|
|
24
26
|
- `cancelled`: the task was stopped; a cancelled queued task is refunded. Do not download or resubmit unless the user asks for a new attempt.
|
|
25
27
|
- `unknown`: preserve the raw response and run `focalapi doctor --json`; never fabricate a success state.
|
|
26
28
|
|
|
27
|
-
Task IDs are case-sensitive and mix `l`/`1`/`I` and `O`/`0`. Copy them exactly from the submission output — stdout JSON or the stderr `task_id=` breadcrumb — instead of retyping.
|
|
29
|
+
Task IDs are case-sensitive and mix `l`/`1`/`I` and `O`/`0`. Copy them exactly from the submission output — stdout JSON or the stderr `task_id=` breadcrumb — instead of retyping. If a submission output is lost entirely, run `task list` to recover recent task IDs before considering any resubmission.
|
|
30
|
+
|
|
31
|
+
Generation commands print the submission's `idempotency_key=` on stderr. When retrying a submission whose outcome is uncertain (timeout, crash, lost output), reuse that same key via `--idempotency-key` so the retry replays the original task instead of double-charging; a fresh key means a deliberate new generation.
|
|
28
32
|
|
|
29
33
|
`task cancel` only works while a task is still queued (`pending`). A 409 `task_already_running` means generation already started and cannot be stopped — keep tracking with `task status`. Cancellation failures return explicit codes (`task_already_finished`, `task_cancel_failed`); follow `error.hint` instead of retrying blindly.
|
|
30
34
|
|