ima2-gen 3.13.1 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/CHANGELOG.md +14 -0
- package/README.md +15 -2
- package/bin/commands/capabilities.js +5 -3
- package/bin/commands/config.js +15 -4
- package/bin/commands/defaults.js +7 -5
- package/bin/commands/doctor.js +88 -39
- package/bin/commands/gen.js +21 -4
- package/bin/commands/models.js +2 -2
- package/bin/commands/ping.js +1 -1
- package/bin/commands/prompt.js +5 -3
- package/bin/commands/service.js +32 -4
- package/bin/commands/tools.js +19 -3
- package/bin/commands/upscale.js +18 -4
- package/bin/commands/video.js +37 -50
- package/bin/ima2.js +28 -9
- package/bin/lib/characterResolve.js +2 -1
- package/bin/lib/client.js +155 -20
- package/bin/lib/config-store.js +40 -7
- package/bin/lib/doctor-bundle.js +6 -2
- package/bin/lib/doctor-checks.js +13 -29
- package/bin/lib/doctor-media.js +3 -3
- package/bin/lib/doctor-providers.js +48 -35
- package/bin/lib/doctor-report.js +78 -0
- package/bin/lib/doctor-runtime.js +173 -0
- package/bin/lib/mcpJob.js +19 -7
- package/bin/lib/output.js +3 -0
- package/bin/lib/platform.js +12 -9
- package/bin/lib/recover-output.js +9 -2
- package/bin/lib/sse.js +9 -1
- package/bin/lib/videoMcp.js +16 -8
- package/config.js +41 -5
- package/docs/API.md +154 -16
- package/docs/CLI.md +65 -1
- package/docs/DOCKER.md +19 -4
- package/docs/DOCKER.zh-CN.md +11 -4
- package/docs/DOCKER.zh-TW.md +11 -4
- package/docs/FAQ.md +13 -0
- package/docs/NPX_QUICKSTART.md +6 -0
- package/docs/README.ja.md +15 -2
- package/docs/README.ko.md +15 -2
- package/docs/README.zh-CN.md +15 -2
- package/docs/README.zh-TW.md +15 -2
- package/docs/migration/runtime-test-inventory.md +79 -8
- package/lib/agyArtifact.js +82 -0
- package/lib/agyArtifactRead.js +249 -0
- package/lib/agyImageAdapter.js +2 -329
- package/lib/agyProcess.js +122 -0
- package/lib/apiRequestBudget.js +48 -0
- package/lib/assetLifecycle.js +42 -9
- package/lib/canvasVersionStore.js +46 -14
- package/lib/capabilities.js +7 -1
- package/lib/configKeys.js +2 -0
- package/lib/errors/providerMap.js +26 -0
- package/lib/eventBus.js +10 -6
- package/lib/eventsPolicy.js +9 -0
- package/lib/geminiApiImageAdapter.js +1 -214
- package/lib/generatePipeline.js +39 -165
- package/lib/generatedMediaAccess.js +74 -0
- package/lib/generationErrors.js +3 -22
- package/lib/grokImageAdapter.js +2 -353
- package/lib/grokImageCore.js +1 -57
- package/lib/grokImageDownload.js +159 -0
- package/lib/grokImageDownloadPolicy.js +124 -0
- package/lib/grokImagePlanner.js +301 -0
- package/lib/grokMultimodeAdapter.js +1 -75
- package/lib/grokVideoDownload.js +137 -34
- package/lib/inflight.js +112 -112
- package/lib/jobs/terminalStore.js +45 -0
- package/lib/lanSessionStore.js +131 -0
- package/lib/localAccessPolicy.js +180 -0
- package/lib/localLanAccess.js +281 -0
- package/lib/logger.js +22 -13
- package/lib/mcp/downloadMediaResult.js +72 -78
- package/lib/multimodePipeline.js +39 -126
- package/lib/nodeGeneration.js +44 -101
- package/lib/nodeStore.js +25 -11
- package/lib/nodeValidation.js +5 -1
- package/lib/pinnedHttpGet.js +174 -0
- package/lib/promptImport/githubFolder.js +32 -18
- package/lib/promptImport/githubSource.js +14 -10
- package/lib/promptImport/parsePromptCandidates.js +32 -2
- package/lib/providers/adapters/agyOperations.js +179 -0
- package/lib/providers/adapters/comfy.js +1 -0
- package/lib/providers/adapters/geminiOperations.js +214 -0
- package/lib/providers/adapters/googleExecution.js +97 -0
- package/lib/providers/adapters/grokExecution.js +137 -0
- package/lib/providers/adapters/grokMultimodeOperations.js +83 -0
- package/lib/providers/adapters/grokOperations.js +63 -0
- package/lib/providers/adapters/openaiExecution.js +118 -0
- package/lib/providers/adapters/openaiOperations.js +201 -0
- package/lib/providers/adapters/openaiTypes.js +1 -0
- package/lib/providers/derive.js +4 -0
- package/lib/providers/deriveCore.js +2 -7
- package/lib/providers/execution/admission.js +25 -0
- package/lib/providers/execution/index.js +34 -0
- package/lib/providers/execution/legacy.js +17 -0
- package/lib/providers/execution/legacyClassic.js +66 -0
- package/lib/providers/execution/legacyEdit.js +30 -0
- package/lib/providers/execution/legacyMultimode.js +34 -0
- package/lib/providers/execution/legacyNode.js +41 -0
- package/lib/providers/execution/types.js +1 -0
- package/lib/providers/registry.js +19 -14
- package/lib/providers/surfaceSupport.js +22 -0
- package/lib/responsesImageAdapter.js +1 -389
- package/lib/responsesTransport.js +190 -0
- package/lib/spriteJobEvents.js +20 -2
- package/lib/ssePublish.js +5 -4
- package/lib/videoExtendI2vOperation.js +76 -0
- package/package.json +5 -2
- package/routes/edit.js +31 -123
- package/routes/events.js +140 -63
- package/routes/mcpMedia.js +10 -2
- package/routes/models.js +28 -25
- package/routes/videoExtended.js +9 -68
- package/server.js +24 -46
- package/ui/dist/.vite/manifest.json +90 -63
- package/ui/dist/assets/{AgentWorkspace-PbBytTeV.js → AgentWorkspace-CL3MLv3l.js} +2 -2
- package/ui/dist/assets/App-B9cEM6dJ.js +9 -0
- package/ui/dist/assets/AssetGenWorkspace-swWPvXrx.js +2 -0
- package/ui/dist/assets/AssetsWorkspace-CRVeYU2F.js +1 -0
- package/ui/dist/assets/{CardNewsWorkspace-BS3jKOja.js → CardNewsWorkspace-BAOxWLKf.js} +2 -2
- package/ui/dist/assets/GenerationRequestLogPanel-DZtYZm-D.js +1 -0
- package/ui/dist/assets/HomeWorkspace-BeZ1s0Jm.js +1 -0
- package/ui/dist/assets/KeyingPanel-ZyBSXioh.js +1 -0
- package/ui/dist/assets/NodeCanvas-BvvcCErf.js +7 -0
- package/ui/dist/assets/{PromptBuilderPanel-D97A4pwB.js → PromptBuilderPanel-DBycmShA.js} +2 -2
- package/ui/dist/assets/PromptImportDialog-ChvMv7ek.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-60py7whH.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-CCRh_-_7.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-BFWQO8OT.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5Ghsgdj.js +1 -0
- package/ui/dist/assets/SpriteRecipeWorkspace-DRWdkmwU.js +1 -0
- package/ui/dist/assets/index-7ngEOVxt.js +5 -0
- package/ui/dist/assets/index-B5jy7ePg.css +1 -0
- package/ui/dist/assets/index-gxh_02DQ.js +22 -0
- package/ui/dist/assets/{pptxgen.es-D4pedbwM.js → pptxgen.es-OUNhQLEV.js} +1 -1
- package/ui/dist/assets/promptBuilderStore-BTUUzbBT.js +1 -0
- package/ui/dist/assets/useAgentDialogFocus-DSc2qY8p.js +1 -0
- package/ui/dist/index.html +3 -2
- package/ui/dist/assets/AssetGenWorkspace-B-TGlvVb.js +0 -2
- package/ui/dist/assets/AssetsWorkspace-LnGaW151.js +0 -1
- package/ui/dist/assets/GenerationRequestLogPanel-DuDWD_ic.js +0 -1
- package/ui/dist/assets/HomeWorkspace-DVP7UG6O.js +0 -1
- package/ui/dist/assets/KeyingPanel-5bRUFQaq.js +0 -1
- package/ui/dist/assets/NodeCanvas-BBEpERTb.js +0 -7
- package/ui/dist/assets/PromptImportDialog-C89MN5JY.js +0 -2
- package/ui/dist/assets/PromptImportDiscoverySection-BgRfDOs2.js +0 -1
- package/ui/dist/assets/PromptImportFolderSection-zTMyriT1.js +0 -1
- package/ui/dist/assets/PromptLibraryPanel-BD96qZu_.js +0 -2
- package/ui/dist/assets/SettingsWorkspace-BAOk3X_8.js +0 -1
- package/ui/dist/assets/SpriteRecipeWorkspace-ClIM5QRo.js +0 -1
- package/ui/dist/assets/index-0fpfY8vu.css +0 -1
- package/ui/dist/assets/index-BFciPGTn.js +0 -5
- package/ui/dist/assets/index-BbykeXR9.js +0 -30
- package/ui/dist/assets/promptBuilderStore-Do7M8g7J.js +0 -1
- package/ui/dist/assets/useAgentDialogFocus-B6-QgEVI.js +0 -1
package/bin/lib/doctor-checks.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createRequire } from "module";
|
|
2
1
|
import { createServer } from "net";
|
|
3
2
|
import { accessSync, constants, existsSync, statSync } from "fs";
|
|
4
3
|
import { execFileSync } from "child_process";
|
|
5
|
-
import { dirname
|
|
4
|
+
import { dirname } from "path";
|
|
6
5
|
import { config as runtimeConfig } from "../../config.js";
|
|
7
6
|
import { isSensitiveConfigKey } from "../../lib/configKeys.js";
|
|
7
|
+
import { checkNativeBinding, checkPackagedSkills } from "./doctor-runtime.js";
|
|
8
8
|
function hasSensitiveValue(value, path = "") {
|
|
9
9
|
if (!value || typeof value !== "object")
|
|
10
10
|
return false;
|
|
@@ -32,34 +32,20 @@ function probeNpmVersion() {
|
|
|
32
32
|
const raw = execFileSync("npm", ["-v"], { encoding: "utf8", timeout: 4000 }).trim();
|
|
33
33
|
const major = Number.parseInt(raw.split(".")[0] || "", 10);
|
|
34
34
|
if (Number.isFinite(major) && major >= 9)
|
|
35
|
-
return { kind: "pass", text: `npm ${raw} (>= 9)` };
|
|
36
|
-
return { kind: "warn", text: `npm ${raw} (recommend >= 9)` };
|
|
35
|
+
return { code: "NPM_READY", kind: "pass", text: `npm ${raw} (>= 9)` };
|
|
36
|
+
return { code: "NPM_OLD", kind: "warn", text: `npm ${raw} (recommend >= 9)` };
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
|
-
return { kind: "warn", text: "npm not found on PATH" };
|
|
39
|
+
return { code: "NPM_MISSING", kind: "warn", text: "npm not found on PATH" };
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
function probeDbPathWritable(dbPath) {
|
|
43
43
|
try {
|
|
44
44
|
accessSync(dirname(dbPath), constants.W_OK);
|
|
45
|
-
return { kind: "pass", text: `dbPath writable: ${dbPath}` };
|
|
45
|
+
return { code: "DB_PARENT_WRITABLE", kind: "pass", text: `dbPath writable: ${dbPath}` };
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
|
-
return { kind: "fail", text: `dbPath not writable: ${dbPath}` };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function probeBetterSqlite(root) {
|
|
52
|
-
try {
|
|
53
|
-
const requireFromRoot = createRequire(join(root, "package.json"));
|
|
54
|
-
const mod = requireFromRoot("better-sqlite3");
|
|
55
|
-
const Database = (mod.default ?? mod);
|
|
56
|
-
const db = new Database(":memory:");
|
|
57
|
-
db.close();
|
|
58
|
-
return { kind: "pass", text: "better-sqlite3 native binding loads" };
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
-
return { kind: "fail", text: `better-sqlite3 native binding failed: ${message}` };
|
|
48
|
+
return { code: "DB_PARENT_UNWRITABLE", kind: "fail", text: `dbPath not writable: ${dbPath}` };
|
|
63
49
|
}
|
|
64
50
|
}
|
|
65
51
|
function configPermissionLine(configFile, fileConfig) {
|
|
@@ -70,30 +56,28 @@ function configPermissionLine(configFile, fileConfig) {
|
|
|
70
56
|
if ((mode & 0o077) === 0)
|
|
71
57
|
return null;
|
|
72
58
|
return {
|
|
59
|
+
code: "CONFIG_PERMISSIONS",
|
|
73
60
|
kind: "warn",
|
|
74
61
|
text: `${configFile} is readable by group/other; consider chmod 600`,
|
|
75
62
|
};
|
|
76
63
|
}
|
|
77
|
-
export async function buildHardeningDoctorLines({ root, configFile, fileConfig, }) {
|
|
64
|
+
export async function buildHardeningDoctorLines({ root, configFile, fileConfig, includeInstallationChecks = true, }) {
|
|
78
65
|
const lines = [];
|
|
79
66
|
const portAvailable = await probePort(runtimeConfig.server.host, runtimeConfig.server.port);
|
|
80
67
|
lines.push({
|
|
68
|
+
code: portAvailable ? "PORT_AVAILABLE" : "PORT_IN_USE",
|
|
81
69
|
kind: "info",
|
|
82
70
|
text: `Preferred backend port ${runtimeConfig.server.port}: ${portAvailable ? "available" : "in use"}`,
|
|
83
71
|
});
|
|
84
72
|
lines.push({
|
|
73
|
+
code: runtimeConfig.features.cardNews ? "FEATURE_ENABLED" : "FEATURE_DISABLED",
|
|
85
74
|
kind: "info",
|
|
86
75
|
text: `Card News: ${runtimeConfig.features.cardNews ? "enabled" : "disabled"}`,
|
|
87
76
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
lines.push(existsSync(skillPath)
|
|
91
|
-
? { kind: "pass", text: `packaged skill found: ${skillPath}` }
|
|
92
|
-
: { kind: "fail", text: `packaged skill missing: ${skillPath}` });
|
|
93
|
-
}
|
|
77
|
+
if (includeInstallationChecks)
|
|
78
|
+
lines.push(...checkPackagedSkills(root), checkNativeBinding(root));
|
|
94
79
|
lines.push(probeNpmVersion());
|
|
95
80
|
lines.push(probeDbPathWritable(runtimeConfig.storage.dbPath));
|
|
96
|
-
lines.push(probeBetterSqlite(root));
|
|
97
81
|
const perm = configPermissionLine(configFile, fileConfig);
|
|
98
82
|
if (perm)
|
|
99
83
|
lines.push(perm);
|
package/bin/lib/doctor-media.js
CHANGED
|
@@ -4,12 +4,12 @@ const execFileAsync = promisify(execFile);
|
|
|
4
4
|
export async function buildMediaDoctorLines() {
|
|
5
5
|
try {
|
|
6
6
|
await execFileAsync("ffmpeg", ["-version"], { timeout: 4000 });
|
|
7
|
-
return [{ kind: "pass", text: "ffmpeg available on PATH" }];
|
|
7
|
+
return [{ code: "FFMPEG_READY", kind: "pass", text: "ffmpeg available on PATH" }];
|
|
8
8
|
}
|
|
9
9
|
catch (error) {
|
|
10
10
|
const code = error.code;
|
|
11
11
|
if (code === "ENOENT")
|
|
12
|
-
return [{ kind: "warn", text: "ffmpeg not on PATH (video features unavailable)" }];
|
|
13
|
-
return [{ kind: "warn", text: "ffmpeg probe failed" }];
|
|
12
|
+
return [{ code: "FFMPEG_MISSING", kind: "warn", text: "ffmpeg not on PATH (video features unavailable)" }];
|
|
13
|
+
return [{ code: "FFMPEG_PROBE_FAILED", kind: "warn", text: "ffmpeg probe failed" }];
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -22,58 +22,58 @@ function configString(fileConfig, key) {
|
|
|
22
22
|
function inspectApiKey(lane, credential, fileConfig) {
|
|
23
23
|
const value = firstEnv(credential.envVars) || configString(fileConfig, credential.configKey);
|
|
24
24
|
if (!value) {
|
|
25
|
-
return { lane, kind: "warn", text: `${lane}: api-key unset` };
|
|
25
|
+
return { code: "CREDENTIAL_MISSING", lane, kind: "warn", text: `${lane}: api-key unset` };
|
|
26
26
|
}
|
|
27
27
|
if (credential.keyPrefix && !value.startsWith(credential.keyPrefix)) {
|
|
28
|
-
return { lane, kind: "fail", text: `${lane}: api-key prefix mismatch (expected ${credential.keyPrefix})` };
|
|
28
|
+
return { code: "CREDENTIAL_SHAPE_INVALID", lane, kind: "fail", text: `${lane}: api-key prefix mismatch (expected ${credential.keyPrefix})` };
|
|
29
29
|
}
|
|
30
30
|
if (!credential.keyPrefix) {
|
|
31
31
|
// Several lanes (minimax region-specific, nai persistent tokens) have no
|
|
32
32
|
// stable prefix, so the message must stay vocabulary-neutral.
|
|
33
|
-
return { lane, kind: "pass", text: `${lane}: api-key present (no prefix check; this lane has no fixed key prefix)` };
|
|
33
|
+
return { code: "CREDENTIAL_PRESENT", lane, kind: "pass", text: `${lane}: api-key present (no prefix check; this lane has no fixed key prefix)` };
|
|
34
34
|
}
|
|
35
|
-
return { lane, kind: "pass", text: `${lane}: api-key present` };
|
|
35
|
+
return { code: "CREDENTIAL_PRESENT", lane, kind: "pass", text: `${lane}: api-key present` };
|
|
36
36
|
}
|
|
37
37
|
function inspectOauth(lane) {
|
|
38
38
|
if (lane === "oauth") {
|
|
39
39
|
const auth = detectCodexAuth();
|
|
40
40
|
if (auth.proxyReady)
|
|
41
|
-
return { lane, kind: "pass", text: `${lane}: file-backed Codex session ready` };
|
|
42
|
-
return { lane, kind: "fail", text: `${lane}: no file-backed Codex session; run ima2 login` };
|
|
41
|
+
return { code: "OAUTH_FILE_READY", lane, kind: "pass", text: `${lane}: file-backed Codex session ready` };
|
|
42
|
+
return { code: "OAUTH_FILE_REQUIRED", lane, kind: "fail", text: `${lane}: no file-backed Codex session; run ima2 login` };
|
|
43
43
|
}
|
|
44
44
|
if (lane === "grok") {
|
|
45
45
|
const home = homedir();
|
|
46
46
|
const files = [join(home, ".progrok", "auth.json"), join(home, ".grok", "auth.json")];
|
|
47
47
|
if (files.some((path) => existsSync(path))) {
|
|
48
|
-
return { lane, kind: "pass", text: `${lane}: local Grok auth file present` };
|
|
48
|
+
return { code: "CREDENTIAL_PRESENT", lane, kind: "pass", text: `${lane}: local Grok auth file present` };
|
|
49
49
|
}
|
|
50
|
-
return { lane, kind: "warn", text: `${lane}: no ~/.progrok or ~/.grok auth file` };
|
|
50
|
+
return { code: "CREDENTIAL_MISSING", lane, kind: "warn", text: `${lane}: no ~/.progrok or ~/.grok auth file` };
|
|
51
51
|
}
|
|
52
|
-
return { lane, kind: "warn", text: `${lane}: oauth-proxy has no lane-specific checker` };
|
|
52
|
+
return { code: "DIAGNOSTIC_UNKNOWN", lane, kind: "warn", text: `${lane}: oauth-proxy has no lane-specific checker` };
|
|
53
53
|
}
|
|
54
54
|
function inspectServiceAccount(lane, credential, fileConfig) {
|
|
55
55
|
const raw = firstEnv(credential.envVars) || configString(fileConfig, credential.configKey);
|
|
56
56
|
if (!raw)
|
|
57
|
-
return { lane, kind: "warn", text: `${lane}: service-account unset` };
|
|
57
|
+
return { code: "CREDENTIAL_MISSING", lane, kind: "warn", text: `${lane}: service-account unset` };
|
|
58
58
|
try {
|
|
59
59
|
const parsed = JSON.parse(raw);
|
|
60
60
|
if (parsed.type !== "service_account" || typeof parsed.project_id !== "string" || !parsed.project_id) {
|
|
61
|
-
return { lane, kind: "fail", text: `${lane}: service-account JSON missing type/project_id` };
|
|
61
|
+
return { code: "CREDENTIAL_SHAPE_INVALID", lane, kind: "fail", text: `${lane}: service-account JSON missing type/project_id` };
|
|
62
62
|
}
|
|
63
|
-
return { lane, kind: "pass", text: `${lane}: service-account JSON present` };
|
|
63
|
+
return { code: "CREDENTIAL_PRESENT", lane, kind: "pass", text: `${lane}: service-account JSON present` };
|
|
64
64
|
}
|
|
65
65
|
catch {
|
|
66
|
-
return { lane, kind: "fail", text: `${lane}: service-account is not JSON` };
|
|
66
|
+
return { code: "CREDENTIAL_SHAPE_INVALID", lane, kind: "fail", text: `${lane}: service-account is not JSON` };
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
function inspectLocalCli(lane, credential) {
|
|
70
70
|
const override = firstEnv(credential.envVars);
|
|
71
71
|
if (override) {
|
|
72
72
|
return existsSync(override)
|
|
73
|
-
? { lane, kind: "pass", text: `${lane}: local CLI override found` }
|
|
74
|
-
: { lane, kind: "fail", text: `${lane}: local CLI override missing` };
|
|
73
|
+
? { code: "LOCAL_CLI_FOUND", lane, kind: "pass", text: `${lane}: local CLI override found` }
|
|
74
|
+
: { code: "LOCAL_CLI_MISSING", lane, kind: "fail", text: `${lane}: local CLI override missing` };
|
|
75
75
|
}
|
|
76
|
-
return { lane, kind: "warn", text: `${lane}: local CLI env unset` };
|
|
76
|
+
return { code: "LOCAL_CLI_MISSING", lane, kind: "warn", text: `${lane}: local CLI env unset` };
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
79
|
* A local-http lane has no binary and no key: its env var holds a URL, so the
|
|
@@ -88,10 +88,10 @@ function inspectLocalHttp(lane, credential) {
|
|
|
88
88
|
const raw = firstEnv(credential.envVars) ?? runtimeConfig.comfy.defaultUrl;
|
|
89
89
|
try {
|
|
90
90
|
const origin = normalizeComfyOrigin(raw);
|
|
91
|
-
return { lane, kind: "pass", text: `${lane}: origin ${origin}` };
|
|
91
|
+
return { code: "LOCAL_ORIGIN_VALID", lane, kind: "pass", text: `${lane}: origin ${origin}` };
|
|
92
92
|
}
|
|
93
93
|
catch {
|
|
94
|
-
return { lane, kind: "fail", text: `${lane}: invalid origin
|
|
94
|
+
return { code: "LOCAL_ORIGIN_INVALID", lane, kind: "fail", text: `${lane}: invalid configured origin` };
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
export function inspectProviderLane(provider, fileConfig) {
|
|
@@ -126,7 +126,7 @@ export function listedValidateUrls() {
|
|
|
126
126
|
return url ? [url] : [];
|
|
127
127
|
}));
|
|
128
128
|
}
|
|
129
|
-
export async function verifyConfiguredKeys(fileConfig, fetchImpl = fetch) {
|
|
129
|
+
export async function verifyConfiguredKeys(fileConfig, fetchImpl = fetch, options = {}) {
|
|
130
130
|
const lines = [];
|
|
131
131
|
for (const provider of listProviders()) {
|
|
132
132
|
for (const credential of provider.credentials) {
|
|
@@ -138,23 +138,36 @@ export async function verifyConfiguredKeys(fileConfig, fetchImpl = fetch) {
|
|
|
138
138
|
const value = firstEnv(credential.envVars) || configString(fileConfig, credential.configKey);
|
|
139
139
|
if (!value)
|
|
140
140
|
continue;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const response = await fetchImpl(url, { headers });
|
|
146
|
-
if (response.ok) {
|
|
147
|
-
lines.push({ lane: provider.id, kind: "pass", text: `${provider.id}: validateUrl ok` });
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
lines.push({ lane: provider.id, kind: "fail", text: `${provider.id}: AUTH_INVALID (${response.status})` });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
-
lines.push({ lane: provider.id, kind: "fail", text: `${provider.id}: AUTH_INVALID (${message})` });
|
|
156
|
-
}
|
|
141
|
+
const headers = credential.keyVocabulary === "gemini"
|
|
142
|
+
? { "x-goog-api-key": value } : { Authorization: `Bearer ${value}` };
|
|
143
|
+
const code = await verifyKey(url, headers, fetchImpl, options.timeoutMs ?? runtimeConfig.diagnostics.keyTimeoutMs);
|
|
144
|
+
lines.push({ lane: provider.id, code, kind: code === "AUTH_VERIFIED" ? "pass" : "fail", evidence: "remote-auth", text: `${provider.id}: ${code}` });
|
|
157
145
|
}
|
|
158
146
|
}
|
|
159
147
|
return lines;
|
|
160
148
|
}
|
|
149
|
+
async function verifyKey(url, headers, fetchImpl, requestedTimeout) {
|
|
150
|
+
const timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout > 0 ? Math.min(requestedTimeout, 30000) : 5000;
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
let timer;
|
|
153
|
+
const deadline = new Promise((_, reject) => {
|
|
154
|
+
timer = setTimeout(() => { controller.abort(); reject(Error("AUTH_TIMEOUT")); }, timeoutMs);
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
const request = (async () => {
|
|
158
|
+
const response = await fetchImpl(url, { headers, signal: controller.signal, redirect: "error" });
|
|
159
|
+
const code = response.ok ? "AUTH_VERIFIED" : response.status === 401 || response.status === 403 ? "AUTH_INVALID"
|
|
160
|
+
: response.status === 429 ? "AUTH_RATE_LIMITED" : "AUTH_UPSTREAM_FAILED";
|
|
161
|
+
await response.body?.cancel();
|
|
162
|
+
return code;
|
|
163
|
+
})();
|
|
164
|
+
return await Promise.race([request, deadline]);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return controller.signal.aborted ? "AUTH_TIMEOUT" : "AUTH_NETWORK_FAILED";
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
controller.abort();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { listProviders } from "../../lib/providers/registry.js";
|
|
2
|
+
const REINSTALL = "Reinstall the same approved package version.";
|
|
3
|
+
const CODES = {
|
|
4
|
+
NODE_RUNTIME_OK: { message: "Node satisfies the package engine requirement." },
|
|
5
|
+
NODE_RUNTIME_UNSUPPORTED: { message: "Node does not satisfy the package engine requirement.", action: "Install a Node version satisfying package.json engines.node." },
|
|
6
|
+
ENGINE_REQUIREMENT_INVALID: { message: "The package Node engine requirement is invalid.", action: "Inspect package.json engines.node." },
|
|
7
|
+
INSTALL_PACKAGE_OK: { message: "Package metadata is present." },
|
|
8
|
+
INSTALL_PACKAGE_MISSING: { message: "Package metadata is missing or invalid.", action: REINSTALL },
|
|
9
|
+
INSTALL_DEPENDENCIES_OK: { message: "Required runtime dependencies and CLI entries resolve." },
|
|
10
|
+
INSTALL_DEPENDENCY_MISSING: { message: "A required runtime dependency or CLI entry is missing.", action: REINSTALL },
|
|
11
|
+
INSTALL_NATIVE_OK: { message: "The in-memory SQLite native binding loads." },
|
|
12
|
+
INSTALL_NATIVE_FAILED: { message: "The SQLite native binding could not load.", action: REINSTALL },
|
|
13
|
+
INSTALL_SKILLS_OK: { message: "Packaged skills are present." },
|
|
14
|
+
INSTALL_SKILL_MISSING: { message: "A packaged skill is missing.", action: REINSTALL },
|
|
15
|
+
INSTALL_UI_OK: { message: "The packaged UI build is available." },
|
|
16
|
+
INSTALL_UI_MISSING: { message: "The packaged UI build is missing.", action: REINSTALL },
|
|
17
|
+
INSTALL_UI_STALE: { message: "The source checkout UI build is stale.", action: "Run the project UI build in the source checkout." },
|
|
18
|
+
NPM_READY: { message: "The package manager is available." },
|
|
19
|
+
NPM_MISSING: { message: "The package manager is unavailable.", action: "Install the required package manager." },
|
|
20
|
+
NPM_OLD: { message: "The package manager is older than recommended.", action: "Update the package manager." },
|
|
21
|
+
DB_PARENT_WRITABLE: { message: "The configured database parent is writable." },
|
|
22
|
+
DB_PARENT_UNWRITABLE: { message: "The configured database parent is not writable.", action: "Check configured directory permissions." },
|
|
23
|
+
CONFIG_PERMISSIONS: { message: "Configuration permissions allow other users to read it.", action: "Restrict access to the configuration file." },
|
|
24
|
+
CONFIG_INVALID: { message: "Configuration is not a valid JSON object.", action: "Repair the configuration JSON object." },
|
|
25
|
+
CONFIG_PRESENT: { message: "A provider configuration is present; execution is not verified." },
|
|
26
|
+
CONFIG_MISSING: { message: "No provider configuration is selected.", action: "Run ima2 setup when ready to configure a provider." },
|
|
27
|
+
ADVERTISEMENT_INVALID: { message: "The local server advertisement is invalid.", action: "Start the intended local server." },
|
|
28
|
+
CREDENTIAL_PRESENT: { message: "A local credential is present; remote validity is not verified." },
|
|
29
|
+
CREDENTIAL_MISSING: { message: "A local credential is not configured.", action: "Configure this provider lane when needed." },
|
|
30
|
+
CREDENTIAL_SHAPE_INVALID: { message: "The configured credential has an invalid shape.", action: "Check this provider credential configuration." },
|
|
31
|
+
OAUTH_FILE_READY: { message: "A file-backed OAuth session is available." },
|
|
32
|
+
OAUTH_FILE_REQUIRED: { message: "A file-backed OAuth session is required.", action: "Run ima2 login yourself." },
|
|
33
|
+
LOCAL_CLI_FOUND: { message: "The configured local CLI exists." },
|
|
34
|
+
LOCAL_CLI_MISSING: { message: "The local CLI is not configured or missing.", action: "Check the configured executable." },
|
|
35
|
+
LOCAL_ORIGIN_VALID: { message: "The configured local origin has a valid shape; reachability is not verified." },
|
|
36
|
+
LOCAL_ORIGIN_INVALID: { message: "The configured local origin is invalid.", action: "Correct the configured local origin." },
|
|
37
|
+
FFMPEG_READY: { message: "ffmpeg is available." },
|
|
38
|
+
FFMPEG_MISSING: { message: "ffmpeg is not installed.", action: "Install ffmpeg for video tasks." },
|
|
39
|
+
FFMPEG_PROBE_FAILED: { message: "The ffmpeg check failed.", action: "Check the ffmpeg installation." },
|
|
40
|
+
AUTH_VERIFIED: { message: "The non-generating authentication endpoint accepted the credential." },
|
|
41
|
+
AUTH_INVALID: { message: "The authentication endpoint rejected the credential.", action: "Sign in again or replace this provider credential." },
|
|
42
|
+
AUTH_RATE_LIMITED: { message: "The authentication check was rate limited.", action: "Retry later; do not reset the credential for this result." },
|
|
43
|
+
AUTH_UPSTREAM_FAILED: { message: "The authentication endpoint returned an upstream failure.", action: "Check provider status and retry later." },
|
|
44
|
+
AUTH_NETWORK_FAILED: { message: "The authentication check could not complete.", action: "Inspect network connectivity and retry later." },
|
|
45
|
+
AUTH_TIMEOUT: { message: "The authentication check timed out.", action: "Check connectivity and retry later." },
|
|
46
|
+
RUNTIME_READY: { message: "The local server responded with the expected version; upstream generation is not verified." },
|
|
47
|
+
RUNTIME_AUTH_REQUIRED: { message: "The local server requires authorization.", action: "Use the configured authorized client." },
|
|
48
|
+
RUNTIME_VERSION_MISMATCH: { message: "The local server version differs from this CLI.", action: "Start the intended version of the server." },
|
|
49
|
+
RUNTIME_UNREACHABLE: { message: "The requested local server could not be reached.", action: "Start the intended server and check its address." },
|
|
50
|
+
RUNTIME_TIMEOUT: { message: "The local server health request timed out.", action: "Check the intended local server." },
|
|
51
|
+
RUNTIME_INVALID_HEALTH: { message: "The local server returned an invalid health response.", action: "Verify that the endpoint belongs to ima2-gen." },
|
|
52
|
+
RUNTIME_ORIGIN_INVALID: { message: "Runtime diagnostics require an explicit loopback origin without credentials.", action: "Use a loopback HTTP or HTTPS origin with no path, query or fragment." },
|
|
53
|
+
PORT_AVAILABLE: { message: "The preferred local port is available." },
|
|
54
|
+
PORT_IN_USE: { message: "The preferred local port is in use." },
|
|
55
|
+
FEATURE_ENABLED: { message: "The optional feature is enabled." },
|
|
56
|
+
FEATURE_DISABLED: { message: "The optional feature is disabled." },
|
|
57
|
+
DIAGNOSTIC_UNKNOWN: { message: "A diagnostic returned an unrecognized result.", action: "Inspect this package version's diagnostic support." },
|
|
58
|
+
};
|
|
59
|
+
export function buildDoctorReport(input) {
|
|
60
|
+
const lanes = new Set(listProviders().map((provider) => provider.id));
|
|
61
|
+
const checks = input.lines.map((line) => {
|
|
62
|
+
const known = Object.hasOwn(CODES, line.code), code = known ? line.code : "DIAGNOSTIC_UNKNOWN";
|
|
63
|
+
const definition = CODES[code];
|
|
64
|
+
const kind = known ? line.kind : line.kind === "fail" ? "fail" : "warn";
|
|
65
|
+
const evidence = line.evidence === "remote-auth" || line.evidence === "local-http" ? line.evidence : "local";
|
|
66
|
+
return { code, kind, evidence, ...definition, ...(line.lane && lanes.has(line.lane) ? { lane: line.lane } : {}) };
|
|
67
|
+
});
|
|
68
|
+
const passed = checks.filter((line) => line.kind === "pass").length;
|
|
69
|
+
const failed = checks.filter((line) => line.kind === "fail").length;
|
|
70
|
+
const warned = checks.filter((line) => line.kind === "warn").length;
|
|
71
|
+
return { schemaVersion: 1, version: input.version, mode: input.mode, checks,
|
|
72
|
+
summary: { passed, failed, warned, exitCode: failed ? 1 : 0 } };
|
|
73
|
+
}
|
|
74
|
+
export function renderDoctorReport(report) {
|
|
75
|
+
const checks = report.checks.map((line) => `[${line.kind}] ${line.lane ? line.lane + ": " : ""}${line.message}${line.action ? " Next: " + line.action : ""}`);
|
|
76
|
+
return [`ima2-gen ${report.version} — ${report.mode} doctor`, ...checks,
|
|
77
|
+
`${report.summary.passed} passed, ${report.summary.failed} failed, ${report.summary.warned} warnings`].join("\n");
|
|
78
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { getUiDistBuildStatus } from "./ui-build.js";
|
|
5
|
+
const RUNTIME_DEPENDENCIES = ["express", "better-sqlite3", "openai", "openai-oauth", "progrok/package.json", "@openai/codex/package.json", "zod"];
|
|
6
|
+
const REQUIRED_BINS = [["openai-oauth", "openai-oauth"], ["@openai/codex", "codex"]];
|
|
7
|
+
const line = (code, kind, text) => ({ code, kind, text, evidence: "local" });
|
|
8
|
+
export function parseMinimumNodeMajor(engine) {
|
|
9
|
+
if (typeof engine !== "string" || !/^>=[1-9][0-9]*$/.test(engine))
|
|
10
|
+
throw Error("ENGINE_REQUIREMENT_INVALID");
|
|
11
|
+
const major = Number(engine.slice(2));
|
|
12
|
+
if (!Number.isSafeInteger(major))
|
|
13
|
+
throw Error("ENGINE_REQUIREMENT_INVALID");
|
|
14
|
+
return major;
|
|
15
|
+
}
|
|
16
|
+
export function checkNodeEngine(version, engine) {
|
|
17
|
+
try {
|
|
18
|
+
const minimum = parseMinimumNodeMajor(engine), match = /^v?([0-9]+)\.[0-9]+\.[0-9]+$/.exec(version);
|
|
19
|
+
const major = match ? Number(match[1]) : NaN;
|
|
20
|
+
if (!Number.isSafeInteger(major) || major < minimum)
|
|
21
|
+
return line("NODE_RUNTIME_UNSUPPORTED", "fail", `Node runtime requires >=${minimum}`);
|
|
22
|
+
return line("NODE_RUNTIME_OK", "pass", `Node.js ${version} (>=${minimum})`);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return line("ENGINE_REQUIREMENT_INVALID", "fail", "Package Node engine requirement is invalid");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function missingRuntimeDeps(root) {
|
|
29
|
+
const require = createRequire(join(resolve(root), "package.json")), missing = [];
|
|
30
|
+
for (const dependency of RUNTIME_DEPENDENCIES) {
|
|
31
|
+
try {
|
|
32
|
+
require.resolve(dependency);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
missing.push(dependency.replace(/\/package\.json$/, ""));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [name, bin] of REQUIRED_BINS) {
|
|
39
|
+
try {
|
|
40
|
+
let manifestPath;
|
|
41
|
+
try {
|
|
42
|
+
manifestPath = require.resolve(name + "/package.json");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
let parent = dirname(require.resolve(name));
|
|
46
|
+
while (!existsSync(join(parent, "package.json"))) {
|
|
47
|
+
const next = dirname(parent);
|
|
48
|
+
if (next === parent)
|
|
49
|
+
throw Error("MISSING_MANIFEST");
|
|
50
|
+
parent = next;
|
|
51
|
+
}
|
|
52
|
+
manifestPath = join(parent, "package.json");
|
|
53
|
+
}
|
|
54
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
55
|
+
const entry = typeof manifest.bin === "string" ? manifest.bin
|
|
56
|
+
: manifest.bin && typeof manifest.bin === "object" ? manifest.bin[bin] : undefined;
|
|
57
|
+
if (manifest.name !== name || typeof entry !== "string" || !existsSync(resolve(dirname(manifestPath), entry)))
|
|
58
|
+
throw Error("MISSING_BIN");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
if (!missing.includes(name))
|
|
62
|
+
missing.push(name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return missing;
|
|
66
|
+
}
|
|
67
|
+
export function checkNativeBinding(root) {
|
|
68
|
+
let db;
|
|
69
|
+
try {
|
|
70
|
+
const require = createRequire(join(resolve(root), "package.json"));
|
|
71
|
+
const module = require("better-sqlite3");
|
|
72
|
+
const Database = (module.default ?? module);
|
|
73
|
+
db = new Database(":memory:");
|
|
74
|
+
db.close();
|
|
75
|
+
db = undefined;
|
|
76
|
+
return line("INSTALL_NATIVE_OK", "pass", "better-sqlite3 native binding loads");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return line("INSTALL_NATIVE_FAILED", "fail", "better-sqlite3 native binding failed; reinstall the approved package");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
try {
|
|
83
|
+
db?.close();
|
|
84
|
+
}
|
|
85
|
+
catch { /* The failed native check is already reported. */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function checkPackagedSkills(root) {
|
|
89
|
+
return ["ima2", "ima2-front", "ima2-uiux"].map((name) => existsSync(join(root, "skills", name, "SKILL.md"))
|
|
90
|
+
? line("INSTALL_SKILLS_OK", "pass", `packaged skill found: ${name}`)
|
|
91
|
+
: line("INSTALL_SKILL_MISSING", "fail", `packaged skill missing: ${name}`));
|
|
92
|
+
}
|
|
93
|
+
export function buildInstallationDoctorLines(root) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
try {
|
|
96
|
+
const metadata = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
97
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
98
|
+
throw Error("INSTALL_PACKAGE_MISSING");
|
|
99
|
+
lines.push(line("INSTALL_PACKAGE_OK", "pass", "package.json found"), checkNodeEngine(process.version, metadata.engines?.node));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
lines.push(line("INSTALL_PACKAGE_MISSING", "fail", "package.json missing or invalid"));
|
|
103
|
+
}
|
|
104
|
+
const missing = missingRuntimeDeps(root);
|
|
105
|
+
lines.push(missing.length ? line("INSTALL_DEPENDENCY_MISSING", "fail", `Missing runtime dependencies: ${missing.join(", ")}`)
|
|
106
|
+
: line("INSTALL_DEPENDENCIES_OK", "pass", "Runtime dependencies and bins resolve"));
|
|
107
|
+
lines.push(checkNativeBinding(root), ...checkPackagedSkills(root));
|
|
108
|
+
try {
|
|
109
|
+
const ui = getUiDistBuildStatus(root);
|
|
110
|
+
const code = ui.reason === "stale" ? "INSTALL_UI_STALE"
|
|
111
|
+
: ui.reason === "missing" || ui.reason === "missing-source-and-dist" ? "INSTALL_UI_MISSING" : "INSTALL_UI_OK";
|
|
112
|
+
lines.push(line(code, code === "INSTALL_UI_OK" ? "pass" : code === "INSTALL_UI_STALE" ? "warn" : "fail", code));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
lines.push(line("INSTALL_UI_MISSING", "fail", "UI build could not be inspected"));
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
export async function probeDoctorRuntime(input) {
|
|
120
|
+
const result = (code, kind = "fail") => [{ code, kind, text: code, evidence: "local-http" }];
|
|
121
|
+
let url;
|
|
122
|
+
try {
|
|
123
|
+
url = new URL(input.url);
|
|
124
|
+
if (!["http:", "https:"].includes(url.protocol) || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)
|
|
125
|
+
|| url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
126
|
+
return result("RUNTIME_ORIGIN_INVALID");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return result("RUNTIME_ORIGIN_INVALID");
|
|
130
|
+
}
|
|
131
|
+
const timeoutMs = Number.isFinite(input.timeoutMs) && input.timeoutMs > 0 ? Math.min(input.timeoutMs, 30000) : 1500;
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
let timeout, response;
|
|
134
|
+
const deadline = new Promise((_, reject) => {
|
|
135
|
+
timeout = setTimeout(() => { controller.abort(); reject(Error("RUNTIME_TIMEOUT")); }, timeoutMs);
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
const work = (async () => {
|
|
139
|
+
response = await (input.fetchImpl ?? fetch)(new URL("/api/health", url), {
|
|
140
|
+
redirect: "error", signal: controller.signal, headers: { Connection: "close" },
|
|
141
|
+
});
|
|
142
|
+
if (response.status === 401 || response.status === 403) {
|
|
143
|
+
await response.body?.cancel();
|
|
144
|
+
return result("RUNTIME_AUTH_REQUIRED", "warn");
|
|
145
|
+
}
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
await response.body?.cancel();
|
|
148
|
+
return result("RUNTIME_INVALID_HEALTH");
|
|
149
|
+
}
|
|
150
|
+
let data;
|
|
151
|
+
try {
|
|
152
|
+
data = await response.json();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return result(controller.signal.aborted ? "RUNTIME_TIMEOUT" : "RUNTIME_INVALID_HEALTH");
|
|
156
|
+
}
|
|
157
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
158
|
+
return result("RUNTIME_INVALID_HEALTH");
|
|
159
|
+
const health = data;
|
|
160
|
+
if (health.ok !== true || typeof health.version !== "string" || (health.pid !== undefined && (typeof health.pid !== "number" || !Number.isFinite(health.pid))))
|
|
161
|
+
return result("RUNTIME_INVALID_HEALTH");
|
|
162
|
+
return health.version === input.expectedVersion ? result("RUNTIME_READY", "pass") : result("RUNTIME_VERSION_MISMATCH", "warn");
|
|
163
|
+
})();
|
|
164
|
+
return await Promise.race([work, deadline]);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return result(controller.signal.aborted ? "RUNTIME_TIMEOUT" : "RUNTIME_UNREACHABLE");
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
controller.abort();
|
|
172
|
+
}
|
|
173
|
+
}
|
package/bin/lib/mcpJob.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { openSse, sseUrlWithCursor } from "./sse.js";
|
|
2
2
|
import { normalizeTerminalStatus } from "../../lib/jobStatus.js";
|
|
3
|
+
import { fetchServer } from "./client.js";
|
|
4
|
+
const JOB_TRACKING_TIMEOUT_MESSAGE = "Job tracking expired; upstream completion is unknown. Inspect history before retrying.";
|
|
3
5
|
export class McpJobError extends Error {
|
|
4
6
|
code;
|
|
5
7
|
status;
|
|
@@ -54,7 +56,7 @@ async function responseError(response) {
|
|
|
54
56
|
}
|
|
55
57
|
async function submitJob(opts, signal) {
|
|
56
58
|
try {
|
|
57
|
-
const response = await
|
|
59
|
+
const response = await fetchServer(opts.serverBase, opts.postPath ?? "/api/mcp/generate", {
|
|
58
60
|
method: "POST",
|
|
59
61
|
headers: { "Content-Type": "application/json" },
|
|
60
62
|
body: JSON.stringify({ ...opts.body, kind: opts.kind, requestId: opts.requestId }),
|
|
@@ -121,26 +123,33 @@ function matchingOutcome(event, opts) {
|
|
|
121
123
|
}
|
|
122
124
|
else {
|
|
123
125
|
const envErr = asRecord(envelope.error);
|
|
126
|
+
const code = typeof envErr?.code === "string" ? envErr.code
|
|
127
|
+
: typeof data.code === "string" ? data.code : fallbackCode(phase);
|
|
124
128
|
return {
|
|
125
129
|
kind: "error",
|
|
126
|
-
error:
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
error: code === "JOB_TRACKING_TIMEOUT"
|
|
131
|
+
? new McpJobError(code, JOB_TRACKING_TIMEOUT_MESSAGE, { status: 504 })
|
|
132
|
+
: new McpJobError(code, errorMessage(data)),
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
if (event.event === "done")
|
|
133
137
|
return doneResult(data);
|
|
134
138
|
if (event.event === "error") {
|
|
139
|
+
const code = typeof data.code === "string" ? data.code : "MCP_JOB_FAILED";
|
|
135
140
|
return {
|
|
136
141
|
kind: "error",
|
|
137
|
-
error:
|
|
142
|
+
error: code === "JOB_TRACKING_TIMEOUT"
|
|
143
|
+
? new McpJobError(code, JOB_TRACKING_TIMEOUT_MESSAGE, { status: 504 })
|
|
144
|
+
: new McpJobError(code, typeof data.message === "string" ? data.message : "MCP job failed"),
|
|
138
145
|
};
|
|
139
146
|
}
|
|
140
147
|
return null;
|
|
141
148
|
}
|
|
142
149
|
async function consumeStream(stream, opts) {
|
|
143
|
-
|
|
150
|
+
// Keep the server's pre-job bookmark until an event is actually observed.
|
|
151
|
+
// This makes an EOF before the first frame recoverable without reposting.
|
|
152
|
+
let lastEventId = stream.initialEventId;
|
|
144
153
|
try {
|
|
145
154
|
for await (const event of stream.events) {
|
|
146
155
|
if (event.id !== undefined)
|
|
@@ -160,6 +169,9 @@ function terminalResult(job) {
|
|
|
160
169
|
const meta = asRecord(job.meta) ?? {};
|
|
161
170
|
const normalized = normalizeTerminalStatus(status);
|
|
162
171
|
if (normalized === "error") {
|
|
172
|
+
if (job.errorCode === "JOB_TRACKING_TIMEOUT") {
|
|
173
|
+
return new McpJobError("JOB_TRACKING_TIMEOUT", JOB_TRACKING_TIMEOUT_MESSAGE, { status: 504 });
|
|
174
|
+
}
|
|
163
175
|
return new McpJobError(typeof job.errorCode === "string" ? job.errorCode : "MCP_JOB_FAILED", typeof meta.message === "string" ? meta.message : "MCP job failed");
|
|
164
176
|
}
|
|
165
177
|
// A canceled job is terminal too. Falling through would report the generic
|
|
@@ -179,7 +191,7 @@ function terminalResult(job) {
|
|
|
179
191
|
}
|
|
180
192
|
async function recoverReplayGap(opts, signal) {
|
|
181
193
|
try {
|
|
182
|
-
const response = await
|
|
194
|
+
const response = await fetchServer(opts.serverBase, "/api/inflight?includeTerminal=1", { signal });
|
|
183
195
|
if (!response.ok)
|
|
184
196
|
throw await responseError(response);
|
|
185
197
|
const envelope = asRecord(await response.json()) ?? {};
|
package/bin/lib/output.js
CHANGED
|
@@ -78,6 +78,9 @@ export function exitCodeForError(e) {
|
|
|
78
78
|
const err = e;
|
|
79
79
|
if (err?.code === "SERVER_UNREACHABLE")
|
|
80
80
|
return 3;
|
|
81
|
+
if (err?.status === 401 || err?.status === 403 || ["LAN_TOKEN_REQUIRED", "LOCAL_HOST_REJECTED",
|
|
82
|
+
"LOCAL_ORIGIN_REJECTED", "SERVER_ACCESS_DENIED"].includes(err?.code ?? ""))
|
|
83
|
+
return 4;
|
|
81
84
|
if (err?.code === "APIKEY_DISABLED")
|
|
82
85
|
return 4;
|
|
83
86
|
if (err?.code === "AUTH_CHATGPT_EXPIRED" || err?.code === "OAUTH_UNAVAILABLE")
|
package/bin/lib/platform.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Cross-platform helpers (Windows / macOS / Linux / WSL).
|
|
2
2
|
// Keep this file tiny & dependency-free. Node 18+ only.
|
|
3
|
-
import { spawn, execSync } from "node:child_process";
|
|
3
|
+
import { spawn, execFileSync, execSync } from "node:child_process";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { errInfo } from "../../lib/errInfo.js";
|
|
6
6
|
export const isWin = process.platform === "win32";
|
|
@@ -52,21 +52,24 @@ export function spawnBin(name, args, opts = {}) {
|
|
|
52
52
|
*/
|
|
53
53
|
export function openUrl(url) {
|
|
54
54
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const parsed = new URL(url);
|
|
56
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
57
|
+
return { ok: false, error: "only http(s) URLs can be opened" };
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
if (isMac) {
|
|
60
|
+
execFileSync("open", [url], { stdio: "ignore" });
|
|
60
61
|
}
|
|
61
|
-
else if (isWsl()) {
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
else if (isWin || isWsl()) {
|
|
63
|
+
// Only base64 data varies; URL punctuation never enters PowerShell syntax.
|
|
64
|
+
const encoded = Buffer.from(url, "utf8").toString("base64");
|
|
65
|
+
const command = `$u=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')); Start-Process -FilePath $u -ErrorAction Stop`;
|
|
66
|
+
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(command, "utf16le").toString("base64")], { stdio: "ignore", windowsHide: true });
|
|
64
67
|
}
|
|
65
68
|
else {
|
|
66
69
|
if (!hasDesktopSession()) {
|
|
67
70
|
return { ok: false, error: "no desktop session (DISPLAY/WAYLAND_DISPLAY unset)" };
|
|
68
71
|
}
|
|
69
|
-
|
|
72
|
+
execFileSync("xdg-open", [url], { stdio: "ignore" });
|
|
70
73
|
}
|
|
71
74
|
return { ok: true };
|
|
72
75
|
}
|