@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.
- package/README.md +107 -28
- package/dist/cli.js +1155 -448
- package/package.json +17 -10
- 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
|
|
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:
|
|
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() ?? "
|
|
36
|
-
return isBuildEnvironment(value) ? value : "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
727
|
-
|
|
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" : "
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1342
|
-
|
|
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
|
|
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
|
|
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
|
|
2212
|
+
if (!text) throw responseProtocolError2(`${fallback}: empty response`);
|
|
1629
2213
|
try {
|
|
1630
2214
|
return JSON.parse(text);
|
|
1631
2215
|
} catch {
|
|
1632
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2286
|
+
throw responseProtocolError2("Invalid async generation query response");
|
|
1703
2287
|
}
|
|
1704
2288
|
if (!task.outputUris.every((uri) => typeof uri === "string")) {
|
|
1705
|
-
throw
|
|
2289
|
+
throw responseProtocolError2("Invalid async generation output URI list");
|
|
1706
2290
|
}
|
|
1707
2291
|
if (typeof task.progress !== "number" || !Number.isFinite(task.progress)) {
|
|
1708
|
-
throw
|
|
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
|
|
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
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
|
2147
|
-
import { join as
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
2276
|
-
if (!
|
|
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
|
|
2285
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
2936
|
+
const bundleDir = join6(this.previewsDir, bundleId);
|
|
2341
2937
|
mkdirSync4(bundleDir, { recursive: true, mode: 448 });
|
|
2342
|
-
mkdirSync4(
|
|
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(
|
|
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 =
|
|
2388
|
-
const summaryPath =
|
|
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 =
|
|
2406
|
-
const manifestPath =
|
|
2407
|
-
const summaryPath =
|
|
2408
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
2464
|
-
if (!
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 &&
|
|
4279
|
-
throw audioConstraint(`Suno only supports variants
|
|
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
|
-
//
|
|
4294
|
-
//
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
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 =
|
|
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 {
|
|
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
|
|
6632
|
+
async requestJson(url, init, operation, timeoutMs = this.requestTimeoutMs) {
|
|
5957
6633
|
try {
|
|
5958
|
-
return await
|
|
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
|
|
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
|
|
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
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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());
|