@oiioii/cli 0.1.0 → 0.1.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.
Files changed (4) hide show
  1. package/README.md +107 -28
  2. package/dist/cli.js +1155 -448
  3. package/package.json +17 -10
  4. package/dist/cli.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,19 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command14, CommanderError } from "commander";
4
+ import { Command as Command15, CommanderError } from "commander";
5
5
 
6
6
  // src/commands/workspace.ts
7
- import { Command } from "commander";
7
+ import { Command as Command2 } from "commander";
8
8
 
9
9
  // src/config/config_store.ts
10
10
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, chmodSync as chmodSync2 } from "fs";
11
11
  import { join as join3 } from "path";
12
12
 
13
13
  // src/config/build_environment.ts
14
+ var BUILD_ENVIRONMENT_URL_VARIABLES = {
15
+ production: "OIIOII_PRODUCTION_BASE_URL",
16
+ test: "OIIOII_TEST_BASE_URL",
17
+ local: void 0
18
+ };
14
19
  var BUILD_ENVIRONMENT_URLS = {
15
20
  production: "https://api.oiioii.ai",
16
- test: "https://api-hoe.oiioii.tv",
21
+ test: void 0,
17
22
  local: "http://localhost:4000"
18
23
  };
19
24
  var RELEASE_CHANNELS = {
@@ -31,15 +36,24 @@ function injectedBuildBaseUrl() {
31
36
  const value = true ? "https://api.oiioii.ai" : process.env["OIIOII_BUILD_BASE_URL"];
32
37
  return value?.trim() || void 0;
33
38
  }
39
+ function environmentBuildBaseUrl(environment) {
40
+ const variable = BUILD_ENVIRONMENT_URL_VARIABLES[environment];
41
+ return variable ? process.env[variable]?.trim() || void 0 : void 0;
42
+ }
34
43
  function getBuildEnvironment() {
35
- const value = injectedBuildEnvironment() ?? "test";
36
- return isBuildEnvironment(value) ? value : "test";
44
+ const value = injectedBuildEnvironment() ?? "local";
45
+ return isBuildEnvironment(value) ? value : "local";
37
46
  }
38
47
  function getBuildEnvironmentConfig() {
39
48
  const environment = getBuildEnvironment();
49
+ const baseUrl = injectedBuildBaseUrl() ?? environmentBuildBaseUrl(environment) ?? BUILD_ENVIRONMENT_URLS[environment];
50
+ if (!baseUrl) {
51
+ const variable = BUILD_ENVIRONMENT_URL_VARIABLES[environment] ?? "OIIOII_BUILD_BASE_URL";
52
+ throw new Error(`${variable} is required for ${environment} builds.`);
53
+ }
40
54
  return {
41
55
  environment,
42
- baseUrl: injectedBuildBaseUrl() ?? BUILD_ENVIRONMENT_URLS[environment]
56
+ baseUrl
43
57
  };
44
58
  }
45
59
  function getDefaultBaseUrl() {
@@ -454,13 +468,18 @@ var FileConfigStore = class {
454
468
  /**
455
469
  * Merge partial updates into the existing config.
456
470
  */
457
- update(partial) {
471
+ update(partial, options = {}) {
458
472
  this._ensureDir();
459
473
  withFileLock(`${this.configPath}.lock`, () => {
460
474
  const current = this.load();
461
- this._write({ ...current, ...partial });
475
+ const next = { ...current, ...partial };
476
+ if (options.resetBaseUrl) delete next.baseUrl;
477
+ this._write(next);
478
+ });
479
+ logFeatureStep("config_store", "config.updated", {
480
+ fields: Object.keys(partial),
481
+ reset_base_url: options.resetBaseUrl === true
462
482
  });
463
- logFeatureStep("config_store", "config.updated", { fields: Object.keys(partial) });
464
483
  }
465
484
  /** Ensure the config directory exists. */
466
485
  _ensureDir() {
@@ -496,6 +515,7 @@ var ErrorCode = {
496
515
  RECORD_NOT_FOUND: "RECORD_NOT_FOUND",
497
516
  // Local tasks
498
517
  TASK_NOT_FOUND: "TASK_NOT_FOUND",
518
+ TASK_RETRY_REQUIRED: "TASK_RETRY_REQUIRED",
499
519
  TASK_TIMEOUT: "TASK_TIMEOUT",
500
520
  // Quota
501
521
  QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
@@ -618,6 +638,78 @@ function startApiCall(context2, logger = createDefaultLogger()) {
618
638
  return new ApiCallSpan(logger, context2);
619
639
  }
620
640
 
641
+ // src/app/version.ts
642
+ var CLI_VERSION = true ? "0.1.1" : process.env["OIIOII_CLI_VERSION"] ?? "0.1.1";
643
+ var CLI_NPM_PACKAGE_NAME = true ? "@oiioii/cli" : process.env["OIIOII_NPM_PACKAGE_NAME"] ?? "@oiioii/cli";
644
+
645
+ // src/clients/client_context.ts
646
+ var HOGI_CLIENT_CONTEXT_HEADERS = {
647
+ clientType: "x-hogi-client-type",
648
+ clientVersion: "x-hogi-client-version",
649
+ clientOS: "x-hogi-client-os"
650
+ };
651
+ function normalizeClientOS(platform) {
652
+ if (platform === "win32") return "windows";
653
+ if (platform === "darwin") return "mac";
654
+ return platform;
655
+ }
656
+ function buildCliClientContext() {
657
+ return {
658
+ clientType: "cli",
659
+ clientVersion: CLI_VERSION,
660
+ clientOS: normalizeClientOS(process.platform)
661
+ };
662
+ }
663
+ function withClientContextHeaders(headers) {
664
+ const context2 = buildCliClientContext();
665
+ return {
666
+ ...headers,
667
+ ...Object.fromEntries(
668
+ Object.entries(HOGI_CLIENT_CONTEXT_HEADERS).map(([field, header]) => [
669
+ header,
670
+ context2[field]
671
+ ])
672
+ )
673
+ };
674
+ }
675
+
676
+ // src/clients/request_timeout.ts
677
+ var DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 3e4;
678
+ var DEFAULT_HTTP_TRANSFER_TIMEOUT_MS = 5 * 6e4;
679
+ var HttpRequestTimeoutError = class extends Error {
680
+ constructor(operation, timeoutMs) {
681
+ super(`${operation} timed out after ${timeoutMs}ms`);
682
+ this.operation = operation;
683
+ this.timeoutMs = timeoutMs;
684
+ this.name = "HttpRequestTimeoutError";
685
+ }
686
+ operation;
687
+ timeoutMs;
688
+ };
689
+ async function withRequestTimeout(params) {
690
+ const timeoutMs = params.timeoutMs ?? DEFAULT_HTTP_REQUEST_TIMEOUT_MS;
691
+ const controller = new AbortController();
692
+ let timer;
693
+ const timeout = new Promise((_resolve, reject) => {
694
+ timer = setTimeout(() => {
695
+ controller.abort();
696
+ reject(new HttpRequestTimeoutError(params.operation, timeoutMs));
697
+ }, timeoutMs);
698
+ });
699
+ timer?.unref();
700
+ try {
701
+ return await Promise.race([params.run(controller.signal), timeout]);
702
+ } catch (error2) {
703
+ if (error2 instanceof HttpRequestTimeoutError) throw error2;
704
+ if (controller.signal.aborted) {
705
+ throw new HttpRequestTimeoutError(params.operation, timeoutMs);
706
+ }
707
+ throw error2;
708
+ } finally {
709
+ if (timer) clearTimeout(timer);
710
+ }
711
+ }
712
+
621
713
  // src/clients/hogi_workspace_client.ts
622
714
  function asObject(value) {
623
715
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -654,10 +746,11 @@ async function parseEnvelope(response) {
654
746
  }
655
747
  function mapWorkspaceError(status, envelope) {
656
748
  const backendCode = envelope.error?.code;
657
- const message = envelope.error?.message ?? `Workspace creation failed: HTTP ${status}`;
749
+ const message = envelope.error?.message ?? `Workspace request failed: HTTP ${status}`;
658
750
  const details = { status, ...backendCode ? { code: backendCode } : {} };
659
751
  if (status === 401) return new CliError(ErrorCode.API_KEY_INVALID, message, details);
660
752
  if (status === 403) return new CliError(ErrorCode.WORKSPACE_FORBIDDEN, message, details);
753
+ if (status === 404) return new CliError(ErrorCode.WORKSPACE_NOT_FOUND, message, details);
661
754
  if (status === 400 || status === 422) return new CliError(ErrorCode.INVALID_ARGUMENT, message, details);
662
755
  if (status === 429) return new CliError(ErrorCode.QUOTA_EXCEEDED, message, details);
663
756
  return new CliError(ErrorCode.SERVER_ERROR, message, details);
@@ -675,16 +768,45 @@ function parseCreateResult(envelope) {
675
768
  }
676
769
  return { workspaceId, name };
677
770
  }
771
+ function parseRenameResult(envelope) {
772
+ if (envelope.ok === false) throw mapWorkspaceError(400, envelope);
773
+ const data = asObject(envelope.data);
774
+ const workspaceId = data?.workspace_id;
775
+ const name = data?.name;
776
+ if (typeof workspaceId !== "string" || !workspaceId.trim()) {
777
+ throw new CliError(ErrorCode.SERVER_ERROR, "Invalid workspace rename response: missing workspace_id");
778
+ }
779
+ if (typeof name !== "string" || !name.trim()) {
780
+ throw new CliError(ErrorCode.SERVER_ERROR, "Invalid workspace rename response: missing name");
781
+ }
782
+ return { workspaceId, name };
783
+ }
784
+ function parseWorkspaceAssets(envelope) {
785
+ if (envelope.ok === false) throw mapWorkspaceError(400, envelope);
786
+ const data = asObject(envelope.data);
787
+ if (!Array.isArray(data?.asset)) {
788
+ throw new CliError(ErrorCode.SERVER_ERROR, "Invalid workspace detail response: missing asset list");
789
+ }
790
+ return data.asset.map((value, index) => {
791
+ const asset = asObject(value);
792
+ if (!asset || typeof asset.assetId !== "string" || typeof asset.type !== "string" || typeof asset.uri !== "string") {
793
+ throw new CliError(ErrorCode.SERVER_ERROR, `Invalid workspace asset at index ${index}`);
794
+ }
795
+ return asset;
796
+ });
797
+ }
678
798
  var HogiWorkspaceClient = class {
679
799
  baseUrl;
680
800
  apiKey;
681
801
  _fetch;
682
802
  logger;
803
+ timeoutMs;
683
804
  constructor(options) {
684
805
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
685
806
  this.apiKey = options.apiKey;
686
807
  this._fetch = options.fetch ?? globalThis.fetch;
687
808
  this.logger = options.logger;
809
+ this.timeoutMs = options.timeoutMs;
688
810
  }
689
811
  async createWorkspace(request) {
690
812
  const apiLog = startApiCall({
@@ -696,19 +818,43 @@ var HogiWorkspaceClient = class {
696
818
  }, this.logger);
697
819
  let response;
698
820
  try {
699
- response = await this._fetch(`${this.baseUrl}/v1/workspace/create`, {
700
- method: "POST",
701
- headers: {
702
- authorization: `Bearer ${this.apiKey}`,
703
- "content-type": "application/json",
704
- accept: "application/json"
705
- },
706
- body: JSON.stringify({
707
- ...request.name !== void 0 ? { name: request.name } : {},
708
- ...request.language !== void 0 ? { language: request.language } : {}
709
- })
821
+ return await withRequestTimeout({
822
+ operation: "Workspace creation request",
823
+ timeoutMs: this.timeoutMs,
824
+ run: async (signal) => {
825
+ response = await this._fetch(`${this.baseUrl}/v1/workspace/create`, {
826
+ method: "POST",
827
+ headers: withClientContextHeaders({
828
+ authorization: `Bearer ${this.apiKey}`,
829
+ "content-type": "application/json",
830
+ accept: "application/json"
831
+ }),
832
+ body: JSON.stringify({
833
+ ...request.name !== void 0 ? { name: request.name } : {},
834
+ ...request.language !== void 0 ? { language: request.language } : {}
835
+ }),
836
+ signal
837
+ });
838
+ const envelope = await parseEnvelope(response);
839
+ if (!response.ok) throw mapWorkspaceError(response.status, envelope);
840
+ const result = parseCreateResult(envelope);
841
+ apiLog.success(response.status, { workspace_id: result.workspaceId });
842
+ return result;
843
+ }
710
844
  });
711
845
  } catch (error2) {
846
+ if (error2 instanceof HttpRequestTimeoutError) {
847
+ const timeoutError = new CliError(ErrorCode.NETWORK_ERROR, error2.message, {
848
+ retryable: true,
849
+ timeout_ms: error2.timeoutMs
850
+ });
851
+ apiLog.failure(timeoutError, response?.status);
852
+ throw timeoutError;
853
+ }
854
+ if (error2 instanceof CliError) {
855
+ apiLog.failure(error2, response?.status);
856
+ throw error2;
857
+ }
712
858
  const networkError = new CliError(
713
859
  ErrorCode.NETWORK_ERROR,
714
860
  `Network error: ${error2 instanceof Error ? error2.message : String(error2)}`
@@ -716,17 +862,98 @@ var HogiWorkspaceClient = class {
716
862
  apiLog.failure(networkError);
717
863
  throw networkError;
718
864
  }
865
+ }
866
+ async renameWorkspace(workspaceId, name) {
867
+ const path = "/v1/workspace/rename";
868
+ const apiLog = startApiCall({
869
+ source: "hogi_workspace_client",
870
+ operation: "workspace.rename",
871
+ method: "POST",
872
+ path,
873
+ metadata: { workspace_id: workspaceId }
874
+ }, this.logger);
875
+ let response;
719
876
  try {
720
- const envelope = await parseEnvelope(response);
721
- if (!response.ok) throw mapWorkspaceError(response.status, envelope);
722
- const result = parseCreateResult(envelope);
723
- apiLog.success(response.status, { workspace_id: result.workspaceId });
724
- return result;
877
+ return await withRequestTimeout({
878
+ operation: "Workspace rename request",
879
+ timeoutMs: this.timeoutMs,
880
+ run: async (signal) => {
881
+ response = await this._fetch(`${this.baseUrl}${path}`, {
882
+ method: "POST",
883
+ headers: {
884
+ authorization: `Bearer ${this.apiKey}`,
885
+ "content-type": "application/json",
886
+ accept: "application/json"
887
+ },
888
+ body: JSON.stringify({ workspace_id: workspaceId, name }),
889
+ signal
890
+ });
891
+ const envelope = await parseEnvelope(response);
892
+ if (!response.ok) throw mapWorkspaceError(response.status, envelope);
893
+ const result = parseRenameResult(envelope);
894
+ apiLog.success(response.status, { workspace_id: result.workspaceId });
895
+ return result;
896
+ }
897
+ });
725
898
  } catch (error2) {
726
- apiLog.failure(error2, response.status);
727
- throw error2;
899
+ throw this.mapRequestError(error2, "Workspace rename request", apiLog, response);
900
+ }
901
+ }
902
+ async listAssets(workspaceId) {
903
+ const path = "/v1/workspace/detail";
904
+ const apiLog = startApiCall({
905
+ source: "hogi_workspace_client",
906
+ operation: "workspace.assets.list",
907
+ method: "GET",
908
+ path,
909
+ metadata: { workspace_id: workspaceId }
910
+ }, this.logger);
911
+ const params = new URLSearchParams({ workspace_id: workspaceId, relateTypes: "asset" });
912
+ let response;
913
+ try {
914
+ return await withRequestTimeout({
915
+ operation: "Workspace asset list request",
916
+ timeoutMs: this.timeoutMs,
917
+ run: async (signal) => {
918
+ response = await this._fetch(`${this.baseUrl}${path}?${params.toString()}`, {
919
+ method: "GET",
920
+ headers: {
921
+ authorization: `Bearer ${this.apiKey}`,
922
+ accept: "application/json"
923
+ },
924
+ signal
925
+ });
926
+ const envelope = await parseEnvelope(response);
927
+ if (!response.ok) throw mapWorkspaceError(response.status, envelope);
928
+ const assets = parseWorkspaceAssets(envelope);
929
+ apiLog.success(response.status, { workspace_id: workspaceId, asset_count: assets.length });
930
+ return assets;
931
+ }
932
+ });
933
+ } catch (error2) {
934
+ throw this.mapRequestError(error2, "Workspace asset list request", apiLog, response);
728
935
  }
729
936
  }
937
+ mapRequestError(error2, operation, apiLog, response) {
938
+ if (error2 instanceof HttpRequestTimeoutError) {
939
+ const timeoutError = new CliError(ErrorCode.NETWORK_ERROR, error2.message, {
940
+ retryable: true,
941
+ timeout_ms: error2.timeoutMs
942
+ });
943
+ apiLog.failure(timeoutError, response?.status);
944
+ return timeoutError;
945
+ }
946
+ if (error2 instanceof CliError) {
947
+ apiLog.failure(error2, response?.status);
948
+ return error2;
949
+ }
950
+ const networkError = new CliError(
951
+ ErrorCode.NETWORK_ERROR,
952
+ `${operation} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
953
+ );
954
+ apiLog.failure(networkError);
955
+ return networkError;
956
+ }
730
957
  };
731
958
 
732
959
  // src/app/command_context.ts
@@ -751,6 +978,21 @@ function requireApiKeyAuth(rootOptions, configStore) {
751
978
  const baseUrl = typeof baseUrlOption === "string" && baseUrlOption ? baseUrlOption : config.baseUrl;
752
979
  return { apiKey, baseUrl };
753
980
  }
981
+ function resolveWorkspaceId(rootOptions, configStore, explicitWorkspaceId) {
982
+ const explicit = explicitWorkspaceId?.trim();
983
+ if (explicitWorkspaceId !== void 0 && !explicit) {
984
+ throw new CliError(ErrorCode.INVALID_ARGUMENT, "Workspace ID must not be empty");
985
+ }
986
+ const rootOverride = typeof rootOptions.workspace === "string" ? rootOptions.workspace.trim() : "";
987
+ const workspaceId = explicit || rootOverride || configStore.loadResolved().defaultWorkspaceId?.trim();
988
+ if (!workspaceId) {
989
+ throw new CliError(
990
+ ErrorCode.WORKSPACE_REQUIRED,
991
+ "No workspace set. Pass a workspace ID, use --workspace <id>, set OIIOII_WORKSPACE_ID, or set an active workspace."
992
+ );
993
+ }
994
+ return workspaceId;
995
+ }
754
996
 
755
997
  // src/telemetry/event_registry.ts
756
998
  var DOMAIN_EVENTS = {
@@ -779,6 +1021,9 @@ var BUSINESS_COMMAND_REGISTRY = {
779
1021
  "workspace.create": definition("workspace", "create"),
780
1022
  "workspace.set": definition("workspace", "set"),
781
1023
  "workspace.current": definition("workspace", "current"),
1024
+ "workspace.rename": definition("workspace", "rename"),
1025
+ "workspace.assets.list": definition("workspace", "assets_list"),
1026
+ "workspace.assets.download": definition("workspace", "asset_download"),
782
1027
  "image.text": definition("generation", "image_text"),
783
1028
  "image.edit": definition("generation", "image_edit"),
784
1029
  "video.text": definition("generation", "video_text"),
@@ -789,6 +1034,7 @@ var BUSINESS_COMMAND_REGISTRY = {
789
1034
  "audio.suno": definition("generation", "audio_suno"),
790
1035
  "upload.file": definition("upload", "file"),
791
1036
  "task.list": definition("task", "list"),
1037
+ "task.retry": definition("task", "retry"),
792
1038
  "task.status": definition("task", "status"),
793
1039
  "task.wait": definition("task", "wait"),
794
1040
  "record.get": definition("record", "get"),
@@ -814,10 +1060,6 @@ function getBusinessCommandDefinition(command) {
814
1060
  // src/telemetry/telemetry_manager.ts
815
1061
  import { randomBytes as randomBytes4 } from "crypto";
816
1062
 
817
- // src/app/version.ts
818
- var CLI_VERSION = true ? "0.1.0" : process.env["OIIOII_CLI_VERSION"] ?? "0.1.0";
819
- var CLI_NPM_PACKAGE_NAME = true ? "@oiioii/cli" : process.env["OIIOII_NPM_PACKAGE_NAME"] ?? "@oiioii/cli";
820
-
821
1063
  // src/telemetry/telemetry_context.ts
822
1064
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
823
1065
  var telemetryContextStorage = new AsyncLocalStorage2();
@@ -948,7 +1190,7 @@ function defaultCommonParams() {
948
1190
  const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
949
1191
  return {
950
1192
  schema_version: 1,
951
- app_env: environment === "local" ? "dev" : environment === "production" ? "prod" : "hoe",
1193
+ app_env: environment === "local" ? "dev" : environment === "production" ? "prod" : "test",
952
1194
  app_name: "oiioii_cli",
953
1195
  cli_version: CLI_VERSION,
954
1196
  os: process.platform,
@@ -1101,7 +1343,7 @@ function telemetryResourceType(uri) {
1101
1343
  }
1102
1344
 
1103
1345
  // src/clients/hogi_mcp_client.ts
1104
- import { Agent } from "undici";
1346
+ import { Agent, fetch as undiciFetch } from "undici";
1105
1347
  function errorFromMcpToolResult(result, fallback) {
1106
1348
  const backendCode = typeof result.errorCode === "string" ? result.errorCode : void 0;
1107
1349
  const text = (result.content ?? []).filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text.trim()).filter(Boolean).join("\n");
@@ -1156,7 +1398,19 @@ function errorFromJsonRpc(error2) {
1156
1398
  if (Number(error2.code) === -32601) {
1157
1399
  return new CliError(ErrorCode.MODEL_NOT_SUPPORTED, message, details);
1158
1400
  }
1159
- return new CliError(ErrorCode.GENERATION_FAILED, message, error2);
1401
+ if (Number(error2.code) === -32700 || Number(error2.code) === -32600) {
1402
+ return new CliError(ErrorCode.SERVER_ERROR, message, {
1403
+ ...error2,
1404
+ category: "protocol",
1405
+ retryable: false
1406
+ });
1407
+ }
1408
+ return new CliError(ErrorCode.SERVER_ERROR, message, {
1409
+ ...error2,
1410
+ category: "server",
1411
+ retryable: true,
1412
+ request_outcome: "unknown"
1413
+ });
1160
1414
  }
1161
1415
  var nextJsonRpcId = 1;
1162
1416
  async function errorForHttpResponse(response) {
@@ -1201,10 +1455,25 @@ async function errorForHttpResponse(response) {
1201
1455
  retryable: false
1202
1456
  });
1203
1457
  }
1204
- if (response.status >= 500) return new CliError(ErrorCode.SERVER_ERROR, backendMessage ?? `Server error: ${response.statusText || response.status}`, details);
1458
+ if (response.status >= 500) {
1459
+ return new CliError(ErrorCode.SERVER_ERROR, backendMessage ?? `Server error: ${response.statusText || response.status}`, {
1460
+ ...details,
1461
+ category: "server",
1462
+ retryable: true,
1463
+ request_outcome: "unknown"
1464
+ });
1465
+ }
1205
1466
  return new CliError(ErrorCode.GENERATION_FAILED, backendMessage ?? `Request failed: ${response.statusText || response.status}`, details);
1206
1467
  }
1207
- function parseSseJson(text) {
1468
+ function responseProtocolError(message, status) {
1469
+ return new CliError(ErrorCode.SERVER_ERROR, message, {
1470
+ ...status !== void 0 ? { status } : {},
1471
+ category: "response_protocol",
1472
+ retryable: true,
1473
+ response_protocol: true
1474
+ });
1475
+ }
1476
+ function parseSseJson(text, status) {
1208
1477
  const dataLines = [];
1209
1478
  for (const line of text.split(/\r?\n/)) {
1210
1479
  if (line.startsWith("data:")) {
@@ -1212,46 +1481,68 @@ function parseSseJson(text) {
1212
1481
  }
1213
1482
  }
1214
1483
  if (dataLines.length === 0) {
1215
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid SSE response from server");
1484
+ throw responseProtocolError("Invalid SSE response from server", status);
1216
1485
  }
1217
1486
  try {
1218
1487
  return JSON.parse(dataLines.join("\n"));
1219
1488
  } catch {
1220
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid JSON in SSE response from server");
1489
+ throw responseProtocolError("Invalid JSON in SSE response from server", status);
1221
1490
  }
1222
1491
  }
1223
1492
  async function parseJsonOrSseResponse(response) {
1224
1493
  const text = await response.text();
1225
1494
  const trimmed = text.trim();
1226
1495
  if (!trimmed) {
1227
- throw new CliError(ErrorCode.SERVER_ERROR, "Empty response from server", { status: response.status });
1496
+ throw responseProtocolError("Empty response from server", response.status);
1228
1497
  }
1229
1498
  if (trimmed.startsWith("event:") || trimmed.startsWith("data:")) {
1230
- return parseSseJson(trimmed);
1499
+ return parseSseJson(trimmed, response.status);
1231
1500
  }
1232
1501
  try {
1233
1502
  return JSON.parse(trimmed);
1234
1503
  } catch {
1235
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid JSON response from server", { status: response.status });
1504
+ throw responseProtocolError("Invalid JSON response from server", response.status);
1236
1505
  }
1237
1506
  }
1238
- function asJsonRpcResponse(payload) {
1507
+ function asJsonRpcResponse(payload, status) {
1239
1508
  if (!payload || typeof payload !== "object") {
1240
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid MCP response from server");
1509
+ throw responseProtocolError("Invalid MCP response from server", status);
1241
1510
  }
1242
1511
  return payload;
1243
1512
  }
1513
+ function undiciErrorCode(error2) {
1514
+ if (!error2 || typeof error2 !== "object") return void 0;
1515
+ if ("code" in error2 && typeof error2.code === "string") return error2.code;
1516
+ if ("cause" in error2) return undiciErrorCode(error2.cause);
1517
+ return void 0;
1518
+ }
1519
+ function isRequestTimeoutError(error2) {
1520
+ if (error2 instanceof Error && error2.name === "AbortError") return true;
1521
+ return [
1522
+ "UND_ERR_HEADERS_TIMEOUT",
1523
+ "UND_ERR_BODY_TIMEOUT",
1524
+ "UND_ERR_CONNECT_TIMEOUT"
1525
+ ].includes(undiciErrorCode(error2) ?? "");
1526
+ }
1527
+ function createMcpDispatcher(timeoutMs) {
1528
+ return new Agent({
1529
+ headersTimeout: timeoutMs,
1530
+ bodyTimeout: timeoutMs
1531
+ });
1532
+ }
1244
1533
  var HogiMcpClient = class {
1245
1534
  baseUrl;
1246
1535
  apiKey;
1247
1536
  timeoutMs;
1248
1537
  _fetch;
1538
+ usesDefaultUndiciTransport;
1249
1539
  logger;
1250
1540
  constructor(options) {
1251
1541
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1252
1542
  this.apiKey = options.apiKey;
1253
1543
  this.timeoutMs = options.timeoutMs ?? 3e5;
1254
- this._fetch = options.fetch ?? globalThis.fetch;
1544
+ this.usesDefaultUndiciTransport = options.fetch === void 0;
1545
+ this._fetch = options.fetch ?? undiciFetch;
1255
1546
  this.logger = options.logger;
1256
1547
  }
1257
1548
  async callTool(toolName, args, options = {}) {
@@ -1267,7 +1558,7 @@ var HogiMcpClient = class {
1267
1558
  { tool_name: toolName }
1268
1559
  );
1269
1560
  if (!result || typeof result !== "object") {
1270
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid MCP tool result from server");
1561
+ throw responseProtocolError("Invalid MCP tool result from server");
1271
1562
  }
1272
1563
  return result;
1273
1564
  }
@@ -1288,58 +1579,56 @@ var HogiMcpClient = class {
1288
1579
  try {
1289
1580
  response = await this._fetch(`${this.baseUrl}/mcp/${serverName}/sse`, {
1290
1581
  method: "POST",
1291
- headers: {
1582
+ headers: withClientContextHeaders({
1292
1583
  "content-type": "application/json",
1293
1584
  "accept": "application/json, text/event-stream",
1294
1585
  "x-hogi-api-key": this.apiKey,
1295
1586
  "x-oiioii-cli-version": CLI_VERSION
1296
- },
1587
+ }),
1297
1588
  body: JSON.stringify({
1298
1589
  jsonrpc: "2.0",
1299
1590
  id: nextJsonRpcId++,
1300
1591
  method,
1301
1592
  params
1302
1593
  }),
1303
- // Node's built-in fetch uses undici, whose default headers/body timeouts are 300s.
1304
- // Long-running video MCP calls intentionally wait up to timeoutMs, so keep undici
1305
- // aligned with the CLI AbortController timeout instead of failing early at 300s.
1306
- dispatcher: new Agent({
1307
- headersTimeout: this.timeoutMs,
1308
- bodyTimeout: this.timeoutMs
1309
- }),
1594
+ // Undici defaults headers/body timeouts to 300s. Long-running video MCP calls
1595
+ // intentionally wait up to timeoutMs, so align the default transport with the
1596
+ // CLI AbortController instead of failing early at 300s.
1597
+ ...this.usesDefaultUndiciTransport ? {
1598
+ dispatcher: createMcpDispatcher(this.timeoutMs)
1599
+ } : {},
1310
1600
  signal: controller.signal
1311
1601
  });
1312
- } catch (error2) {
1313
- if (error2 instanceof Error && error2.name === "AbortError") {
1314
- const timeoutError = new CliError(ErrorCode.GENERATION_TIMEOUT, `Request timed out after ${this.timeoutMs}ms`);
1315
- apiLog.failure(timeoutError);
1316
- throw timeoutError;
1602
+ if (!response.ok) {
1603
+ throw await errorForHttpResponse(response);
1317
1604
  }
1318
- const message = error2 instanceof Error ? error2.message : String(error2);
1319
- const networkError = new CliError(ErrorCode.NETWORK_ERROR, `Network error: ${message}`);
1320
- apiLog.failure(networkError);
1321
- throw networkError;
1322
- } finally {
1323
- clearTimeout(timer);
1324
- }
1325
- if (!response.ok) {
1326
- const httpError = await errorForHttpResponse(response);
1327
- apiLog.failure(httpError, response.status);
1328
- throw httpError;
1329
- }
1330
- try {
1331
- const jsonRpc = asJsonRpcResponse(await parseJsonOrSseResponse(response));
1605
+ const jsonRpc = asJsonRpcResponse(await parseJsonOrSseResponse(response), response.status);
1332
1606
  if (jsonRpc.error) {
1333
1607
  throw errorFromJsonRpc(jsonRpc.error);
1334
1608
  }
1335
1609
  if (jsonRpc.result === void 0) {
1336
- throw new CliError(ErrorCode.SERVER_ERROR, "Missing MCP result from server");
1610
+ throw responseProtocolError("Missing MCP result from server", response.status);
1337
1611
  }
1338
1612
  apiLog.success(response.status);
1339
1613
  return jsonRpc.result;
1340
1614
  } catch (error2) {
1341
- apiLog.failure(error2, response.status);
1342
- throw error2;
1615
+ let mappedError = error2;
1616
+ if (controller.signal.aborted || isRequestTimeoutError(error2)) {
1617
+ mappedError = new CliError(ErrorCode.GENERATION_TIMEOUT, `Request timed out after ${this.timeoutMs}ms`, {
1618
+ retryable: true,
1619
+ ...undiciErrorCode(error2) ? { cause_code: undiciErrorCode(error2) } : {}
1620
+ });
1621
+ } else if (!(error2 instanceof CliError)) {
1622
+ const message = error2 instanceof Error ? error2.message : String(error2);
1623
+ mappedError = new CliError(ErrorCode.NETWORK_ERROR, `Network error: ${message}`, {
1624
+ retryable: true,
1625
+ ...undiciErrorCode(error2) ? { cause_code: undiciErrorCode(error2) } : {}
1626
+ });
1627
+ }
1628
+ apiLog.failure(mappedError, response?.status);
1629
+ throw mappedError;
1630
+ } finally {
1631
+ clearTimeout(timer);
1343
1632
  }
1344
1633
  }
1345
1634
  };
@@ -1476,6 +1765,267 @@ ${title}`);
1476
1765
  console.log("\u2500".repeat(title.length));
1477
1766
  }
1478
1767
 
1768
+ // src/commands/workspace_assets.ts
1769
+ import { Command } from "commander";
1770
+
1771
+ // src/clients/hogi_res_client.ts
1772
+ import { mkdir, writeFile } from "fs/promises";
1773
+ import { existsSync as existsSync3, statSync as statSync3 } from "fs";
1774
+ import { basename, dirname as dirname2, join as join4, resolve as resolve2 } from "path";
1775
+ function assertHogiUri(uri) {
1776
+ if (!uri || !uri.startsWith("hogi://")) {
1777
+ throw new CliError(
1778
+ ErrorCode.INVALID_ARGUMENT,
1779
+ `Invalid hogi URI: "${uri}". Expected format: hogi://image/<id> or hogi://video/<id>`
1780
+ );
1781
+ }
1782
+ }
1783
+ function filenameFromUri(uri) {
1784
+ try {
1785
+ const parsed = new URL(uri);
1786
+ const name = basename(parsed.pathname);
1787
+ if (name && name !== "." && name !== "/") return name;
1788
+ } catch {
1789
+ }
1790
+ return "resource.bin";
1791
+ }
1792
+ function resolveOutputPath(uri, output) {
1793
+ const fallbackName = filenameFromUri(uri);
1794
+ if (!output) return resolve2(process.cwd(), fallbackName);
1795
+ const resolved = resolve2(output);
1796
+ if (existsSync3(resolved) && statSync3(resolved).isDirectory()) {
1797
+ return join4(resolved, fallbackName);
1798
+ }
1799
+ return resolved;
1800
+ }
1801
+ function looksLikeJsonError(contentType, buffer) {
1802
+ if (contentType?.toLowerCase().includes("application/json")) return true;
1803
+ const prefix = buffer.subarray(0, 128).toString("utf8").trimStart();
1804
+ return prefix.startsWith("{") && (prefix.includes('"error"') || prefix.includes('"code"'));
1805
+ }
1806
+ var HogiResClient = class {
1807
+ baseUrl;
1808
+ apiKey;
1809
+ _fetch;
1810
+ logger;
1811
+ timeoutMs;
1812
+ constructor(options) {
1813
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1814
+ this.apiKey = options.apiKey;
1815
+ this._fetch = options.fetch ?? globalThis.fetch;
1816
+ this.logger = options.logger;
1817
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TRANSFER_TIMEOUT_MS;
1818
+ }
1819
+ getReadUrl(uri, options = {}) {
1820
+ assertHogiUri(uri);
1821
+ const params = new URLSearchParams();
1822
+ params.set("uri", uri);
1823
+ if (options.filename) params.set("filename", options.filename);
1824
+ if (typeof options.watermark === "boolean") params.set("wm", options.watermark ? "1" : "0");
1825
+ return `${this.baseUrl}/v1/res/read_file?${params.toString()}`;
1826
+ }
1827
+ async download(uri, options = {}) {
1828
+ assertHogiUri(uri);
1829
+ const url = this.getReadUrl(uri, options);
1830
+ const outputPath = resolveOutputPath(uri, options.output);
1831
+ const apiLog = startApiCall({
1832
+ source: "hogi_res_client",
1833
+ operation: "resource.read",
1834
+ method: "GET",
1835
+ path: "/v1/res/read_file"
1836
+ }, this.logger);
1837
+ let response;
1838
+ let downloaded;
1839
+ try {
1840
+ downloaded = await withRequestTimeout({
1841
+ operation: "Resource download request",
1842
+ timeoutMs: this.timeoutMs,
1843
+ run: async (signal) => {
1844
+ response = await this._fetch(url, {
1845
+ method: "GET",
1846
+ redirect: "follow",
1847
+ headers: withClientContextHeaders({
1848
+ "x-hogi-api-key": this.apiKey,
1849
+ "accept": "image/*,video/*,application/octet-stream,application/json;q=0.5,*/*;q=0.1"
1850
+ }),
1851
+ signal
1852
+ });
1853
+ if (response.status === 401) throw new CliError(ErrorCode.API_KEY_INVALID, "Invalid API key", { status: 401 });
1854
+ if (response.status === 404) {
1855
+ throw new CliError(ErrorCode.RECORD_NOT_FOUND, `Resource not found for URI: ${uri}`, { status: 404 });
1856
+ }
1857
+ if (!response.ok) {
1858
+ const text = await response.text().catch(() => "");
1859
+ throw new CliError(ErrorCode.SERVER_ERROR, `HTTP ${response.status}: ${text || response.statusText}`, {
1860
+ status: response.status
1861
+ });
1862
+ }
1863
+ const contentType = response.headers.get("content-type");
1864
+ const buffer = Buffer.from(await response.arrayBuffer());
1865
+ if (looksLikeJsonError(contentType, buffer)) {
1866
+ let message = "Resource read failed";
1867
+ try {
1868
+ const body = JSON.parse(buffer.toString("utf8"));
1869
+ message = body.message ?? body.error ?? body.code ?? message;
1870
+ } catch {
1871
+ }
1872
+ throw new CliError(ErrorCode.SERVER_ERROR, message);
1873
+ }
1874
+ return { contentType, buffer };
1875
+ }
1876
+ });
1877
+ } catch (err) {
1878
+ if (err instanceof HttpRequestTimeoutError) {
1879
+ const timeoutError = new CliError(ErrorCode.NETWORK_ERROR, err.message, {
1880
+ retryable: true,
1881
+ timeout_ms: err.timeoutMs
1882
+ });
1883
+ apiLog.failure(timeoutError, response?.status);
1884
+ throw timeoutError;
1885
+ }
1886
+ if (err instanceof CliError) {
1887
+ apiLog.failure(err, response?.status);
1888
+ throw err;
1889
+ }
1890
+ const msg = err instanceof Error ? err.message : String(err);
1891
+ const error2 = new CliError(ErrorCode.NETWORK_ERROR, `Network error: ${msg}`);
1892
+ apiLog.failure(error2);
1893
+ throw error2;
1894
+ }
1895
+ apiLog.success(response.status, { bytes: downloaded.buffer.byteLength, mime_type: downloaded.contentType });
1896
+ await mkdir(dirname2(outputPath), { recursive: true });
1897
+ await writeFile(outputPath, downloaded.buffer);
1898
+ logFeatureStep("record", "resource.saved", { bytes: downloaded.buffer.byteLength, mime_type: downloaded.contentType });
1899
+ return {
1900
+ path: outputPath,
1901
+ bytes: downloaded.buffer.byteLength,
1902
+ mimeType: downloaded.contentType,
1903
+ uri
1904
+ };
1905
+ }
1906
+ };
1907
+
1908
+ // src/commands/workspace_assets.ts
1909
+ function createClients(rootOptions, deps) {
1910
+ const configStore = deps.configStore ?? new FileConfigStore();
1911
+ const auth = requireApiKeyAuth(rootOptions, configStore);
1912
+ return {
1913
+ auth,
1914
+ configStore,
1915
+ workspaceClient: new HogiWorkspaceClient({
1916
+ baseUrl: auth.baseUrl,
1917
+ apiKey: auth.apiKey,
1918
+ ...deps.fetch ? { fetch: deps.fetch } : {}
1919
+ })
1920
+ };
1921
+ }
1922
+ async function executeWorkspaceAssetsList(params, rootOptions, deps = {}) {
1923
+ const { configStore, workspaceClient } = createClients(rootOptions, deps);
1924
+ const workspaceId = resolveWorkspaceId(rootOptions, configStore, params.workspaceId);
1925
+ const assets = await workspaceClient.listAssets(workspaceId);
1926
+ logFeatureStep("workspace_assets", "assets.listed", { workspace_id: workspaceId, count: assets.length });
1927
+ return { workspace_id: workspaceId, count: assets.length, assets };
1928
+ }
1929
+ function assetIdFromReference(reference) {
1930
+ return reference.startsWith("hogi://asset/") ? reference.slice("hogi://asset/".length) : reference;
1931
+ }
1932
+ function resolveDownloadAsset(assets, reference) {
1933
+ const assetId = assetIdFromReference(reference.trim());
1934
+ const asset = assets.find((item) => item.assetId === assetId);
1935
+ if (!asset) {
1936
+ throw new CliError(ErrorCode.RECORD_NOT_FOUND, `Workspace asset not found: ${reference}`, {
1937
+ asset_reference: reference
1938
+ });
1939
+ }
1940
+ if (asset.status) {
1941
+ throw new CliError(ErrorCode.INVALID_ARGUMENT, `Workspace asset ${asset.assetId} is not ready for download (status: ${asset.status}).`, {
1942
+ asset_id: asset.assetId,
1943
+ status: asset.status
1944
+ });
1945
+ }
1946
+ if (!asset.uri.startsWith("hogi://") || asset.uri.startsWith("hogi://asset/")) {
1947
+ throw new CliError(ErrorCode.INVALID_ARGUMENT, `Workspace asset ${asset.assetId} has no downloadable resource URI.`, {
1948
+ asset_id: asset.assetId,
1949
+ uri: asset.uri
1950
+ });
1951
+ }
1952
+ return asset;
1953
+ }
1954
+ async function executeWorkspaceAssetDownload(params, rootOptions, deps = {}) {
1955
+ const reference = params.asset.trim();
1956
+ if (!reference) throw new CliError(ErrorCode.INVALID_ARGUMENT, "Asset ID is required");
1957
+ const { auth, configStore, workspaceClient } = createClients(rootOptions, deps);
1958
+ const workspaceId = resolveWorkspaceId(rootOptions, configStore, params.workspaceId);
1959
+ const asset = resolveDownloadAsset(await workspaceClient.listAssets(workspaceId), reference);
1960
+ const downloaded = await new HogiResClient({
1961
+ baseUrl: auth.baseUrl,
1962
+ apiKey: auth.apiKey,
1963
+ ...deps.fetch ? { fetch: deps.fetch } : {}
1964
+ }).download(asset.uri, {
1965
+ ...params.output ? { output: params.output } : {},
1966
+ ...params.filename ? { filename: params.filename } : {},
1967
+ ...params.watermark !== void 0 ? { watermark: params.watermark } : {}
1968
+ });
1969
+ logFeatureStep("workspace_assets", "asset.downloaded", {
1970
+ workspace_id: workspaceId,
1971
+ asset_id: asset.assetId,
1972
+ bytes: downloaded.bytes
1973
+ });
1974
+ return {
1975
+ workspace_id: workspaceId,
1976
+ asset_id: asset.assetId,
1977
+ uri: asset.uri,
1978
+ path: downloaded.path,
1979
+ bytes: downloaded.bytes,
1980
+ mime_type: downloaded.mimeType
1981
+ };
1982
+ }
1983
+ function handleError(error2, jsonMode, type) {
1984
+ const cliError = error2 instanceof CliError ? error2 : new CliError(ErrorCode.SERVER_ERROR, error2 instanceof Error ? error2.message : String(error2));
1985
+ if (jsonMode) printJson(errorJson(type, cliError.code, cliError.message, cliError.details));
1986
+ else error(cliError.message);
1987
+ process.exitCode = 1;
1988
+ }
1989
+ function createWorkspaceAssetsCommand(deps = {}) {
1990
+ const assets = new Command("assets").description("List and download workspace assets");
1991
+ assets.command("list").description("List assets in a workspace").argument("[workspace_id]", "Workspace ID; defaults to the active workspace").action(async (workspaceId, _opts, cmd) => {
1992
+ const jsonMode = isJsonOutput(cmd);
1993
+ try {
1994
+ const result = await executeWorkspaceAssetsList({ workspaceId }, getRootOptions(cmd), deps);
1995
+ setTelemetryWorkspaceId(result.workspace_id);
1996
+ recordTelemetryBusinessInfo({ asset_count: result.count });
1997
+ if (jsonMode) {
1998
+ printJson(successJson("workspace_assets_list", result));
1999
+ } else {
2000
+ heading(`Workspace assets (${result.count})`);
2001
+ for (const asset of result.assets) {
2002
+ info(` ${asset.assetId} ${asset.type} ${asset.status ?? "ready"} ${asset.uri || "-"}`);
2003
+ }
2004
+ }
2005
+ } catch (error2) {
2006
+ handleError(error2, jsonMode, "workspace_assets_list");
2007
+ }
2008
+ });
2009
+ assets.command("download").description("Download a workspace asset by asset ID or hogi://asset URI").argument("<asset>", "Workspace asset ID or hogi://asset/<id>").option("-o, --output <path>", "Output file or directory path").option("--filename <name>", "Download filename hint passed to backend").option("--wm", "Request watermarked asset").action(async (asset, opts, cmd) => {
2010
+ const jsonMode = isJsonOutput(cmd);
2011
+ try {
2012
+ const result = await executeWorkspaceAssetDownload({
2013
+ asset,
2014
+ ...opts.output ? { output: opts.output } : {},
2015
+ ...opts.filename ? { filename: opts.filename } : {},
2016
+ ...opts.wm !== void 0 ? { watermark: opts.wm } : {}
2017
+ }, getRootOptions(cmd), deps);
2018
+ setTelemetryWorkspaceId(result.workspace_id);
2019
+ recordTelemetryBusinessInfo({ resource_type: new URL(result.uri).hostname, size_bytes: result.bytes });
2020
+ if (jsonMode) printJson(successJson("workspace_asset_download", result));
2021
+ else success(`Downloaded workspace asset ${result.asset_id}: ${result.path}`);
2022
+ } catch (error2) {
2023
+ handleError(error2, jsonMode, "workspace_asset_download");
2024
+ }
2025
+ });
2026
+ return assets;
2027
+ }
2028
+
1479
2029
  // src/commands/workspace.ts
1480
2030
  function getWorkspaceOverride(cmd) {
1481
2031
  let root = cmd;
@@ -1521,6 +2071,21 @@ async function executeWorkspaceCreate(params, rootOptions, deps = {}) {
1521
2071
  set_current: params.setCurrent === true
1522
2072
  };
1523
2073
  }
2074
+ async function executeWorkspaceRename(params, rootOptions, deps = {}) {
2075
+ const name = params.name.trim();
2076
+ if (!name) throw new CliError(ErrorCode.INVALID_ARGUMENT, "--name must not be empty");
2077
+ if (name.length > 100) throw new CliError(ErrorCode.INVALID_ARGUMENT, "--name must not exceed 100 characters");
2078
+ const configStore = deps.configStore ?? new FileConfigStore();
2079
+ const auth = requireApiKeyAuth(rootOptions, configStore);
2080
+ const workspaceId = resolveWorkspaceId(rootOptions, configStore, params.workspaceId);
2081
+ const result = await new HogiWorkspaceClient({
2082
+ baseUrl: auth.baseUrl,
2083
+ apiKey: auth.apiKey,
2084
+ ...deps.fetch ? { fetch: deps.fetch } : {}
2085
+ }).renameWorkspace(workspaceId, name);
2086
+ logFeatureStep("workspace", "workspace.renamed", { workspace_id: result.workspaceId });
2087
+ return { workspace_id: result.workspaceId, name: result.name };
2088
+ }
1524
2089
  function handleWorkspaceError(error2, jsonMode, type) {
1525
2090
  const cliError = error2 instanceof CliError ? error2 : new CliError(ErrorCode.SERVER_ERROR, error2 instanceof Error ? error2.message : String(error2));
1526
2091
  if (jsonMode) printJson(errorJson(type, cliError.code, cliError.message, cliError.details));
@@ -1528,7 +2093,7 @@ function handleWorkspaceError(error2, jsonMode, type) {
1528
2093
  process.exitCode = 1;
1529
2094
  }
1530
2095
  function createWorkspaceCommand(deps = {}) {
1531
- const ws = new Command("workspace").description("Workspace management");
2096
+ const ws = new Command2("workspace").description("Workspace management");
1532
2097
  ws.command("create").description("Create a new workspace").option("--name <name>", "Workspace name").option("--language <language>", "Workspace language: en, zh, zh-Hant, or ja").option("--set-current", "Set the created workspace as the active workspace").action(async (opts, cmd) => {
1533
2098
  const jsonMode = isJsonOutput(cmd);
1534
2099
  try {
@@ -1614,22 +2179,41 @@ function createWorkspaceCommand(deps = {}) {
1614
2179
  info(` Source: ${source}`);
1615
2180
  }
1616
2181
  });
2182
+ ws.command("rename").description("Rename a workspace").argument("[workspace_id]", "Workspace ID; defaults to the active workspace").requiredOption("--name <name>", "New workspace name").action(async (workspaceId, opts, cmd) => {
2183
+ const jsonMode = isJsonOutput(cmd);
2184
+ try {
2185
+ const result = await executeWorkspaceRename({ workspaceId, name: opts.name }, getRootOptions(cmd), deps);
2186
+ setTelemetryWorkspaceId(result.workspace_id);
2187
+ if (jsonMode) printJson(successJson("workspace_renamed", result));
2188
+ else success(`Workspace renamed to: ${result.name}`);
2189
+ } catch (error2) {
2190
+ handleWorkspaceError(error2, jsonMode, "workspace_renamed");
2191
+ }
2192
+ });
2193
+ ws.addCommand(createWorkspaceAssetsCommand(deps));
1617
2194
  return ws;
1618
2195
  }
1619
2196
 
1620
2197
  // src/commands/image.ts
1621
- import { Command as Command2, Option } from "commander";
2198
+ import { Command as Command3, Option } from "commander";
1622
2199
 
1623
2200
  // src/clients/hogi_async_generation_client.ts
1624
2201
  var DEFAULT_ASYNC_GENERATION_REQUEST_TIMEOUT_MS = 3e4;
2202
+ function responseProtocolError2(message) {
2203
+ return new CliError(ErrorCode.SERVER_ERROR, message, {
2204
+ category: "response_protocol",
2205
+ retryable: true,
2206
+ response_protocol: true
2207
+ });
2208
+ }
1625
2209
  function textPayload(result, fallback) {
1626
2210
  if (result.isError) throw errorFromMcpToolResult(result, fallback);
1627
2211
  const text = (result.content ?? []).filter((item) => item?.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n").trim();
1628
- if (!text) throw new CliError(ErrorCode.SERVER_ERROR, `${fallback}: empty response`);
2212
+ if (!text) throw responseProtocolError2(`${fallback}: empty response`);
1629
2213
  try {
1630
2214
  return JSON.parse(text);
1631
2215
  } catch {
1632
- throw new CliError(ErrorCode.SERVER_ERROR, `${fallback}: invalid JSON response`);
2216
+ throw responseProtocolError2(`${fallback}: invalid JSON response`);
1633
2217
  }
1634
2218
  }
1635
2219
  function isRecord(value) {
@@ -1639,11 +2223,11 @@ function assertStatus(value) {
1639
2223
  if (value === "pending" || value === "processing" || value === "succeeded" || value === "failed" || value === "cancelled") {
1640
2224
  return value;
1641
2225
  }
1642
- throw new CliError(ErrorCode.SERVER_ERROR, `Invalid backend async task status: ${String(value)}`);
2226
+ throw responseProtocolError2(`Invalid backend async task status: ${String(value)}`);
1643
2227
  }
1644
2228
  function assertMediaType(value) {
1645
2229
  if (value === "image" || value === "video" || value === "audio") return value;
1646
- throw new CliError(ErrorCode.SERVER_ERROR, `Invalid backend async media type: ${String(value)}`);
2230
+ throw responseProtocolError2(`Invalid backend async media type: ${String(value)}`);
1647
2231
  }
1648
2232
  var HogiAsyncGenerationClient = class {
1649
2233
  constructor(mcp) {
@@ -1668,7 +2252,7 @@ var HogiAsyncGenerationClient = class {
1668
2252
  });
1669
2253
  const payload = textPayload(result, "Async generation submission failed");
1670
2254
  if (!isRecord(payload) || typeof payload.taskId !== "string" || payload.taskId.length === 0 || typeof payload.assetId !== "string" || payload.assetId.length === 0 || typeof payload.workspaceId !== "string" || payload.workspaceId.length === 0 || typeof payload.reused !== "boolean") {
1671
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid async generation submission response");
2255
+ throw responseProtocolError2("Invalid async generation submission response");
1672
2256
  }
1673
2257
  const submission = {
1674
2258
  assetId: payload.assetId,
@@ -1680,7 +2264,7 @@ var HogiAsyncGenerationClient = class {
1680
2264
  workspaceId: payload.workspaceId
1681
2265
  };
1682
2266
  if (submission.workspaceId !== params.workspaceId || submission.mediaType !== params.mediaType) {
1683
- throw new CliError(ErrorCode.SERVER_ERROR, "Async generation submission response does not match the request");
2267
+ throw responseProtocolError2("Async generation submission response does not match the request");
1684
2268
  }
1685
2269
  logFeatureStep("async_generation", "submission.accepted", {
1686
2270
  task_id: submission.taskId,
@@ -1699,16 +2283,16 @@ var HogiAsyncGenerationClient = class {
1699
2283
  const payload = textPayload(result, "Async generation query failed");
1700
2284
  const task = isRecord(payload) && isRecord(payload.task) ? payload.task : void 0;
1701
2285
  if (!task || typeof task.taskId !== "string" || typeof task.workspaceId !== "string" || !Array.isArray(task.outputUris)) {
1702
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid async generation query response");
2286
+ throw responseProtocolError2("Invalid async generation query response");
1703
2287
  }
1704
2288
  if (!task.outputUris.every((uri) => typeof uri === "string")) {
1705
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid async generation output URI list");
2289
+ throw responseProtocolError2("Invalid async generation output URI list");
1706
2290
  }
1707
2291
  if (typeof task.progress !== "number" || !Number.isFinite(task.progress)) {
1708
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid async generation progress");
2292
+ throw responseProtocolError2("Invalid async generation progress");
1709
2293
  }
1710
2294
  if (task.error !== null && task.error !== void 0 && (!isRecord(task.error) || typeof task.error.code !== "string" || typeof task.error.message !== "string")) {
1711
- throw new CliError(ErrorCode.SERVER_ERROR, "Invalid async generation error payload");
2295
+ throw responseProtocolError2("Invalid async generation error payload");
1712
2296
  }
1713
2297
  const outputUris = task.outputUris;
1714
2298
  const response = {
@@ -1724,7 +2308,7 @@ var HogiAsyncGenerationClient = class {
1724
2308
  workspaceId: task.workspaceId
1725
2309
  };
1726
2310
  if (response.taskId !== taskId || response.workspaceId !== workspaceId) {
1727
- throw new CliError(ErrorCode.SERVER_ERROR, "Async generation query response does not match the requested task");
2311
+ throw responseProtocolError2("Async generation query response does not match the requested task");
1728
2312
  }
1729
2313
  logFeatureStep("async_generation", "query.completed", {
1730
2314
  task_id: taskId,
@@ -1743,11 +2327,13 @@ var HogiModelCatalogClient = class {
1743
2327
  apiKey;
1744
2328
  _fetch;
1745
2329
  logger;
2330
+ timeoutMs;
1746
2331
  constructor(options) {
1747
2332
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1748
2333
  this.apiKey = options.apiKey;
1749
2334
  this._fetch = options.fetch ?? globalThis.fetch;
1750
2335
  this.logger = options.logger;
2336
+ this.timeoutMs = options.timeoutMs;
1751
2337
  }
1752
2338
  async listModels() {
1753
2339
  const apiLog = startApiCall({
@@ -1758,55 +2344,61 @@ var HogiModelCatalogClient = class {
1758
2344
  }, this.logger);
1759
2345
  let response;
1760
2346
  try {
1761
- response = await this._fetch(`${this.baseUrl}/points/mcp_model_pricings`, {
1762
- method: "POST",
1763
- headers: {
1764
- "content-type": "application/json",
1765
- "accept": "application/json",
1766
- "connection": "close",
1767
- "x-hogi-api-key": this.apiKey
1768
- },
1769
- body: "{}"
2347
+ return await withRequestTimeout({
2348
+ operation: "Model catalog request",
2349
+ timeoutMs: this.timeoutMs,
2350
+ run: async (signal) => {
2351
+ response = await this._fetch(`${this.baseUrl}/points/mcp_model_pricings`, {
2352
+ method: "POST",
2353
+ headers: withClientContextHeaders({
2354
+ "content-type": "application/json",
2355
+ "accept": "application/json",
2356
+ "connection": "close",
2357
+ "x-hogi-api-key": this.apiKey
2358
+ }),
2359
+ body: "{}",
2360
+ signal
2361
+ });
2362
+ if (response.status === 401) throw new CliError(ErrorCode.API_KEY_INVALID, "Invalid API key", { status: 401 });
2363
+ if (!response.ok) {
2364
+ throw new CliError(
2365
+ ErrorCode.SERVER_ERROR,
2366
+ `Model catalog request failed: HTTP ${response.status}`,
2367
+ { status: response.status }
2368
+ );
2369
+ }
2370
+ let body;
2371
+ try {
2372
+ body = await response.json();
2373
+ } catch {
2374
+ throw new CliError(ErrorCode.SERVER_ERROR, "Invalid JSON response from model catalog");
2375
+ }
2376
+ const pricings = body.data?.pricings;
2377
+ if (body.code !== "SUCCESS" || !Array.isArray(pricings) || pricings.some((item) => !isBackendModelPricing(item))) {
2378
+ throw new CliError(ErrorCode.SERVER_ERROR, body.error ?? "Invalid model catalog response from server");
2379
+ }
2380
+ apiLog.success(response.status, { model_count: pricings.length });
2381
+ return pricings;
2382
+ }
1770
2383
  });
1771
2384
  } catch (error2) {
2385
+ if (error2 instanceof HttpRequestTimeoutError) {
2386
+ const timeoutError = new CliError(ErrorCode.NETWORK_ERROR, error2.message, {
2387
+ retryable: true,
2388
+ timeout_ms: error2.timeoutMs
2389
+ });
2390
+ apiLog.failure(timeoutError, response?.status);
2391
+ throw timeoutError;
2392
+ }
2393
+ if (error2 instanceof CliError) {
2394
+ apiLog.failure(error2, response?.status);
2395
+ throw error2;
2396
+ }
1772
2397
  const message = error2 instanceof Error ? error2.message : String(error2);
1773
2398
  const networkError = new CliError(ErrorCode.NETWORK_ERROR, `Network error: ${message}`);
1774
2399
  apiLog.failure(networkError);
1775
2400
  throw networkError;
1776
2401
  }
1777
- if (response.status === 401) {
1778
- const authError = new CliError(ErrorCode.API_KEY_INVALID, "Invalid API key", { status: 401 });
1779
- apiLog.failure(authError, response.status);
1780
- throw authError;
1781
- }
1782
- if (!response.ok) {
1783
- const serverError = new CliError(
1784
- ErrorCode.SERVER_ERROR,
1785
- `Model catalog request failed: HTTP ${response.status}`,
1786
- { status: response.status }
1787
- );
1788
- apiLog.failure(serverError, response.status);
1789
- throw serverError;
1790
- }
1791
- let body;
1792
- try {
1793
- body = await response.json();
1794
- } catch {
1795
- const parseError = new CliError(ErrorCode.SERVER_ERROR, "Invalid JSON response from model catalog");
1796
- apiLog.failure(parseError, response.status);
1797
- throw parseError;
1798
- }
1799
- const pricings = body.data?.pricings;
1800
- if (body.code !== "SUCCESS" || !Array.isArray(pricings) || pricings.some((item) => !isBackendModelPricing(item))) {
1801
- const shapeError = new CliError(
1802
- ErrorCode.SERVER_ERROR,
1803
- body.error ?? "Invalid model catalog response from server"
1804
- );
1805
- apiLog.failure(shapeError, response.status);
1806
- throw shapeError;
1807
- }
1808
- apiLog.success(response.status, { model_count: pricings.length });
1809
- return pricings;
1810
2402
  }
1811
2403
  };
1812
2404
  function isBackendModelPricing(value) {
@@ -2143,8 +2735,8 @@ function selectionCliArgs(selection) {
2143
2735
  }
2144
2736
 
2145
2737
  // src/core/task_store.ts
2146
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, appendFileSync as appendFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync3, chmodSync as chmodSync3 } from "fs";
2147
- import { join as join4, dirname as dirname2 } from "path";
2738
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, appendFileSync as appendFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync4, chmodSync as chmodSync3 } from "fs";
2739
+ import { join as join5, dirname as dirname3 } from "path";
2148
2740
  import { randomBytes as randomBytes5 } from "crypto";
2149
2741
  function generateTaskId() {
2150
2742
  return "task_" + randomBytes5(8).toString("hex");
@@ -2154,7 +2746,7 @@ var TaskStore = class {
2154
2746
  tasksPath;
2155
2747
  constructor(options) {
2156
2748
  this.configDir = options?.configDir ?? resolveConfigDir();
2157
- this.tasksPath = join4(this.configDir, "tasks.jsonl");
2749
+ this.tasksPath = join5(this.configDir, "tasks.jsonl");
2158
2750
  }
2159
2751
  /** Return the local state directory shared by tasks and preview bundles. */
2160
2752
  getConfigDir() {
@@ -2172,6 +2764,10 @@ var TaskStore = class {
2172
2764
  command: params.command,
2173
2765
  toolName: params.toolName,
2174
2766
  inputArgs: params.inputArgs,
2767
+ ...params.pendingSubmission ? { pendingSubmission: params.pendingSubmission } : {},
2768
+ ...params.credentialFingerprint ? { credentialFingerprint: params.credentialFingerprint } : {},
2769
+ ...params.backendBaseUrl ? { backendBaseUrl: params.backendBaseUrl.replace(/\/+$/, "") } : {},
2770
+ ...params.mediaType ? { mediaType: params.mediaType } : {},
2175
2771
  status: "created",
2176
2772
  resourceUris: [],
2177
2773
  createdAt: now,
@@ -2254,7 +2850,7 @@ var TaskStore = class {
2254
2850
  }
2255
2851
  /** Internal: read all tasks from the JSONL file. */
2256
2852
  _readAll() {
2257
- if (!existsSync3(this.tasksPath)) return [];
2853
+ if (!existsSync4(this.tasksPath)) return [];
2258
2854
  try {
2259
2855
  const raw = readFileSync3(this.tasksPath, "utf-8");
2260
2856
  const lines = raw.trim().split("\n").filter((l) => l.trim() !== "");
@@ -2272,8 +2868,8 @@ var TaskStore = class {
2272
2868
  }
2273
2869
  /** Ensure the parent directory exists. */
2274
2870
  _ensureDir() {
2275
- const dir = dirname2(this.tasksPath);
2276
- if (!existsSync3(dir)) {
2871
+ const dir = dirname3(this.tasksPath);
2872
+ if (!existsSync4(dir)) {
2277
2873
  mkdirSync3(dir, { recursive: true, mode: 448 });
2278
2874
  }
2279
2875
  chmodSync3(dir, 448);
@@ -2281,8 +2877,8 @@ var TaskStore = class {
2281
2877
  };
2282
2878
 
2283
2879
  // src/core/preview_store.ts
2284
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4, readdirSync, chmodSync as chmodSync4 } from "fs";
2285
- import { join as join5 } from "path";
2880
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync5, readdirSync, chmodSync as chmodSync4 } from "fs";
2881
+ import { join as join6 } from "path";
2286
2882
  import { randomBytes as randomBytes6 } from "crypto";
2287
2883
  function generateBundleId() {
2288
2884
  return "preview_" + randomBytes6(8).toString("hex");
@@ -2325,7 +2921,7 @@ var PreviewStore = class {
2325
2921
  previewsDir;
2326
2922
  constructor(options) {
2327
2923
  this.configDir = options?.configDir ?? resolveConfigDir();
2328
- this.previewsDir = join5(this.configDir, "previews");
2924
+ this.previewsDir = join6(this.configDir, "previews");
2329
2925
  }
2330
2926
  /** Return the local state directory shared by previews and task history. */
2331
2927
  getConfigDir() {
@@ -2337,13 +2933,13 @@ var PreviewStore = class {
2337
2933
  */
2338
2934
  createBundle(params) {
2339
2935
  const bundleId = generateBundleId();
2340
- const bundleDir = join5(this.previewsDir, bundleId);
2936
+ const bundleDir = join6(this.previewsDir, bundleId);
2341
2937
  mkdirSync4(bundleDir, { recursive: true, mode: 448 });
2342
- mkdirSync4(join5(bundleDir, "artifacts"), { recursive: true, mode: 448 });
2938
+ mkdirSync4(join6(bundleDir, "artifacts"), { recursive: true, mode: 448 });
2343
2939
  chmodSync4(this.configDir, 448);
2344
2940
  chmodSync4(this.previewsDir, 448);
2345
2941
  chmodSync4(bundleDir, 448);
2346
- chmodSync4(join5(bundleDir, "artifacts"), 448);
2942
+ chmodSync4(join6(bundleDir, "artifacts"), 448);
2347
2943
  const now = (/* @__PURE__ */ new Date()).toISOString();
2348
2944
  const manifest = {
2349
2945
  schema_version: 1,
@@ -2384,8 +2980,8 @@ var PreviewStore = class {
2384
2980
  agent_summary: agentSummary,
2385
2981
  next: nextCommands
2386
2982
  };
2387
- const manifestPath = join5(bundleDir, "manifest.json");
2388
- const summaryPath = join5(bundleDir, "summary.json");
2983
+ const manifestPath = join6(bundleDir, "manifest.json");
2984
+ const summaryPath = join6(bundleDir, "summary.json");
2389
2985
  writeFileSync4(manifestPath, JSON.stringify(manifest, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
2390
2986
  writeFileSync4(summaryPath, JSON.stringify(summary, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
2391
2987
  chmodSync4(manifestPath, 384);
@@ -2402,10 +2998,10 @@ var PreviewStore = class {
2402
2998
  * Get a bundle by its bundle ID.
2403
2999
  */
2404
3000
  getBundle(bundleId) {
2405
- const bundleDir = join5(this.previewsDir, bundleId);
2406
- const manifestPath = join5(bundleDir, "manifest.json");
2407
- const summaryPath = join5(bundleDir, "summary.json");
2408
- if (!existsSync4(manifestPath) || !existsSync4(summaryPath)) {
3001
+ const bundleDir = join6(this.previewsDir, bundleId);
3002
+ const manifestPath = join6(bundleDir, "manifest.json");
3003
+ const summaryPath = join6(bundleDir, "summary.json");
3004
+ if (!existsSync5(manifestPath) || !existsSync5(summaryPath)) {
2409
3005
  logFeatureStep("preview_store", "preview.read", { bundle_id: bundleId, found: false });
2410
3006
  return void 0;
2411
3007
  }
@@ -2455,13 +3051,13 @@ var PreviewStore = class {
2455
3051
  * Scan the previews directory and read all manifest.json files.
2456
3052
  */
2457
3053
  _scanBundles() {
2458
- if (!existsSync4(this.previewsDir)) return [];
3054
+ if (!existsSync5(this.previewsDir)) return [];
2459
3055
  const entries = readdirSync(this.previewsDir, { withFileTypes: true });
2460
3056
  const manifests = [];
2461
3057
  for (const entry of entries) {
2462
3058
  if (!entry.isDirectory()) continue;
2463
- const manifestPath = join5(this.previewsDir, entry.name, "manifest.json");
2464
- if (!existsSync4(manifestPath)) continue;
3059
+ const manifestPath = join6(this.previewsDir, entry.name, "manifest.json");
3060
+ if (!existsSync5(manifestPath)) continue;
2465
3061
  try {
2466
3062
  const manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
2467
3063
  manifests.push(manifest);
@@ -2519,6 +3115,46 @@ function localStatusForBackend(status) {
2519
3115
  function isTerminalTask(task) {
2520
3116
  return task.status === "succeeded" || task.status === "failed" || task.status === "cancelled";
2521
3117
  }
3118
+ function isAmbiguousSubmissionError(error2) {
3119
+ if (!(error2 instanceof CliError)) return false;
3120
+ if (error2.code === ErrorCode.NETWORK_ERROR || error2.code === ErrorCode.GENERATION_TIMEOUT) return true;
3121
+ const details = errorDetails(error2);
3122
+ return details.response_protocol === true || details.request_outcome === "unknown";
3123
+ }
3124
+ function errorDetails(error2) {
3125
+ return error2.details && typeof error2.details === "object" && !Array.isArray(error2.details) ? error2.details : {};
3126
+ }
3127
+ function persistAsyncSubmissionCommandError(params) {
3128
+ if (params.taskStore.get(params.task.id)?.backendTaskId) return params.error;
3129
+ const code = params.error instanceof CliError ? params.error.code : ErrorCode.GENERATION_FAILED;
3130
+ const message = params.error instanceof Error ? params.error.message : String(params.error);
3131
+ if (isAmbiguousSubmissionError(params.error)) {
3132
+ params.taskStore.update(params.task.id, {
3133
+ status: "submitting",
3134
+ error: { code, message }
3135
+ });
3136
+ return new CliError(code, message, {
3137
+ ...errorDetails(params.error),
3138
+ submission_outcome: "unknown",
3139
+ local_task_id: params.task.id,
3140
+ task_status: "submitting",
3141
+ retry_command: `oiioii task retry ${params.task.id}`
3142
+ });
3143
+ }
3144
+ const failedTask = params.taskStore.update(params.task.id, {
3145
+ status: "failed",
3146
+ error: { code, message }
3147
+ }) ?? params.task;
3148
+ createGenerationPreview({
3149
+ taskStore: params.taskStore,
3150
+ previewStore: params.previewStore,
3151
+ task: failedTask,
3152
+ resources: [],
3153
+ status: "failed",
3154
+ error: { code, message }
3155
+ });
3156
+ return params.error;
3157
+ }
2522
3158
  function assertTaskOutputUris(task) {
2523
3159
  const prefix = URI_PREFIX[task.mediaType];
2524
3160
  const mismatched = task.outputUris.find((uri) => !uri.startsWith(prefix));
@@ -2604,6 +3240,38 @@ async function submitAsyncGeneration(params) {
2604
3240
  }
2605
3241
  return { submission, ...synced };
2606
3242
  }
3243
+ async function retryAsyncGenerationSubmission(params) {
3244
+ if (params.task.backendTaskId) {
3245
+ throw new CliError(ErrorCode.INVALID_ARGUMENT, `Task ${params.task.id} has already been accepted by the backend.`);
3246
+ }
3247
+ if (params.task.status !== "submitting" || !params.task.pendingSubmission || !params.task.error) {
3248
+ throw new CliError(
3249
+ ErrorCode.INVALID_ARGUMENT,
3250
+ `Task ${params.task.id} is not an ambiguous submission that can be retried.`
3251
+ );
3252
+ }
3253
+ const pending = params.task.pendingSubmission;
3254
+ try {
3255
+ return await submitAsyncGeneration({
3256
+ client: params.client,
3257
+ taskStore: params.taskStore,
3258
+ previewStore: params.previewStore,
3259
+ task: params.task,
3260
+ mediaType: pending.mediaType,
3261
+ prompt: pending.prompt,
3262
+ ...pending.model ? { model: pending.model } : {},
3263
+ backendBaseUrl: params.backendBaseUrl,
3264
+ input: pending.input
3265
+ });
3266
+ } catch (error2) {
3267
+ throw persistAsyncSubmissionCommandError({
3268
+ taskStore: params.taskStore,
3269
+ previewStore: params.previewStore,
3270
+ task: params.task,
3271
+ error: error2
3272
+ });
3273
+ }
3274
+ }
2607
3275
  async function syncAsyncGenerationTask(params) {
2608
3276
  if (!params.task.backendTaskId) {
2609
3277
  if (isTerminalTask(params.task)) return { task: params.task };
@@ -2650,6 +3318,12 @@ async function syncAsyncGenerationTask(params) {
2650
3318
  return { task: updated, ...previewWarning ? { previewWarning } : {} };
2651
3319
  }
2652
3320
 
3321
+ // src/core/credential_fingerprint.ts
3322
+ import { createHash } from "crypto";
3323
+ function credentialFingerprint(apiKey) {
3324
+ return `sha256:${createHash("sha256").update("oiioii-cli-task-credential-v1\0").update(apiKey).digest("hex")}`;
3325
+ }
3326
+
2653
3327
  // src/core/video_model_contract.ts
2654
3328
  var VIDEO_GENERATE_MODES = [
2655
3329
  "text2Video",
@@ -3238,6 +3912,12 @@ async function executeImageText(params, workspaceId, apiKey, baseUrl, deps = {})
3238
3912
  ...prepared.args,
3239
3913
  workspace_id: workspaceId
3240
3914
  };
3915
+ const pendingSubmission = {
3916
+ mediaType: "image",
3917
+ prompt: params.prompt,
3918
+ model: selection.variant,
3919
+ input: buildAsyncImageInput(prepared.args)
3920
+ };
3241
3921
  const commandStr = `oiioii image text --prompt "${params.prompt}" ${selectionCliArgs(selection)}${params.aspectRatio ? ` --aspect-ratio ${params.aspectRatio}` : ""}${params.size ? ` --size ${params.size}` : ""}${params.resolution ? ` --resolution ${params.resolution}` : ""}`;
3242
3922
  const task = taskStore.create({
3243
3923
  workspaceId,
@@ -3256,7 +3936,11 @@ async function executeImageText(params, workspaceId, apiKey, baseUrl, deps = {})
3256
3936
  size: toolArgs.size,
3257
3937
  resolution: toolArgs.resolution
3258
3938
  }
3259
- }
3939
+ },
3940
+ pendingSubmission,
3941
+ credentialFingerprint: credentialFingerprint(apiKey),
3942
+ backendBaseUrl: baseUrl,
3943
+ mediaType: "image"
3260
3944
  });
3261
3945
  taskStore.update(task.id, { status: "submitting" });
3262
3946
  const client = new HogiMcpClient({
@@ -3275,7 +3959,7 @@ async function executeImageText(params, workspaceId, apiKey, baseUrl, deps = {})
3275
3959
  prompt: params.prompt,
3276
3960
  model: selection.variant,
3277
3961
  backendBaseUrl: baseUrl,
3278
- input: buildAsyncImageInput(prepared.args)
3962
+ input: pendingSubmission.input
3279
3963
  });
3280
3964
  const artifacts = submitted.task.resourceUris.map((uri) => ({ type: "image", uri, mime_type: "image/jpeg" }));
3281
3965
  const warnings = submitted.previewWarning ? [submitted.previewWarning] : [];
@@ -3297,13 +3981,8 @@ async function executeImageText(params, workspaceId, apiKey, baseUrl, deps = {})
3297
3981
  };
3298
3982
  } catch (err) {
3299
3983
  const errCode = err instanceof CliError ? err.code : ErrorCode.GENERATION_FAILED;
3300
- const errMsg = err instanceof Error ? err.message : String(err);
3301
- if (!taskStore.get(task.id)?.backendTaskId) {
3302
- taskStore.update(task.id, { status: "failed", error: { code: errCode, message: errMsg } });
3303
- createGenerationPreview({ taskStore, previewStore: deps.previewStore, task, resources: [], status: "failed", error: { code: errCode, message: errMsg } });
3304
- }
3305
3984
  logFeatureStep("image", "generation.command_failed", { task_id: task.id, backend_task_cached: taskStore.get(task.id)?.backendTaskId !== void 0, error_code: errCode }, "error");
3306
- throw err;
3985
+ throw persistAsyncSubmissionCommandError({ taskStore, previewStore: deps.previewStore, task, error: err });
3307
3986
  }
3308
3987
  }
3309
3988
  async function executeImageEdit(params, workspaceId, apiKey, baseUrl, deps = {}) {
@@ -3322,6 +4001,12 @@ async function executeImageEdit(params, workspaceId, apiKey, baseUrl, deps = {})
3322
4001
  ...prepared.args,
3323
4002
  workspace_id: workspaceId
3324
4003
  };
4004
+ const pendingSubmission = {
4005
+ mediaType: "image",
4006
+ prompt: params.prompt,
4007
+ model: selection.variant,
4008
+ input: buildAsyncImageInput(prepared.args)
4009
+ };
3325
4010
  const commandStr = `oiioii image edit --image ${params.image} --prompt "${params.prompt}" ${selectionCliArgs(selection)}${params.aspectRatio ? ` --aspect-ratio ${params.aspectRatio}` : ""}${params.size ? ` --size ${params.size}` : ""}${params.resolution ? ` --resolution ${params.resolution}` : ""}`;
3326
4011
  const task = taskStore.create({
3327
4012
  workspaceId,
@@ -3342,7 +4027,11 @@ async function executeImageEdit(params, workspaceId, apiKey, baseUrl, deps = {})
3342
4027
  size: toolArgs.size,
3343
4028
  resolution: toolArgs.resolution
3344
4029
  }
3345
- }
4030
+ },
4031
+ pendingSubmission,
4032
+ credentialFingerprint: credentialFingerprint(apiKey),
4033
+ backendBaseUrl: baseUrl,
4034
+ mediaType: "image"
3346
4035
  });
3347
4036
  taskStore.update(task.id, { status: "submitting" });
3348
4037
  const client = new HogiMcpClient({
@@ -3361,7 +4050,7 @@ async function executeImageEdit(params, workspaceId, apiKey, baseUrl, deps = {})
3361
4050
  prompt: params.prompt,
3362
4051
  model: selection.variant,
3363
4052
  backendBaseUrl: baseUrl,
3364
- input: buildAsyncImageInput(prepared.args)
4053
+ input: pendingSubmission.input
3365
4054
  });
3366
4055
  const artifacts = submitted.task.resourceUris.map((uri) => ({ type: "image", uri, mime_type: "image/jpeg" }));
3367
4056
  const warnings = submitted.previewWarning ? [submitted.previewWarning] : [];
@@ -3383,17 +4072,12 @@ async function executeImageEdit(params, workspaceId, apiKey, baseUrl, deps = {})
3383
4072
  };
3384
4073
  } catch (err) {
3385
4074
  const errCode = err instanceof CliError ? err.code : ErrorCode.GENERATION_FAILED;
3386
- const errMsg = err instanceof Error ? err.message : String(err);
3387
- if (!taskStore.get(task.id)?.backendTaskId) {
3388
- taskStore.update(task.id, { status: "failed", error: { code: errCode, message: errMsg } });
3389
- createGenerationPreview({ taskStore, previewStore: deps.previewStore, task, resources: [], status: "failed", error: { code: errCode, message: errMsg } });
3390
- }
3391
4075
  logFeatureStep("image", "generation.command_failed", { task_id: task.id, backend_task_cached: taskStore.get(task.id)?.backendTaskId !== void 0, error_code: errCode }, "error");
3392
- throw err;
4076
+ throw persistAsyncSubmissionCommandError({ taskStore, previewStore: deps.previewStore, task, error: err });
3393
4077
  }
3394
4078
  }
3395
4079
  function createImageCommand(deps = {}) {
3396
- const image = new Command2("image").description("Image generation commands");
4080
+ const image = new Command3("image").description("Image generation commands");
3397
4081
  image.command("text").description("Generate image from text prompt").requiredOption("--prompt <text>", "Image description").option("--model <model>", "Model name", DEFAULT_IMAGE_MODEL).option("--model-id <id>", "Exact model ID from inspect models").option("--variant <variant>", "Exact model variant from inspect models").option("--aspect-ratio <ratio>", "Image aspect ratio (defaults to the selected model catalog)").addOption(new Option("--size <size>", "Deprecated <width>x<height> compatibility option").hideHelp()).option("--resolution <resolution>", "Image resolution (defaults to the selected model catalog)").option("--dry-run", "Validate and preview the resolved request without submitting").action(async (opts, cmd) => {
3398
4082
  const jsonMode = isJsonMode(cmd);
3399
4083
  const rootOpts = getRootOpts(cmd);
@@ -3584,7 +4268,7 @@ function createImageCommand(deps = {}) {
3584
4268
  }
3585
4269
 
3586
4270
  // src/commands/video.ts
3587
- import { Command as Command3 } from "commander";
4271
+ import { Command as Command4 } from "commander";
3588
4272
  var GROK_IMAGINE_TOOL_NAME = "generate_video_grok_imagine";
3589
4273
  var isJsonMode2 = isJsonOutput;
3590
4274
  var getRootOpts2 = getRootOptions;
@@ -3742,6 +4426,12 @@ async function executeVideoText(params, workspaceId, apiKey, baseUrl, deps = {})
3742
4426
  ...prepared.args,
3743
4427
  workspace_id: workspaceId
3744
4428
  };
4429
+ const pendingSubmission = {
4430
+ mediaType: "video",
4431
+ prompt: params.prompt,
4432
+ model: selection.variant,
4433
+ input: buildAsyncVideoInput(prepared.args)
4434
+ };
3745
4435
  const commandStr = `oiioii video text --prompt "${params.prompt}" ${selectionCliArgs(selection)} --duration ${duration} --aspect-ratio ${aspectRatio} --resolution ${resolution}`;
3746
4436
  const task = taskStore.create({
3747
4437
  workspaceId,
@@ -3758,7 +4448,11 @@ async function executeVideoText(params, workspaceId, apiKey, baseUrl, deps = {})
3758
4448
  aspectRatio,
3759
4449
  duration,
3760
4450
  toolArgs
3761
- }
4451
+ },
4452
+ pendingSubmission,
4453
+ credentialFingerprint: credentialFingerprint(apiKey),
4454
+ backendBaseUrl: baseUrl,
4455
+ mediaType: "video"
3762
4456
  });
3763
4457
  taskStore.update(task.id, { status: "submitting" });
3764
4458
  const client = new HogiMcpClient({
@@ -3777,7 +4471,7 @@ async function executeVideoText(params, workspaceId, apiKey, baseUrl, deps = {})
3777
4471
  prompt: params.prompt,
3778
4472
  model: selection.variant,
3779
4473
  backendBaseUrl: baseUrl,
3780
- input: buildAsyncVideoInput(prepared.args)
4474
+ input: pendingSubmission.input
3781
4475
  });
3782
4476
  const artifacts = submitted.task.resourceUris.map((uri) => ({ type: "video", uri, mime_type: "video/mp4" }));
3783
4477
  const warnings = submitted.previewWarning ? [submitted.previewWarning] : [];
@@ -3800,13 +4494,8 @@ async function executeVideoText(params, workspaceId, apiKey, baseUrl, deps = {})
3800
4494
  };
3801
4495
  } catch (err) {
3802
4496
  const errCode = err instanceof CliError ? err.code : ErrorCode.GENERATION_FAILED;
3803
- const errMsg = err instanceof Error ? err.message : String(err);
3804
- if (!taskStore.get(task.id)?.backendTaskId) {
3805
- taskStore.update(task.id, { status: "failed", error: { code: errCode, message: errMsg } });
3806
- createGenerationPreview({ taskStore, previewStore: deps.previewStore, task, resources: [], status: "failed", error: { code: errCode, message: errMsg } });
3807
- }
3808
4497
  logFeatureStep("video", "generation.command_failed", { task_id: task.id, backend_task_cached: taskStore.get(task.id)?.backendTaskId !== void 0, error_code: errCode }, "error");
3809
- throw err;
4498
+ throw persistAsyncSubmissionCommandError({ taskStore, previewStore: deps.previewStore, task, error: err });
3810
4499
  }
3811
4500
  }
3812
4501
  async function executeVideoImage(params, workspaceId, apiKey, baseUrl, deps = {}) {
@@ -3829,6 +4518,12 @@ async function executeVideoImage(params, workspaceId, apiKey, baseUrl, deps = {}
3829
4518
  ...prepared.args,
3830
4519
  workspace_id: workspaceId
3831
4520
  };
4521
+ const pendingSubmission = {
4522
+ mediaType: "video",
4523
+ prompt: params.prompt,
4524
+ model: selection.variant,
4525
+ input: buildAsyncVideoInput(prepared.args)
4526
+ };
3832
4527
  const referenceArgs = [
3833
4528
  ...preparedReferences.references.images.map((uri) => `--image ${uri}`),
3834
4529
  ...preparedReferences.references.videos.map((uri) => `--video ${uri}`),
@@ -3854,7 +4549,11 @@ async function executeVideoImage(params, workspaceId, apiKey, baseUrl, deps = {}
3854
4549
  workspaceId,
3855
4550
  duration,
3856
4551
  toolArgs
3857
- }
4552
+ },
4553
+ pendingSubmission,
4554
+ credentialFingerprint: credentialFingerprint(apiKey),
4555
+ backendBaseUrl: baseUrl,
4556
+ mediaType: "video"
3858
4557
  });
3859
4558
  taskStore.update(task.id, { status: "submitting" });
3860
4559
  const client = new HogiMcpClient({
@@ -3873,7 +4572,7 @@ async function executeVideoImage(params, workspaceId, apiKey, baseUrl, deps = {}
3873
4572
  prompt: params.prompt,
3874
4573
  model: selection.variant,
3875
4574
  backendBaseUrl: baseUrl,
3876
- input: buildAsyncVideoInput(prepared.args)
4575
+ input: pendingSubmission.input
3877
4576
  });
3878
4577
  const artifacts = submitted.task.resourceUris.map((uri) => ({ type: "video", uri, mime_type: "video/mp4" }));
3879
4578
  const warnings = submitted.previewWarning ? [submitted.previewWarning] : [];
@@ -3896,17 +4595,12 @@ async function executeVideoImage(params, workspaceId, apiKey, baseUrl, deps = {}
3896
4595
  };
3897
4596
  } catch (err) {
3898
4597
  const errCode = err instanceof CliError ? err.code : ErrorCode.GENERATION_FAILED;
3899
- const errMsg = err instanceof Error ? err.message : String(err);
3900
- if (!taskStore.get(task.id)?.backendTaskId) {
3901
- taskStore.update(task.id, { status: "failed", error: { code: errCode, message: errMsg } });
3902
- createGenerationPreview({ taskStore, previewStore: deps.previewStore, task, resources: [], status: "failed", error: { code: errCode, message: errMsg } });
3903
- }
3904
4598
  logFeatureStep("video", "generation.command_failed", { task_id: task.id, backend_task_cached: taskStore.get(task.id)?.backendTaskId !== void 0, error_code: errCode }, "error");
3905
- throw err;
4599
+ throw persistAsyncSubmissionCommandError({ taskStore, previewStore: deps.previewStore, task, error: err });
3906
4600
  }
3907
4601
  }
3908
4602
  function createVideoCommand(deps = {}) {
3909
- const video = new Command3("video").description("Video generation commands");
4603
+ const video = new Command4("video").description("Video generation commands");
3910
4604
  video.command("text").description("Generate video from text prompt").requiredOption("--prompt <text>", "Video description").option("--model <model>", "Model name", DEFAULT_VIDEO_MODEL).option("--model-id <id>", "Exact model ID from inspect models").option("--variant <variant>", "Exact model variant from inspect models").option("--duration <seconds>", "Duration in seconds (defaults to the selected model catalog)").option("--aspect-ratio <ratio>", "Aspect ratio (defaults to the selected model catalog)").option("--resolution <resolution>", "Video resolution (defaults to the selected model catalog)").option("--dry-run", "Validate and preview the resolved request without submitting").action(async (opts, cmd) => {
3911
4605
  const jsonMode = isJsonMode2(cmd);
3912
4606
  const rootOpts = getRootOpts2(cmd);
@@ -4193,9 +4887,10 @@ function createVideoCommand(deps = {}) {
4193
4887
  }
4194
4888
 
4195
4889
  // src/commands/audio.ts
4196
- import { Command as Command4 } from "commander";
4890
+ import { Command as Command5 } from "commander";
4197
4891
  var SEED_AUDIO_TOOL = "generate_music_seed_audio";
4198
4892
  var SUNO_TOOL = "generate_music_suno";
4893
+ var SUNO_VARIANTS = ["chirp-v4-5", "chirp-v5", "chirp-v5-5"];
4199
4894
  var AUDIO_FORMATS = ["wav", "mp3", "pcm", "ogg_opus"];
4200
4895
  function requireWorkspace3(rootOpts, configStore) {
4201
4896
  const flag = rootOpts.workspace;
@@ -4275,8 +4970,8 @@ function buildAudioToolArgs(toolName, params, variant, workspaceId) {
4275
4970
  };
4276
4971
  }
4277
4972
  if (toolName === SUNO_TOOL) {
4278
- if (variant && variant !== "chirp-v4-5" && variant !== "chirp-v5") {
4279
- throw audioConstraint(`Suno only supports variants chirp-v4-5 and chirp-v5, received ${variant}.`, { field: "variant", received: variant, supported: ["chirp-v4-5", "chirp-v5"] });
4973
+ if (variant && !SUNO_VARIANTS.some((supportedVariant) => supportedVariant === variant)) {
4974
+ throw audioConstraint(`Suno only supports variants ${SUNO_VARIANTS.join(", ")}, received ${variant}.`, { field: "variant", received: variant, supported: [...SUNO_VARIANTS] });
4280
4975
  }
4281
4976
  if (audioUrls.length || referenceImages.length || params.format || params.enableSubtitle !== void 0 || params.loudnessRate !== void 0 || params.pitchRate !== void 0 || params.sampleRate !== void 0 || params.speechRate !== void 0) {
4282
4977
  throw audioConstraint("Reference and audio configuration options are only valid for Seed Audio.", { field: "seed_audio_options" });
@@ -4290,8 +4985,8 @@ function buildAudioToolArgs(toolName, params, variant, workspaceId) {
4290
4985
  return {
4291
4986
  args: {
4292
4987
  custom,
4293
- // HOE currently validates `prompt` as required even though the backend
4294
- // source schema marks it optional for description mode. Send the
4988
+ // The backend validates `prompt` as required even though its source
4989
+ // schema marks it optional for description mode. Send the
4295
4990
  // user prompt in both fields for compatibility; current backends use
4296
4991
  // gptDescriptionPrompt when custom=false and safely ignore prompt.
4297
4992
  prompt,
@@ -4381,6 +5076,12 @@ async function executeAudioGenerate(params, workspaceId, apiKey, baseUrl, deps =
4381
5076
  });
4382
5077
  const commandName = expectedFamily ?? "generate";
4383
5078
  const command = `oiioii audio ${commandName} --prompt "${params.prompt}" ${selectionCliArgs(selection)}`;
5079
+ const pendingSubmission = {
5080
+ mediaType: "audio",
5081
+ prompt: params.prompt,
5082
+ model: selection.variant,
5083
+ input: prepared.args
5084
+ };
4384
5085
  const task = taskStore.create({
4385
5086
  workspaceId,
4386
5087
  command,
@@ -4402,7 +5103,11 @@ async function executeAudioGenerate(params, workspaceId, apiKey, baseUrl, deps =
4402
5103
  custom: params.custom,
4403
5104
  instrumental: params.instrumental
4404
5105
  }
4405
- }
5106
+ },
5107
+ pendingSubmission,
5108
+ credentialFingerprint: credentialFingerprint(apiKey),
5109
+ backendBaseUrl: baseUrl,
5110
+ mediaType: "audio"
4406
5111
  });
4407
5112
  taskStore.update(task.id, { status: "submitting" });
4408
5113
  const client = new HogiMcpClient({
@@ -4421,7 +5126,7 @@ async function executeAudioGenerate(params, workspaceId, apiKey, baseUrl, deps =
4421
5126
  prompt: params.prompt,
4422
5127
  model: selection.variant,
4423
5128
  backendBaseUrl: baseUrl,
4424
- input: prepared.args
5129
+ input: pendingSubmission.input
4425
5130
  });
4426
5131
  const artifacts = submitted.task.resourceUris.map((uri) => ({ type: "audio", uri, mime_type: "audio/mpeg" }));
4427
5132
  const warnings = submitted.previewWarning ? [submitted.previewWarning] : [];
@@ -4442,13 +5147,8 @@ async function executeAudioGenerate(params, workspaceId, apiKey, baseUrl, deps =
4442
5147
  };
4443
5148
  } catch (error2) {
4444
5149
  const code = error2 instanceof CliError ? error2.code : ErrorCode.GENERATION_FAILED;
4445
- const message = error2 instanceof Error ? error2.message : String(error2);
4446
- if (!taskStore.get(task.id)?.backendTaskId) {
4447
- taskStore.update(task.id, { status: "failed", error: { code, message } });
4448
- createGenerationPreview({ taskStore, previewStore: deps.previewStore, task, resources: [], status: "failed", error: { code, message } });
4449
- }
4450
5150
  logFeatureStep("audio", "generation.command_failed", { task_id: task.id, backend_task_cached: taskStore.get(task.id)?.backendTaskId !== void 0, error_code: code }, "error");
4451
- throw error2;
5151
+ throw persistAsyncSubmissionCommandError({ taskStore, previewStore: deps.previewStore, task, error: error2 });
4452
5152
  }
4453
5153
  }
4454
5154
  function optionalNumber(value, name) {
@@ -4563,7 +5263,7 @@ function addSunoOptions(command) {
4563
5263
  return command.option("--model-id <id>", "Exact Suno model ID from inspect models").option("--variant <variant>", "Exact Suno variant from inspect models").option("--custom", "Use custom lyrics/style mode").option("--tags <text>", "Custom-mode music style tags").option("--instrumental", "Generate instrumental music").option("--auto-generate-video", "Ask Suno to also generate its provider-side video").option("--dry-run", "Validate and preview the resolved request without submitting");
4564
5264
  }
4565
5265
  function createAudioCommand(deps = {}) {
4566
- const audio = new Command4("audio").description("Audio and music generation commands");
5266
+ const audio = new Command5("audio").description("Audio and music generation commands");
4567
5267
  audio.command("generate").description("Generate audio with Seed Audio or Suno").requiredOption("--prompt <text>", "Audio description; interpreted as lyrics in Suno custom mode").option("--model <model>", "Model name (seed-audio or suno)", DEFAULT_AUDIO_MODEL).option("--model-id <id>", "Exact model ID from inspect models").option("--variant <variant>", "Exact model variant from inspect models").option("--audio <uri...>", "Seed Audio reference audio URIs (maximum 3)").option("--image <uri...>", "Seed Audio reference image URI (maximum 1)").option("--format <format>", `Seed Audio output format (${AUDIO_FORMATS.join(", ")})`).option("--subtitle", "Request Seed Audio subtitle metadata").option("--loudness-rate <number>", "Seed Audio loudness adjustment (-50 to 100)").option("--pitch-rate <number>", "Seed Audio pitch adjustment (-12 to 12)").option("--sample-rate <number>", "Seed Audio output sample rate").option("--speech-rate <number>", "Seed Audio speech rate adjustment (-50 to 100)").option("--custom", "Use Suno custom lyrics/style mode").option("--tags <text>", "Suno custom-mode music style tags").option("--instrumental", "Generate instrumental music with Suno").option("--auto-generate-video", "Ask Suno to also generate its provider-side video").option("--dry-run", "Validate and preview the resolved request without submitting").action(async (opts, cmd) => await runAudioCommand(opts, cmd, deps));
4568
5268
  addSeedAudioOptions(audio.command("seed-audio").description("Generate speech or audio with Seed Audio").requiredOption("--prompt <text>", "Audio description")).action(async (opts, cmd) => await runAudioCommand(opts, cmd, deps, "seed-audio"));
4569
5269
  addSunoOptions(audio.command("suno").description("Generate music with Suno").requiredOption("--prompt <text>", "Music description; interpreted as lyrics in custom mode")).action(async (opts, cmd) => await runAudioCommand(opts, cmd, deps, "suno"));
@@ -4571,7 +5271,7 @@ function createAudioCommand(deps = {}) {
4571
5271
  }
4572
5272
 
4573
5273
  // src/commands/task.ts
4574
- import { Command as Command5 } from "commander";
5274
+ import { Command as Command6 } from "commander";
4575
5275
  function parsePositiveInteger(value, label) {
4576
5276
  const parsed = Number(value);
4577
5277
  if (!Number.isInteger(parsed) || parsed <= 0) {
@@ -4582,6 +5282,24 @@ function parsePositiveInteger(value, label) {
4582
5282
  function printTask(task) {
4583
5283
  info(`${task.backendTaskId ?? task.id} ${task.status} ${task.toolName} ${task.updatedAt}`);
4584
5284
  }
5285
+ function taskForOutput(task) {
5286
+ const visible = { ...task };
5287
+ delete visible.credentialFingerprint;
5288
+ return visible;
5289
+ }
5290
+ function taskRetryRequiredError(task) {
5291
+ return new CliError(
5292
+ ErrorCode.TASK_RETRY_REQUIRED,
5293
+ `Task ${task.id} has an unknown submission outcome. Retry it with the original idempotency key.`,
5294
+ {
5295
+ retryable: true,
5296
+ local_task_id: task.id,
5297
+ task_status: task.status,
5298
+ retry_command: `oiioii task retry ${task.id}`,
5299
+ ...task.error ? { last_error: task.error } : {}
5300
+ }
5301
+ );
5302
+ }
4585
5303
  function handleTaskError(error2, jsonMode, type) {
4586
5304
  const cliError = error2 instanceof CliError ? error2 : new CliError(ErrorCode.SERVER_ERROR, error2 instanceof Error ? error2.message : String(error2));
4587
5305
  reportFeatureFailure(cliError.code, cliError.message);
@@ -4589,21 +5307,21 @@ function handleTaskError(error2, jsonMode, type) {
4589
5307
  else error(cliError.message);
4590
5308
  process.exitCode = 1;
4591
5309
  }
4592
- function createAsyncClient(cmd, deps, backendBaseUrl) {
5310
+ function createAsyncClient(cmd, deps, backendBaseUrl, requestTimeoutMs = 3e4) {
4593
5311
  const configStore = deps.configStore ?? new FileConfigStore();
4594
5312
  const auth = requireApiKeyAuth(getRootOptions(cmd), configStore);
4595
5313
  return new HogiAsyncGenerationClient(new HogiMcpClient({
4596
5314
  baseUrl: backendBaseUrl ?? auth.baseUrl,
4597
5315
  apiKey: auth.apiKey,
4598
5316
  fetch: deps.fetch,
4599
- timeoutMs: 3e4
5317
+ timeoutMs: requestTimeoutMs
4600
5318
  }));
4601
5319
  }
4602
- async function refreshTask(taskItem, cmd, deps) {
5320
+ async function refreshTask(taskItem, cmd, deps, requestTimeoutMs) {
4603
5321
  if (!taskItem.backendTaskId || isTerminalTask(taskItem)) return taskItem;
4604
5322
  const taskStore = deps.taskStore ?? new TaskStore();
4605
5323
  const result = await syncAsyncGenerationTask({
4606
- client: createAsyncClient(cmd, deps, taskItem.backendBaseUrl),
5324
+ client: createAsyncClient(cmd, deps, taskItem.backendBaseUrl, requestTimeoutMs),
4607
5325
  taskStore,
4608
5326
  previewStore: deps.previewStore,
4609
5327
  task: taskItem
@@ -4611,7 +5329,7 @@ async function refreshTask(taskItem, cmd, deps) {
4611
5329
  return result.task;
4612
5330
  }
4613
5331
  function createTaskCommand(deps = {}) {
4614
- const task = new Command5("task").description("Generation task management");
5332
+ const task = new Command6("task").description("Generation task management");
4615
5333
  task.command("list").description("List recent tasks").option("--limit <count>", "Maximum tasks to return", "20").action((opts, cmd) => {
4616
5334
  const jsonMode = isJsonOutput(cmd);
4617
5335
  try {
@@ -4619,7 +5337,7 @@ function createTaskCommand(deps = {}) {
4619
5337
  const tasks = (deps.taskStore ?? new TaskStore()).list(limit);
4620
5338
  recordTelemetryBusinessInfo({ task_count: tasks.length, limit });
4621
5339
  logFeatureStep("task", "task.listed", { count: tasks.length, limit });
4622
- if (jsonMode) printJson(successJson("task_list", { tasks, count: tasks.length }));
5340
+ if (jsonMode) printJson(successJson("task_list", { tasks: tasks.map(taskForOutput), count: tasks.length }));
4623
5341
  else {
4624
5342
  heading("Recent Tasks");
4625
5343
  for (const taskItem of tasks) printTask(taskItem);
@@ -4638,12 +5356,56 @@ function createTaskCommand(deps = {}) {
4638
5356
  taskItem = await refreshTask(taskItem, cmd, { ...deps, taskStore: store });
4639
5357
  recordTelemetryBusinessInfo({ task_status: taskItem.status });
4640
5358
  logFeatureStep("task", "task.status_read", { task_id: id, status: taskItem.status });
4641
- if (jsonMode) printJson(successJson("task_status", { task: taskItem }));
5359
+ if (jsonMode) printJson(successJson("task_status", { task: taskForOutput(taskItem) }));
4642
5360
  else printTask(taskItem);
4643
5361
  } catch (error2) {
4644
5362
  handleTaskError(error2, jsonMode, "task_status");
4645
5363
  }
4646
5364
  });
5365
+ task.command("retry").description("Retry an ambiguous generation submission with its original idempotency key").argument("<task_id>", "Local task ID").action(async (id, _opts, cmd) => {
5366
+ const jsonMode = isJsonOutput(cmd);
5367
+ try {
5368
+ const store = deps.taskStore ?? new TaskStore();
5369
+ const taskItem = store.get(id);
5370
+ if (!taskItem) throw new CliError(ErrorCode.TASK_NOT_FOUND, `Task not found: ${id}`);
5371
+ const configStore = deps.configStore ?? new FileConfigStore();
5372
+ const auth = requireApiKeyAuth(getRootOptions(cmd), configStore);
5373
+ if (!taskItem.credentialFingerprint || taskItem.credentialFingerprint !== credentialFingerprint(auth.apiKey)) {
5374
+ throw new CliError(
5375
+ ErrorCode.AUTH_INVALID,
5376
+ `The current API key does not match the credential used to submit task ${taskItem.id}.`,
5377
+ {
5378
+ retryable: false,
5379
+ local_task_id: taskItem.id,
5380
+ reason: taskItem.credentialFingerprint ? "credential_mismatch" : "credential_binding_missing"
5381
+ }
5382
+ );
5383
+ }
5384
+ const backendBaseUrl = taskItem.backendBaseUrl ?? auth.baseUrl;
5385
+ const result = await retryAsyncGenerationSubmission({
5386
+ client: new HogiAsyncGenerationClient(new HogiMcpClient({
5387
+ baseUrl: backendBaseUrl,
5388
+ apiKey: auth.apiKey,
5389
+ fetch: deps.fetch
5390
+ })),
5391
+ taskStore: store,
5392
+ previewStore: deps.previewStore,
5393
+ task: taskItem,
5394
+ backendBaseUrl
5395
+ });
5396
+ recordTelemetryBusinessInfo({ task_status: result.task.status });
5397
+ logFeatureStep("task", "task.submission_retried", {
5398
+ local_task_id: result.task.id,
5399
+ backend_task_id: result.task.backendTaskId,
5400
+ status: result.task.status,
5401
+ reused: result.submission.reused
5402
+ });
5403
+ if (jsonMode) printJson(successJson("task_retry", { task: taskForOutput(result.task), reused: result.submission.reused }));
5404
+ else printTask(result.task);
5405
+ } catch (error2) {
5406
+ handleTaskError(error2, jsonMode, "task_retry");
5407
+ }
5408
+ });
4647
5409
  task.command("wait").description("Wait for a task to complete").argument("<task_id>", "Task ID").option("--timeout <seconds>", "Max wait time", "300").action(async (id, opts, cmd) => {
4648
5410
  const jsonMode = isJsonOutput(cmd);
4649
5411
  try {
@@ -4652,8 +5414,19 @@ function createTaskCommand(deps = {}) {
4652
5414
  const deadline = Date.now() + timeoutSeconds * 1e3;
4653
5415
  let taskItem = store.getByAnyId(id);
4654
5416
  if (!taskItem) throw new CliError(ErrorCode.TASK_NOT_FOUND, `Task not found: ${id}`);
5417
+ if (!taskItem.backendTaskId && taskItem.status === "submitting" && taskItem.pendingSubmission && taskItem.error) {
5418
+ throw taskRetryRequiredError(taskItem);
5419
+ }
4655
5420
  while (!isTerminalTask(taskItem) && Date.now() < deadline) {
4656
- taskItem = await refreshTask(taskItem, cmd, { ...deps, taskStore: store });
5421
+ const requestTimeoutMs = Math.max(1, Math.min(3e4, deadline - Date.now()));
5422
+ try {
5423
+ taskItem = await refreshTask(taskItem, cmd, { ...deps, taskStore: store }, requestTimeoutMs);
5424
+ } catch (error2) {
5425
+ if (Date.now() >= deadline && error2 instanceof CliError && error2.code === ErrorCode.GENERATION_TIMEOUT) {
5426
+ throw new CliError(ErrorCode.TASK_TIMEOUT, `Timed out waiting for task ${id} after ${timeoutSeconds}s.`);
5427
+ }
5428
+ throw error2;
5429
+ }
4657
5430
  if (!isTerminalTask(taskItem)) {
4658
5431
  const remainingMs = Math.max(0, deadline - Date.now());
4659
5432
  await new Promise((resolve5) => setTimeout(resolve5, Math.min(deps.pollIntervalMs ?? 2e3, remainingMs)));
@@ -4662,9 +5435,21 @@ function createTaskCommand(deps = {}) {
4662
5435
  if (!isTerminalTask(taskItem)) {
4663
5436
  throw new CliError(ErrorCode.TASK_TIMEOUT, `Timed out waiting for task ${id} after ${timeoutSeconds}s.`);
4664
5437
  }
5438
+ if (taskItem.status === "failed" || taskItem.status === "cancelled") {
5439
+ throw new CliError(
5440
+ ErrorCode.GENERATION_FAILED,
5441
+ taskItem.error?.message ?? `Generation task ${taskItem.backendTaskId ?? taskItem.id} ${taskItem.status}.`,
5442
+ {
5443
+ task_id: taskItem.backendTaskId ?? taskItem.id,
5444
+ local_task_id: taskItem.id,
5445
+ backend_status: taskItem.status,
5446
+ ...taskItem.error ? { backend_error: taskItem.error } : {}
5447
+ }
5448
+ );
5449
+ }
4665
5450
  recordTelemetryBusinessInfo({ task_status: taskItem.status, timeout_seconds: timeoutSeconds });
4666
5451
  logFeatureStep("task", "task.wait_completed", { task_id: id, status: taskItem.status });
4667
- if (jsonMode) printJson(successJson("task_wait", { task: taskItem }));
5452
+ if (jsonMode) printJson(successJson("task_wait", { task: taskForOutput(taskItem) }));
4668
5453
  else printTask(taskItem);
4669
5454
  } catch (error2) {
4670
5455
  handleTaskError(error2, jsonMode, "task_wait");
@@ -4674,7 +5459,7 @@ function createTaskCommand(deps = {}) {
4674
5459
  }
4675
5460
 
4676
5461
  // src/commands/record.ts
4677
- import { Command as Command6 } from "commander";
5462
+ import { Command as Command7 } from "commander";
4678
5463
  import { spawn } from "child_process";
4679
5464
 
4680
5465
  // src/clients/hogi_genai_record_client.ts
@@ -4683,11 +5468,13 @@ var HogiGenAIRecordClient = class {
4683
5468
  apiKey;
4684
5469
  _fetch;
4685
5470
  logger;
5471
+ timeoutMs;
4686
5472
  constructor(options) {
4687
5473
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
4688
5474
  this.apiKey = options.apiKey;
4689
5475
  this._fetch = options.fetch ?? globalThis.fetch;
4690
5476
  this.logger = options.logger;
5477
+ this.timeoutMs = options.timeoutMs;
4691
5478
  }
4692
5479
  /**
4693
5480
  * Fetch a generation record by hogi:// URI.
@@ -4713,190 +5500,57 @@ var HogiGenAIRecordClient = class {
4713
5500
  }, this.logger);
4714
5501
  let response;
4715
5502
  try {
4716
- response = await this._fetch(url, {
4717
- method: "GET",
4718
- headers: {
4719
- "x-hogi-api-key": this.apiKey
4720
- }
4721
- });
4722
- } catch (err) {
4723
- const msg = err instanceof Error ? err.message : String(err);
4724
- const error2 = new CliError(ErrorCode.NETWORK_ERROR, `Network error: ${msg}`);
4725
- apiLog.failure(error2);
4726
- throw error2;
4727
- }
4728
- if (response.status === 400) {
4729
- const error2 = new CliError(ErrorCode.INVALID_ARGUMENT, "URI parameter is required");
4730
- apiLog.failure(error2, response.status);
4731
- throw error2;
4732
- }
4733
- if (response.status === 401) {
4734
- const error2 = new CliError(ErrorCode.API_KEY_INVALID, "Invalid API key", { status: 401 });
4735
- apiLog.failure(error2, response.status);
4736
- throw error2;
4737
- }
4738
- if (response.status === 404) {
4739
- const error2 = new CliError(
4740
- ErrorCode.RECORD_NOT_FOUND,
4741
- `No generation record found for URI: ${uri}`
4742
- );
4743
- apiLog.failure(error2, response.status);
4744
- throw error2;
4745
- }
4746
- if (!response.ok) {
4747
- const text = await response.text().catch(() => "");
4748
- const error2 = new CliError(
4749
- ErrorCode.SERVER_ERROR,
4750
- `HTTP ${response.status}: ${text || response.statusText}`,
4751
- { status: response.status }
4752
- );
4753
- apiLog.failure(error2, response.status);
4754
- throw error2;
4755
- }
4756
- let body;
4757
- try {
4758
- body = await response.json();
4759
- } catch {
4760
- const error2 = new CliError(ErrorCode.SERVER_ERROR, "Invalid JSON response from server");
4761
- apiLog.failure(error2, response.status);
4762
- throw error2;
4763
- }
4764
- if (!body.success || !body.data) {
4765
- const error2 = new CliError(
4766
- ErrorCode.SERVER_ERROR,
4767
- body.error ?? "Unexpected response structure from record API"
4768
- );
4769
- apiLog.failure(error2, response.status);
4770
- throw error2;
4771
- }
4772
- apiLog.success(response.status, { record_type: body.data.type, success: body.data.success });
4773
- return body.data;
4774
- }
4775
- };
4776
-
4777
- // src/clients/hogi_res_client.ts
4778
- import { mkdir, writeFile } from "fs/promises";
4779
- import { existsSync as existsSync5, statSync as statSync3 } from "fs";
4780
- import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "path";
4781
- function assertHogiUri(uri) {
4782
- if (!uri || !uri.startsWith("hogi://")) {
4783
- throw new CliError(
4784
- ErrorCode.INVALID_ARGUMENT,
4785
- `Invalid hogi URI: "${uri}". Expected format: hogi://image/<id> or hogi://video/<id>`
4786
- );
4787
- }
4788
- }
4789
- function filenameFromUri(uri) {
4790
- try {
4791
- const parsed = new URL(uri);
4792
- const name = basename(parsed.pathname);
4793
- if (name && name !== "." && name !== "/") return name;
4794
- } catch {
4795
- }
4796
- return "resource.bin";
4797
- }
4798
- function resolveOutputPath(uri, output) {
4799
- const fallbackName = filenameFromUri(uri);
4800
- if (!output) return resolve2(process.cwd(), fallbackName);
4801
- const resolved = resolve2(output);
4802
- if (existsSync5(resolved) && statSync3(resolved).isDirectory()) {
4803
- return join6(resolved, fallbackName);
4804
- }
4805
- return resolved;
4806
- }
4807
- function looksLikeJsonError(contentType, buffer) {
4808
- if (contentType?.toLowerCase().includes("application/json")) return true;
4809
- const prefix = buffer.subarray(0, 128).toString("utf8").trimStart();
4810
- return prefix.startsWith("{") && (prefix.includes('"error"') || prefix.includes('"code"'));
4811
- }
4812
- var HogiResClient = class {
4813
- baseUrl;
4814
- apiKey;
4815
- _fetch;
4816
- logger;
4817
- constructor(options) {
4818
- this.baseUrl = options.baseUrl.replace(/\/+$/, "");
4819
- this.apiKey = options.apiKey;
4820
- this._fetch = options.fetch ?? globalThis.fetch;
4821
- this.logger = options.logger;
4822
- }
4823
- getReadUrl(uri, options = {}) {
4824
- assertHogiUri(uri);
4825
- const params = new URLSearchParams();
4826
- params.set("uri", uri);
4827
- if (options.filename) params.set("filename", options.filename);
4828
- if (typeof options.watermark === "boolean") params.set("wm", options.watermark ? "1" : "0");
4829
- return `${this.baseUrl}/v1/res/read_file?${params.toString()}`;
4830
- }
4831
- async download(uri, options = {}) {
4832
- assertHogiUri(uri);
4833
- const url = this.getReadUrl(uri, options);
4834
- const outputPath = resolveOutputPath(uri, options.output);
4835
- const apiLog = startApiCall({
4836
- source: "hogi_res_client",
4837
- operation: "resource.read",
4838
- method: "GET",
4839
- path: "/v1/res/read_file"
4840
- }, this.logger);
4841
- let response;
4842
- try {
4843
- response = await this._fetch(url, {
4844
- method: "GET",
4845
- redirect: "follow",
4846
- headers: {
4847
- "x-hogi-api-key": this.apiKey,
4848
- "accept": "image/*,video/*,application/octet-stream,application/json;q=0.5,*/*;q=0.1"
5503
+ return await withRequestTimeout({
5504
+ operation: "Generation record request",
5505
+ timeoutMs: this.timeoutMs,
5506
+ run: async (signal) => {
5507
+ response = await this._fetch(url, {
5508
+ method: "GET",
5509
+ headers: withClientContextHeaders({ "x-hogi-api-key": this.apiKey }),
5510
+ signal
5511
+ });
5512
+ if (response.status === 400) throw new CliError(ErrorCode.INVALID_ARGUMENT, "URI parameter is required");
5513
+ if (response.status === 401) throw new CliError(ErrorCode.API_KEY_INVALID, "Invalid API key", { status: 401 });
5514
+ if (response.status === 404) {
5515
+ throw new CliError(ErrorCode.RECORD_NOT_FOUND, `No generation record found for URI: ${uri}`);
5516
+ }
5517
+ if (!response.ok) {
5518
+ const text = await response.text().catch(() => "");
5519
+ throw new CliError(ErrorCode.SERVER_ERROR, `HTTP ${response.status}: ${text || response.statusText}`, {
5520
+ status: response.status
5521
+ });
5522
+ }
5523
+ let body;
5524
+ try {
5525
+ body = await response.json();
5526
+ } catch {
5527
+ throw new CliError(ErrorCode.SERVER_ERROR, "Invalid JSON response from server");
5528
+ }
5529
+ if (!body.success || !body.data) {
5530
+ throw new CliError(ErrorCode.SERVER_ERROR, body.error ?? "Unexpected response structure from record API");
5531
+ }
5532
+ apiLog.success(response.status, { record_type: body.data.type, success: body.data.success });
5533
+ return body.data;
4849
5534
  }
4850
5535
  });
4851
5536
  } catch (err) {
5537
+ if (err instanceof HttpRequestTimeoutError) {
5538
+ const timeoutError = new CliError(ErrorCode.NETWORK_ERROR, err.message, {
5539
+ retryable: true,
5540
+ timeout_ms: err.timeoutMs
5541
+ });
5542
+ apiLog.failure(timeoutError, response?.status);
5543
+ throw timeoutError;
5544
+ }
5545
+ if (err instanceof CliError) {
5546
+ apiLog.failure(err, response?.status);
5547
+ throw err;
5548
+ }
4852
5549
  const msg = err instanceof Error ? err.message : String(err);
4853
5550
  const error2 = new CliError(ErrorCode.NETWORK_ERROR, `Network error: ${msg}`);
4854
5551
  apiLog.failure(error2);
4855
5552
  throw error2;
4856
5553
  }
4857
- if (response.status === 401) {
4858
- const error2 = new CliError(ErrorCode.API_KEY_INVALID, "Invalid API key", { status: 401 });
4859
- apiLog.failure(error2, response.status);
4860
- throw error2;
4861
- }
4862
- if (response.status === 404) {
4863
- const error2 = new CliError(ErrorCode.RECORD_NOT_FOUND, `Resource not found for URI: ${uri}`, { status: 404 });
4864
- apiLog.failure(error2, response.status);
4865
- throw error2;
4866
- }
4867
- if (!response.ok) {
4868
- const text = await response.text().catch(() => "");
4869
- const error2 = new CliError(
4870
- ErrorCode.SERVER_ERROR,
4871
- `HTTP ${response.status}: ${text || response.statusText}`,
4872
- { status: response.status }
4873
- );
4874
- apiLog.failure(error2, response.status);
4875
- throw error2;
4876
- }
4877
- const contentType = response.headers.get("content-type");
4878
- const buffer = Buffer.from(await response.arrayBuffer());
4879
- if (looksLikeJsonError(contentType, buffer)) {
4880
- let message = "Resource read failed";
4881
- try {
4882
- const body = JSON.parse(buffer.toString("utf8"));
4883
- message = body.message ?? body.error ?? body.code ?? message;
4884
- } catch {
4885
- }
4886
- const error2 = new CliError(ErrorCode.SERVER_ERROR, message);
4887
- apiLog.failure(error2, response.status);
4888
- throw error2;
4889
- }
4890
- apiLog.success(response.status, { bytes: buffer.byteLength, mime_type: contentType });
4891
- await mkdir(dirname3(outputPath), { recursive: true });
4892
- await writeFile(outputPath, buffer);
4893
- logFeatureStep("record", "resource.saved", { bytes: buffer.byteLength, mime_type: contentType });
4894
- return {
4895
- path: outputPath,
4896
- bytes: buffer.byteLength,
4897
- mimeType: contentType,
4898
- uri
4899
- };
4900
5554
  }
4901
5555
  };
4902
5556
 
@@ -4919,7 +5573,7 @@ function openFile(path) {
4919
5573
  }
4920
5574
  }
4921
5575
  function createRecordCommand() {
4922
- const record = new Command6("record").description("GenAI record queries");
5576
+ const record = new Command7("record").description("GenAI record queries");
4923
5577
  record.command("get").description("Get generation record by hogi:// URI").argument("<uri>", "hogi:// URI (e.g. hogi://image/abc123)").action(async (uri, _opts, cmd) => {
4924
5578
  const jsonMode = isJsonMode3(cmd);
4925
5579
  try {
@@ -5037,7 +5691,7 @@ function createRecordCommand() {
5037
5691
  }
5038
5692
 
5039
5693
  // src/commands/inspect.ts
5040
- import { Command as Command7 } from "commander";
5694
+ import { Command as Command8 } from "commander";
5041
5695
  var MODEL_SELECTION_CONTRACT = {
5042
5696
  preferred: "model_id",
5043
5697
  generation_flag: "--model-id",
@@ -5138,7 +5792,7 @@ function isJsonMode4(cmd) {
5138
5792
  return root.opts().json === true;
5139
5793
  }
5140
5794
  function createInspectCommand(deps = {}) {
5141
- const inspect = new Command7("inspect").description("Inspect backend capabilities");
5795
+ const inspect = new Command8("inspect").description("Inspect backend capabilities");
5142
5796
  inspect.command("capabilities").description("List available GenAI capabilities").action((_opts, cmd) => {
5143
5797
  const jsonMode = isJsonMode4(cmd);
5144
5798
  const caps = getCapabilities();
@@ -5215,7 +5869,7 @@ function createInspectCommand(deps = {}) {
5215
5869
  }
5216
5870
 
5217
5871
  // src/commands/preview.ts
5218
- import { Command as Command8 } from "commander";
5872
+ import { Command as Command9 } from "commander";
5219
5873
  function createBundleFromTerminalTask(store, task) {
5220
5874
  if (task.status !== "succeeded" && task.status !== "failed") return void 0;
5221
5875
  const mediaType = task.mediaType ?? (task.toolName.includes("video") ? "video" : task.toolName.includes("music") || task.toolName.includes("audio") ? "audio" : "image");
@@ -5331,7 +5985,7 @@ function printPreviewHuman(data) {
5331
5985
  }
5332
5986
  }
5333
5987
  function createPreviewCommand(deps = {}) {
5334
- const preview = new Command8("preview").description("Preview bundles for generation results");
5988
+ const preview = new Command9("preview").description("Preview bundles for generation results");
5335
5989
  preview.command("latest").description("Show the most recent preview bundle").action((_opts, cmd) => {
5336
5990
  const jsonMode = isJsonMode5(cmd);
5337
5991
  const result = executePreviewLatest(deps);
@@ -5400,7 +6054,7 @@ function createPreviewCommand(deps = {}) {
5400
6054
  }
5401
6055
 
5402
6056
  // src/commands/upload.ts
5403
- import { Command as Command9 } from "commander";
6057
+ import { Command as Command10 } from "commander";
5404
6058
  import { basename as basename2, resolve as resolve3 } from "path";
5405
6059
  import { readFile, stat } from "fs/promises";
5406
6060
 
@@ -5514,11 +6168,13 @@ var HogiResUploadClient = class {
5514
6168
  apiKey;
5515
6169
  _fetch;
5516
6170
  logger;
6171
+ timeoutMs;
5517
6172
  constructor(options) {
5518
6173
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
5519
6174
  this.apiKey = options.apiKey;
5520
6175
  this._fetch = options.fetch ?? globalThis.fetch;
5521
6176
  this.logger = options.logger;
6177
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TRANSFER_TIMEOUT_MS;
5522
6178
  }
5523
6179
  async uploadFile(request) {
5524
6180
  const fileType = normalizeFileType(request.fileType);
@@ -5537,33 +6193,47 @@ var HogiResUploadClient = class {
5537
6193
  }, this.logger);
5538
6194
  let response;
5539
6195
  try {
5540
- response = await this._fetch(`${this.baseUrl}/v1/workspace/uploadAsset?${params.toString()}`, {
5541
- method: "POST",
5542
- headers: { "x-hogi-api-key": this.apiKey, "content-type": "application/octet-stream", "accept": "application/json" },
5543
- body: request.buffer
6196
+ return await withRequestTimeout({
6197
+ operation: "Workspace file upload request",
6198
+ timeoutMs: this.timeoutMs,
6199
+ run: async (signal) => {
6200
+ response = await this._fetch(`${this.baseUrl}/v1/workspace/uploadAsset?${params.toString()}`, {
6201
+ method: "POST",
6202
+ headers: withClientContextHeaders({ "x-hogi-api-key": this.apiKey, "content-type": "application/octet-stream", "accept": "application/json" }),
6203
+ body: request.buffer,
6204
+ signal
6205
+ });
6206
+ const envelope = await parseJsonResponse(response);
6207
+ if (!response.ok) throw mapUploadError(response.status, envelope);
6208
+ const result = parseUploadResult(envelope);
6209
+ if (result.workspaceId !== request.workspaceId) throw new CliError(ErrorCode.SERVER_ERROR, "Upload API returned unexpected workspace_id");
6210
+ if (result.type !== type) throw new CliError(ErrorCode.SERVER_ERROR, "Upload API returned unexpected asset type");
6211
+ apiLog.success(response.status, { asset_id: result.assetId, type: result.type });
6212
+ return result;
6213
+ }
5544
6214
  });
5545
6215
  } catch (err) {
6216
+ if (err instanceof HttpRequestTimeoutError) {
6217
+ const timeoutError = new CliError(ErrorCode.NETWORK_ERROR, err.message, {
6218
+ retryable: true,
6219
+ timeout_ms: err.timeoutMs
6220
+ });
6221
+ apiLog.failure(timeoutError, response?.status);
6222
+ throw timeoutError;
6223
+ }
6224
+ if (err instanceof CliError) {
6225
+ apiLog.failure(err, response?.status);
6226
+ throw err;
6227
+ }
5546
6228
  const error2 = new CliError(ErrorCode.NETWORK_ERROR, `Network error: ${err instanceof Error ? err.message : String(err)}`);
5547
6229
  apiLog.failure(error2);
5548
6230
  throw error2;
5549
6231
  }
5550
- try {
5551
- const envelope = await parseJsonResponse(response);
5552
- if (!response.ok) throw mapUploadError(response.status, envelope);
5553
- const result = parseUploadResult(envelope);
5554
- if (result.workspaceId !== request.workspaceId) throw new CliError(ErrorCode.SERVER_ERROR, "Upload API returned unexpected workspace_id");
5555
- if (result.type !== type) throw new CliError(ErrorCode.SERVER_ERROR, "Upload API returned unexpected asset type");
5556
- apiLog.success(response.status, { asset_id: result.assetId, type: result.type });
5557
- return result;
5558
- } catch (error2) {
5559
- apiLog.failure(error2, response.status);
5560
- throw error2;
5561
- }
5562
6232
  }
5563
6233
  };
5564
6234
 
5565
6235
  // src/clients/hogi_vod_upload_client.ts
5566
- import { createHash } from "crypto";
6236
+ import { createHash as createHash2 } from "crypto";
5567
6237
  import { createReadStream } from "fs";
5568
6238
 
5569
6239
  // src/core/vod_upload_contract.ts
@@ -5921,7 +6591,7 @@ async function readJson(response, operation) {
5921
6591
  return envelope;
5922
6592
  }
5923
6593
  async function md5File(filePath) {
5924
- const hash = createHash("md5");
6594
+ const hash = createHash2("md5");
5925
6595
  for await (const chunk of createReadStream(filePath)) hash.update(chunk);
5926
6596
  return hash.digest("hex");
5927
6597
  }
@@ -5941,6 +6611,7 @@ var HogiVodUploadClient = class {
5941
6611
  directUpload;
5942
6612
  pollIntervalMs;
5943
6613
  timeoutMs;
6614
+ requestTimeoutMs;
5944
6615
  constructor(options) {
5945
6616
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
5946
6617
  this.apiKey = options.apiKey;
@@ -5949,14 +6620,31 @@ var HogiVodUploadClient = class {
5949
6620
  this.directUpload = options.directUpload ?? uploadFileDirectlyToVod;
5950
6621
  this.pollIntervalMs = options.pollIntervalMs ?? 3e3;
5951
6622
  this.timeoutMs = options.timeoutMs ?? 10 * 60 * 1e3;
6623
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_HTTP_REQUEST_TIMEOUT_MS;
5952
6624
  }
5953
6625
  headers(json = false) {
5954
- return { "x-hogi-api-key": this.apiKey, accept: "application/json", ...json ? { "content-type": "application/json" } : {} };
6626
+ return withClientContextHeaders({
6627
+ "x-hogi-api-key": this.apiKey,
6628
+ accept: "application/json",
6629
+ ...json ? { "content-type": "application/json" } : {}
6630
+ });
5955
6631
  }
5956
- async request(url, init, operation) {
6632
+ async requestJson(url, init, operation, timeoutMs = this.requestTimeoutMs) {
5957
6633
  try {
5958
- return await this.fetch(url, init);
6634
+ return await withRequestTimeout({
6635
+ operation,
6636
+ timeoutMs,
6637
+ run: async (signal) => readJson(await this.fetch(url, { ...init, signal }), operation)
6638
+ });
5959
6639
  } catch (error2) {
6640
+ if (error2 instanceof HttpRequestTimeoutError) {
6641
+ throw new CliError(ErrorCode.NETWORK_ERROR, error2.message, {
6642
+ retryable: true,
6643
+ request_timeout: true,
6644
+ timeout_ms: error2.timeoutMs
6645
+ });
6646
+ }
6647
+ if (error2 instanceof CliError) throw error2;
5960
6648
  throw new CliError(ErrorCode.NETWORK_ERROR, `${operation} network error: ${error2 instanceof Error ? error2.message : String(error2)}`);
5961
6649
  }
5962
6650
  }
@@ -5982,12 +6670,11 @@ var HogiVodUploadClient = class {
5982
6670
  }, this.logger);
5983
6671
  try {
5984
6672
  const query = new URLSearchParams({ workspaceId: request.workspaceId, fileSize: String(request.fileSize) });
5985
- const tokenResponse = await this.request(
6673
+ const tokenEnvelope = await this.requestJson(
5986
6674
  `${this.baseUrl}/media/vod/upload_token?${query}`,
5987
6675
  { method: "GET", headers: this.headers() },
5988
6676
  "VOD upload_token"
5989
6677
  );
5990
- const tokenEnvelope = await readJson(tokenResponse, "VOD upload_token");
5991
6678
  const token = tokenEnvelope.data;
5992
6679
  if (!isVodUploadToken(token)) {
5993
6680
  throw new CliError(ErrorCode.SERVER_ERROR, "Invalid VOD upload_token response");
@@ -6004,7 +6691,7 @@ var HogiVodUploadClient = class {
6004
6691
  }),
6005
6692
  md5File(request.filePath)
6006
6693
  ]);
6007
- const commitResponse = await this.request(`${this.baseUrl}/media/vod/commit_upload`, {
6694
+ const commitEnvelope = await this.requestJson(`${this.baseUrl}/media/vod/commit_upload`, {
6008
6695
  method: "POST",
6009
6696
  headers: this.headers(true),
6010
6697
  body: JSON.stringify({
@@ -6015,7 +6702,6 @@ var HogiVodUploadClient = class {
6015
6702
  contentMd5
6016
6703
  })
6017
6704
  }, "VOD commit_upload");
6018
- const commitEnvelope = await readJson(commitResponse, "VOD commit_upload");
6019
6705
  const commit = asObject4(commitEnvelope.data);
6020
6706
  if (typeof commit?.assetId !== "string" || typeof commit.taskId !== "string") {
6021
6707
  throw new CliError(ErrorCode.SERVER_ERROR, "Invalid VOD commit_upload response");
@@ -6038,19 +6724,30 @@ var HogiVodUploadClient = class {
6038
6724
  const deadline = Date.now() + this.timeoutMs;
6039
6725
  while (Date.now() < deadline) {
6040
6726
  const query = new URLSearchParams({ taskId, workspaceId });
6041
- const response = await this.request(
6042
- `${this.baseUrl}/media/vod/transcode_status?${query}`,
6043
- { method: "GET", headers: this.headers() },
6044
- "VOD transcode_status"
6045
- );
6046
- const envelope = await readJson(response, "VOD transcode_status");
6727
+ const remainingMs = Math.max(1, deadline - Date.now());
6728
+ let envelope;
6729
+ try {
6730
+ envelope = await this.requestJson(
6731
+ `${this.baseUrl}/media/vod/transcode_status?${query}`,
6732
+ { method: "GET", headers: this.headers() },
6733
+ "VOD transcode_status",
6734
+ Math.min(this.requestTimeoutMs, remainingMs)
6735
+ );
6736
+ } catch (error2) {
6737
+ const details = error2 instanceof CliError && error2.details && typeof error2.details === "object" ? error2.details : void 0;
6738
+ if (details?.request_timeout === true) {
6739
+ if (Date.now() >= deadline) break;
6740
+ continue;
6741
+ }
6742
+ throw error2;
6743
+ }
6047
6744
  const data = asObject4(envelope.data);
6048
6745
  if (!data || typeof data.status !== "string") throw new CliError(ErrorCode.SERVER_ERROR, "Invalid VOD transcode_status response");
6049
6746
  if ((data.status === "completed" || data.status === "finished") && typeof data.outputUri === "string" && data.outputUri.startsWith("hogi://video/")) return data;
6050
6747
  if (data.status === "failed" || data.status === "cancelled") {
6051
6748
  throw new CliError(ErrorCode.SERVER_ERROR, data.errorMessage || data.errorCode || `VOD transcode ${data.status}`);
6052
6749
  }
6053
- await sleep(this.pollIntervalMs);
6750
+ await sleep(Math.min(this.pollIntervalMs, Math.max(0, deadline - Date.now())));
6054
6751
  }
6055
6752
  throw new CliError(ErrorCode.TASK_TIMEOUT, `VOD transcode timed out after ${this.timeoutMs}ms`, { task_id: taskId });
6056
6753
  }
@@ -6167,7 +6864,7 @@ async function executeUploadFile(params, rootOpts = {}, deps = {}) {
6167
6864
  };
6168
6865
  }
6169
6866
  function createUploadCommand(deps = {}) {
6170
- const upload = new Command9("upload").description("Upload local files to Hogi resources");
6867
+ const upload = new Command10("upload").description("Upload local files to Hogi resources");
6171
6868
  upload.command("file").description("Upload an image, video, or audio file").argument("<path>", "Local file path").option("--type <ext>", "File extension override, e.g. png/mp4/mp3").option("--name <name>", "Original filename stored for display").option("--after-asset-id <asset_id>", "Insert after an existing workspace asset").option("--vod", "Force the VOD direct-upload flow for a video").option("--attach", "Deprecated compatibility flag; uploads are always attached").action(async (path, opts, cmd) => {
6172
6869
  const jsonMode = isJsonMode6(cmd);
6173
6870
  const rootOpts = getRootOpts3(cmd);
@@ -6219,7 +6916,7 @@ function createUploadCommand(deps = {}) {
6219
6916
  }
6220
6917
 
6221
6918
  // src/commands/logs.ts
6222
- import { Command as Command10 } from "commander";
6919
+ import { Command as Command11 } from "commander";
6223
6920
  function rootJsonMode(cmd) {
6224
6921
  let root = cmd;
6225
6922
  while (root.parent) root = root.parent;
@@ -6258,14 +6955,14 @@ function parseFilter(opts) {
6258
6955
  function addFilterOptions(command) {
6259
6956
  return command.option("--level <level>", "Filter by debug/info/warn/error").option("--source <source>", "Filter by exact source name").option("--category <category>", "Filter by application/api").option("--trace <trace_id>", "Filter by feature trace ID").option("--feature <name>", "Filter by feature name, e.g. image.text").option("--since <timestamp>", "Filter entries at or after an ISO timestamp").option("--limit <count>", "Maximum number of newest entries");
6260
6957
  }
6261
- function handleError(error2, jsonMode, type) {
6958
+ function handleError2(error2, jsonMode, type) {
6262
6959
  const cliError = error2 instanceof CliError ? error2 : new CliError(ErrorCode.CONFIG_ERROR, error2 instanceof Error ? error2.message : String(error2));
6263
6960
  if (jsonMode) printJson(errorJson(type, cliError.code, cliError.message, cliError.details));
6264
6961
  else error(cliError.message);
6265
6962
  process.exitCode = 1;
6266
6963
  }
6267
6964
  function createLogsCommand(deps = {}) {
6268
- const logs = new Command10("logs").description("Local structured log management");
6965
+ const logs = new Command11("logs").description("Local structured log management");
6269
6966
  addFilterOptions(logs.command("list").description("List locally stored log entries")).action((opts, cmd) => {
6270
6967
  const jsonMode = rootJsonMode(cmd);
6271
6968
  try {
@@ -6289,7 +6986,7 @@ function createLogsCommand(deps = {}) {
6289
6986
  }
6290
6987
  }
6291
6988
  } catch (error2) {
6292
- handleError(error2, jsonMode, "logs_list");
6989
+ handleError2(error2, jsonMode, "logs_list");
6293
6990
  }
6294
6991
  });
6295
6992
  addFilterOptions(
@@ -6315,7 +7012,7 @@ function createLogsCommand(deps = {}) {
6315
7012
  if (jsonMode) printJson(successJson("logs_export", result));
6316
7013
  else success(`Exported ${result.count} log entries to ${result.path}`);
6317
7014
  } catch (error2) {
6318
- handleError(error2, jsonMode, "logs_export");
7015
+ handleError2(error2, jsonMode, "logs_export");
6319
7016
  }
6320
7017
  });
6321
7018
  logs.command("path").description("Show the local log file path").action((_opts, cmd) => {
@@ -6329,7 +7026,7 @@ function createLogsCommand(deps = {}) {
6329
7026
  }
6330
7027
 
6331
7028
  // src/commands/points.ts
6332
- import { Command as Command11 } from "commander";
7029
+ import { Command as Command12 } from "commander";
6333
7030
  function context(rootOpts, deps) {
6334
7031
  const configStore = deps.configStore ?? new FileConfigStore();
6335
7032
  const auth = requireApiKeyAuth(rootOpts, configStore);
@@ -6384,14 +7081,14 @@ function numberOption(value, name, allowZero = false) {
6384
7081
  }
6385
7082
  return parsed;
6386
7083
  }
6387
- function handleError2(error2, jsonMode, type) {
7084
+ function handleError3(error2, jsonMode, type) {
6388
7085
  const cliError = error2 instanceof CliError ? error2 : new CliError(ErrorCode.SERVER_ERROR, error2 instanceof Error ? error2.message : String(error2));
6389
7086
  if (jsonMode) printJson(errorJson(type, cliError.code, cliError.message, cliError.details));
6390
7087
  else error(cliError.message);
6391
7088
  process.exitCode = 1;
6392
7089
  }
6393
7090
  function createPointsCommand(deps = {}) {
6394
- const points = new Command11("points").description("Inspect points balance, estimates, and actual generation charges");
7091
+ const points = new Command12("points").description("Inspect points balance, estimates, and actual generation charges");
6395
7092
  points.command("balance").description("Show current points balance and membership tier").action(async (_opts, cmd) => {
6396
7093
  const jsonMode = isJsonOutput(cmd);
6397
7094
  try {
@@ -6403,7 +7100,7 @@ function createPointsCommand(deps = {}) {
6403
7100
  info(` Tier: ${result.tier}`);
6404
7101
  }
6405
7102
  } catch (error2) {
6406
- handleError2(error2, jsonMode, "points_balance");
7103
+ handleError3(error2, jsonMode, "points_balance");
6407
7104
  }
6408
7105
  });
6409
7106
  points.command("cost").description("Query actual charged points for generated resource URIs").argument("<uris...>", "1-20 hogi:// generation URIs").action(async (uris, _opts, cmd) => {
@@ -6422,7 +7119,7 @@ function createPointsCommand(deps = {}) {
6422
7119
  if (entry.pricingExplanation) info(` ${entry.pricingExplanation}`);
6423
7120
  }
6424
7121
  } catch (error2) {
6425
- handleError2(error2, jsonMode, "points_cost");
7122
+ handleError3(error2, jsonMode, "points_cost");
6426
7123
  }
6427
7124
  });
6428
7125
  points.command("quote").description("Estimate generation points from the current backend model catalog").option("--model-id <id>", "Exact model ID from inspect models").option("--model <model>", "Model key from inspect models").option("--variant <variant>", "Exact model variant from inspect models; requires --model").option("--duration <seconds>", "Requested generation duration").option("--resolution <resolution>", "Requested output resolution").option("--quality <quality>", "Requested quality tier").option("--reference-video-duration <seconds>", "Referenced video duration", "0").action(async (opts, cmd) => {
@@ -6454,14 +7151,14 @@ function createPointsCommand(deps = {}) {
6454
7151
  warn(result.caveat);
6455
7152
  }
6456
7153
  } catch (error2) {
6457
- handleError2(error2, jsonMode, "points_quote");
7154
+ handleError3(error2, jsonMode, "points_quote");
6458
7155
  }
6459
7156
  });
6460
7157
  return points;
6461
7158
  }
6462
7159
 
6463
7160
  // src/commands/auth.ts
6464
- import { Command as Command12 } from "commander";
7161
+ import { Command as Command13 } from "commander";
6465
7162
  function validBaseUrl(value) {
6466
7163
  try {
6467
7164
  const url = new URL(value);
@@ -6471,8 +7168,8 @@ function validBaseUrl(value) {
6471
7168
  }
6472
7169
  }
6473
7170
  function createAuthCommand(deps = {}) {
6474
- const auth = new Command12("auth").description("Authentication configuration");
6475
- auth.command("login").description("Persist an API key in the local CLI configuration").option("--api-key <key>", "Oiioii API key; falls back to OIIOII_API_KEY").option("--base-url <url>", "Persist the backend base URL with the API key").action((opts, cmd) => {
7171
+ const auth = new Command13("auth").description("Authentication configuration");
7172
+ auth.command("login").description("Persist an API key in the local CLI configuration").option("--api-key <key>", "Oiioii API key; falls back to OIIOII_API_KEY").option("--base-url <url>", "Persist the backend base URL with the API key").option("--reset-base-url", "Remove the persisted backend override and use this build's default").action((opts, cmd) => {
6476
7173
  const jsonMode = isJsonOutput(cmd);
6477
7174
  const apiKey = opts.apiKey?.trim() || process.env["OIIOII_API_KEY"]?.trim();
6478
7175
  if (!apiKey) {
@@ -6484,6 +7181,13 @@ function createAuthCommand(deps = {}) {
6484
7181
  }
6485
7182
  const rootBaseUrl = getRootOptions(cmd).baseUrl;
6486
7183
  const baseUrl = (opts.baseUrl ?? (typeof rootBaseUrl === "string" ? rootBaseUrl : void 0))?.trim();
7184
+ if (baseUrl && opts.resetBaseUrl) {
7185
+ const message = "--base-url and --reset-base-url cannot be used together.";
7186
+ if (jsonMode) printJson(errorJson("auth_login", ErrorCode.INVALID_ARGUMENT, message));
7187
+ else error(message);
7188
+ process.exitCode = 1;
7189
+ return;
7190
+ }
6487
7191
  if (baseUrl && !validBaseUrl(baseUrl)) {
6488
7192
  const message = "--base-url must be an HTTP(S) origin without a path, query, or fragment.";
6489
7193
  if (jsonMode) printJson(errorJson("auth_login", ErrorCode.INVALID_ARGUMENT, message));
@@ -6492,7 +7196,10 @@ function createAuthCommand(deps = {}) {
6492
7196
  return;
6493
7197
  }
6494
7198
  const configStore = deps.configStore ?? new FileConfigStore();
6495
- configStore.update({ apiKey, ...baseUrl ? { baseUrl: baseUrl.replace(/\/$/, "") } : {} });
7199
+ configStore.update(
7200
+ { apiKey, ...baseUrl ? { baseUrl: baseUrl.replace(/\/$/, "") } : {} },
7201
+ { resetBaseUrl: opts.resetBaseUrl === true }
7202
+ );
6496
7203
  const persisted = configStore.load();
6497
7204
  const configPath = configStore.getConfigPath();
6498
7205
  if (jsonMode) {
@@ -6510,7 +7217,7 @@ function createAuthCommand(deps = {}) {
6510
7217
  }
6511
7218
 
6512
7219
  // src/commands/cli_version.ts
6513
- import { Command as Command13 } from "commander";
7220
+ import { Command as Command14 } from "commander";
6514
7221
 
6515
7222
  // src/core/npm_cli_manager.ts
6516
7223
  import crossSpawn from "cross-spawn";
@@ -6818,17 +7525,17 @@ function printOperation(type, result, command) {
6818
7525
  else success(`oiioii ${result.version} is already installed.`);
6819
7526
  }
6820
7527
  function createUpdateCommand() {
6821
- return new Command13("update").description("Update oiioii from the npm channel for this build environment").option("--check", "Check for an update without installing it").option("--force", "Reinstall the channel version even when already current").action(async (options, command) => {
7528
+ return new Command14("update").description("Update oiioii from the npm channel for this build environment").option("--check", "Check for an update without installing it").option("--force", "Reinstall the channel version even when already current").action(async (options, command) => {
6822
7529
  printOperation("cli_update", await updateCli(options), command);
6823
7530
  });
6824
7531
  }
6825
7532
  function createInstallCommand() {
6826
- return new Command13("install").description("Install one exact oiioii version from npm").argument("<version>", "Exact stable or prerelease version allowed by this build environment").action(async (version, _options, command) => {
7533
+ return new Command14("install").description("Install one exact oiioii version from npm").argument("<version>", "Exact stable or prerelease version allowed by this build environment").action(async (version, _options, command) => {
6827
7534
  printOperation("cli_install", await installCliVersion(version), command);
6828
7535
  });
6829
7536
  }
6830
7537
  function createVersionsCommand() {
6831
- return new Command13("versions").description("List npm versions available to this build environment").action(async (_options, command) => {
7538
+ return new Command14("versions").description("List npm versions available to this build environment").action(async (_options, command) => {
6832
7539
  const result = await listCliVersions();
6833
7540
  if (isJsonOutput(command)) {
6834
7541
  printJson(successJson("cli_versions", result));
@@ -6845,7 +7552,7 @@ import { fileURLToPath } from "url";
6845
7552
  import { resolve as resolve4 } from "path";
6846
7553
  import { realpathSync } from "fs";
6847
7554
  function createProgram() {
6848
- const program = new Command14();
7555
+ const program = new Command15();
6849
7556
  program.name("oiioii").description("Agent-native CLI for Oiioii / Hogi GenAI capabilities").version(CLI_VERSION, "-V, --version", "Output the current version").option("--json", "Output machine-readable JSON").option("--base-url <url>", "Hogi backend base URL").option("--workspace <id>", "Workspace ID override").option("--verbose", "Enable verbose/debug output").option("--no-color", "Disable colored output");
6850
7557
  program.addCommand(createAuthCommand());
6851
7558
  program.addCommand(createWorkspaceCommand());