focalapi-cli 0.3.0 → 0.3.1

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 - 2026-08-18
4
+
5
+ - Added `--duration` as an alias of `--seconds` on `gen video` so the API contract field name works directly; conflicting values are rejected locally.
6
+ - Emitted a stderr breadcrumb (`task_id=...`) for every `--no-wait` submission so callers that fail to parse stdout JSON can recover the task instead of resubmitting and double-charging.
7
+ - Added case-sensitive ID transcription guidance to `task status`/`task cancel` 404 errors (`l`/`1`/`I`, `O`/`0`) with an explicit warning against blind resubmission.
8
+ - Updated the bundled Agent Skills with resubmission discipline, the Windows `.cmd` shim invocation requirement, and exact task-ID copying guidance.
9
+
3
10
  ## 0.3.0 - 2026-08-18
4
11
 
5
12
  - Added `task cancel` for queued tasks through `DELETE /v1/video/generations/{task_id}`, including the 409 `task_already_running` / `task_already_finished` and 502 `task_cancel_failed` contract with actionable hints.
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.3.0" : "0.0.0-dev";
179
+ var VERSION = true ? "0.3.1" : "0.0.0-dev";
180
180
 
181
181
  // src/commands/auth.ts
182
182
  import { createInterface } from "readline/promises";
@@ -1549,6 +1549,12 @@ async function cancelTask(baseUrl, apiKey, taskId) {
1549
1549
  hint: "\u8FD0\u884C focalapi task status " + taskId + " \u67E5\u770B\u7ED3\u679C\uFF1B\u6210\u529F\u540E\u53EF\u4E0B\u8F7D\u4EA7\u7269\u3002"
1550
1550
  });
1551
1551
  }
1552
+ if (code === "task_not_found") {
1553
+ throw new ApiError("task_not_found", `\u4EFB\u52A1 ${taskId} \u4E0D\u5B58\u5728\u6216\u4E0D\u5C5E\u4E8E\u5F53\u524D Key`, {
1554
+ status: err.status,
1555
+ hint: "task_id \u533A\u5206\u5927\u5C0F\u5199\u4E14\u6DF7\u6709\u5C0F\u5199 l / \u6570\u5B57 1 / \u5927\u5199 I\uFF0C\u5FC5\u987B\u9010\u5B57\u590D\u5236\uFF1B\u8BF7\u56DE\u67E5\u63D0\u4EA4\u8F93\u51FA\u540E\u518D\u8BD5\u3002"
1556
+ });
1557
+ }
1552
1558
  if (code === "task_cancel_incomplete") {
1553
1559
  throw new ApiError("task_cancel_incomplete", `\u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88\u4F46\u6E05\u7406\u672A\u5B8C\u6210`, {
1554
1560
  status: err.status,
@@ -1825,6 +1831,7 @@ function registerGen(program) {
1825
1831
  if (!taskId) {
1826
1832
  throw new ApiError("bad_response", "\u5F02\u6B65\u56FE\u50CF\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: res });
1827
1833
  }
1834
+ info(`task_id=${taskId}`);
1828
1835
  if (g.json) {
1829
1836
  printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, next_command: `focalapi task status ${taskId} --json` });
1830
1837
  } else {
@@ -1951,17 +1958,21 @@ function registerGen(program) {
1951
1958
  if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
1952
1959
  }
1953
1960
  });
1954
- 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>", "\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(
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(
1955
1962
  async (promptParts, opts, cmd) => {
1956
1963
  const g = cmd.optsWithGlobals();
1957
1964
  const auth = resolveAuth(g);
1958
1965
  const model = opts.model ?? (await resolveCreativeModel(auth, "video")).model.id;
1959
1966
  if (!opts.model && !g.json) info(`\u5DF2\u81EA\u52A8\u9009\u62E9\u89C6\u9891\u6A21\u578B\uFF1A${model}`);
1967
+ if (opts.seconds !== void 0 && opts.duration !== void 0 && opts.seconds !== opts.duration) {
1968
+ throw new ApiError("invalid_request", `--seconds \u4E0E --duration \u662F\u540C\u4E00\u53C2\u6570\uFF0C\u53EA\u9700\u4F20\u4E00\u4E2A\uFF08\u6536\u5230 ${opts.seconds} \u548C ${opts.duration}\uFF09`);
1969
+ }
1970
+ const seconds = opts.seconds ?? opts.duration;
1960
1971
  const body = { model, prompt: promptParts.join(" ") };
1961
1972
  const metadata = {};
1962
- if (opts.seconds !== void 0) {
1963
- const seconds = clampInt(opts.seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds");
1964
- body.duration = seconds;
1973
+ if (seconds !== void 0) {
1974
+ const secondsValue = clampInt(seconds, 1, MAX_TASK_DURATION_SECONDS, "seconds");
1975
+ body.duration = secondsValue;
1965
1976
  }
1966
1977
  if (opts.size) body.size = opts.size;
1967
1978
  if (opts.image) body.images = opts.image;
@@ -1982,7 +1993,7 @@ function registerGen(program) {
1982
1993
  if (opts.safetyIdentifier) metadata.safety_identifier = opts.safetyIdentifier;
1983
1994
  if (opts.content) metadata.content = parseJsonArray(opts.content, "content");
1984
1995
  validateVideoGeneration(model, {
1985
- seconds: opts.seconds,
1996
+ seconds,
1986
1997
  resolution: opts.resolution,
1987
1998
  ratio: opts.ratio,
1988
1999
  aspectRatio: opts.aspectRatio,
@@ -2011,6 +2022,7 @@ function registerGen(program) {
2011
2022
  throw new ApiError("bad_response", "\u89C6\u9891\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: created });
2012
2023
  }
2013
2024
  if (opts.wait === false) {
2025
+ info(`task_id=${taskId}`);
2014
2026
  if (g.json) {
2015
2027
  printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
2016
2028
  } else {
@@ -2040,29 +2052,40 @@ function registerGen(program) {
2040
2052
  }
2041
2053
 
2042
2054
  // 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\u56DE\u67E5\u5F53\u65F6\u7684 stderr \u9762\u5305\u5C51\uFF08task_id=...\uFF09\uFF0C\u4E0D\u8981\u76F2\u76EE\u91CD\u65B0\u63D0\u4EA4\u2014\u2014\u91CD\u590D\u63D0\u4EA4\u4F1A\u91CD\u590D\u6263\u8D39\u3002";
2056
+ function withTaskIdHint(err) {
2057
+ if (err instanceof ApiError && err.status === 404) {
2058
+ return new ApiError(err.code, err.message, { status: err.status, hint: TASK_ID_HINT, body: err.body, upstreamCode: err.upstreamCode, requestId: err.requestId });
2059
+ }
2060
+ return err;
2061
+ }
2043
2062
  function registerTask(program) {
2044
2063
  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");
2045
2064
  task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId, _opts, cmd) => {
2046
2065
  const g = cmd.optsWithGlobals();
2047
2066
  const auth = resolveAuth(g);
2048
- const info_ = await fetchTask(auth.baseUrl, auth.apiKey, taskId);
2049
- if (g.json) {
2050
- printJson({ task_id: taskId, status: info_.status, raw_status: info_.rawStatus, progress: info_.progress, raw: info_.raw });
2051
- } else {
2052
- printTable(
2053
- ["\u5B57\u6BB5", "\u503C"],
2054
- [
2055
- ["\u4EFB\u52A1 ID", taskId],
2056
- ["\u72B6\u6001", `${info_.status}${info_.rawStatus && info_.rawStatus !== info_.status ? `\uFF08\u4E0A\u6E38\uFF1A${info_.rawStatus}\uFF09` : ""}`],
2057
- ["\u8FDB\u5EA6", info_.progress !== void 0 ? `${info_.progress}%` : "-"]
2058
- ]
2059
- );
2060
- if (info_.status === "success") {
2061
- info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
2062
- }
2063
- if (info_.status === "pending" || info_.status === "running") {
2064
- info(`\u5982\u9700\u505C\u6B62\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF1Afocalapi task cancel ${taskId}`);
2067
+ try {
2068
+ const info_ = await fetchTask(auth.baseUrl, auth.apiKey, taskId);
2069
+ if (g.json) {
2070
+ printJson({ task_id: taskId, status: info_.status, raw_status: info_.rawStatus, progress: info_.progress, raw: info_.raw });
2071
+ } else {
2072
+ printTable(
2073
+ ["\u5B57\u6BB5", "\u503C"],
2074
+ [
2075
+ ["\u4EFB\u52A1 ID", taskId],
2076
+ ["\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}%` : "-"]
2078
+ ]
2079
+ );
2080
+ if (info_.status === "success") {
2081
+ info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
2082
+ }
2083
+ if (info_.status === "pending" || info_.status === "running") {
2084
+ info(`\u5982\u9700\u505C\u6B62\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF1Afocalapi task cancel ${taskId}`);
2085
+ }
2065
2086
  }
2087
+ } catch (err) {
2088
+ throw withTaskIdHint(err);
2066
2089
  }
2067
2090
  });
2068
2091
  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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "focalapi-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "让任意 AI Agent 直接调用 focalapi 创作模型的命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@ FocalAPI is a creative-model gateway for the current Agent to call. It is not th
17
17
  1. If the user does not specify a model, omit `--model`. The CLI selects a FocalAPI default from the live model pool and detailed contracts available to the current key. Do not generate a test sample first.
18
18
  2. If the user specifies a model or provider, run `focalapi models get <model-id> --json` first. If the model ID is incomplete, run `models search` once to find the exact ID, then read its details.
19
19
  3. Use only parameters and values listed in the detailed `supported_params`. Never infer support from a similar model.
20
- 4. Add `--json` for Agent and script calls. stdout is the only machine-readable result; diagnostics go to stderr.
20
+ 4. Add `--json` for Agent and script calls. stdout is the only machine-readable result; diagnostics go to stderr. Do not merge the two streams (`2>&1`) before parsing. Programmatic callers on Windows must invoke the CLI through a shell (`cmd /c focalapi ...` or `shell=True`) because npm installs a `.cmd` shim.
21
21
  5. After successful generation, return local absolute file paths to the user. If a video response contains `task_id`, follow `next_command`; never submit the same task again.
22
22
 
23
23
  ```bash
@@ -50,6 +50,8 @@ focalapi task download <task-id> -o ./focalapi-out --json
50
50
 
51
51
  `pending` and `running` are not failures. Keep checking the same `task_id` and never resubmit generation. On failure, read the structured `error.code` and `hint`, and fix only the explicit problem instead of rotating models blindly.
52
52
 
53
+ Never resubmit after an output-parsing failure on your side. If your own pipeline fails to parse a submission command's stdout JSON, the task was almost certainly created and charged: recover the `task_id` from the command's stderr breadcrumb line (`task_id=...`) and continue with `task status`. Resubmitting a request whose outcome is unknown duplicates the charge.
54
+
53
55
  Two transient outcomes need no parameter changes:
54
56
 
55
57
  - `capacity_exhausted` (HTTP 503): the platform queue is full. Retry the same command after roughly 10 seconds; do not switch models or shrink the request.
@@ -24,6 +24,8 @@ focalapi task cancel <task-id> --json
24
24
  - `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
25
  - `unknown`: preserve the raw response and run `focalapi doctor --json`; never fabricate a success state.
26
26
 
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.
28
+
27
29
  `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.
28
30
 
29
31
  Polling must be bounded. When the user does not ask for blocking wait behavior, report the current status and `task_id`.