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.
Files changed (157) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/commands/capabilities.js +5 -3
  5. package/bin/commands/config.js +15 -4
  6. package/bin/commands/defaults.js +7 -5
  7. package/bin/commands/doctor.js +88 -39
  8. package/bin/commands/gen.js +21 -4
  9. package/bin/commands/models.js +2 -2
  10. package/bin/commands/ping.js +1 -1
  11. package/bin/commands/prompt.js +5 -3
  12. package/bin/commands/service.js +32 -4
  13. package/bin/commands/tools.js +19 -3
  14. package/bin/commands/upscale.js +18 -4
  15. package/bin/commands/video.js +37 -50
  16. package/bin/ima2.js +28 -9
  17. package/bin/lib/characterResolve.js +2 -1
  18. package/bin/lib/client.js +155 -20
  19. package/bin/lib/config-store.js +40 -7
  20. package/bin/lib/doctor-bundle.js +6 -2
  21. package/bin/lib/doctor-checks.js +13 -29
  22. package/bin/lib/doctor-media.js +3 -3
  23. package/bin/lib/doctor-providers.js +48 -35
  24. package/bin/lib/doctor-report.js +78 -0
  25. package/bin/lib/doctor-runtime.js +173 -0
  26. package/bin/lib/mcpJob.js +19 -7
  27. package/bin/lib/output.js +3 -0
  28. package/bin/lib/platform.js +12 -9
  29. package/bin/lib/recover-output.js +9 -2
  30. package/bin/lib/sse.js +9 -1
  31. package/bin/lib/videoMcp.js +16 -8
  32. package/config.js +41 -5
  33. package/docs/API.md +154 -16
  34. package/docs/CLI.md +65 -1
  35. package/docs/DOCKER.md +19 -4
  36. package/docs/DOCKER.zh-CN.md +11 -4
  37. package/docs/DOCKER.zh-TW.md +11 -4
  38. package/docs/FAQ.md +13 -0
  39. package/docs/NPX_QUICKSTART.md +6 -0
  40. package/docs/README.ja.md +15 -2
  41. package/docs/README.ko.md +15 -2
  42. package/docs/README.zh-CN.md +15 -2
  43. package/docs/README.zh-TW.md +15 -2
  44. package/docs/migration/runtime-test-inventory.md +79 -8
  45. package/lib/agyArtifact.js +82 -0
  46. package/lib/agyArtifactRead.js +249 -0
  47. package/lib/agyImageAdapter.js +2 -329
  48. package/lib/agyProcess.js +122 -0
  49. package/lib/apiRequestBudget.js +48 -0
  50. package/lib/assetLifecycle.js +42 -9
  51. package/lib/canvasVersionStore.js +46 -14
  52. package/lib/capabilities.js +7 -1
  53. package/lib/configKeys.js +2 -0
  54. package/lib/errors/providerMap.js +26 -0
  55. package/lib/eventBus.js +10 -6
  56. package/lib/eventsPolicy.js +9 -0
  57. package/lib/geminiApiImageAdapter.js +1 -214
  58. package/lib/generatePipeline.js +39 -165
  59. package/lib/generatedMediaAccess.js +74 -0
  60. package/lib/generationErrors.js +3 -22
  61. package/lib/grokImageAdapter.js +2 -353
  62. package/lib/grokImageCore.js +1 -57
  63. package/lib/grokImageDownload.js +159 -0
  64. package/lib/grokImageDownloadPolicy.js +124 -0
  65. package/lib/grokImagePlanner.js +301 -0
  66. package/lib/grokMultimodeAdapter.js +1 -75
  67. package/lib/grokVideoDownload.js +137 -34
  68. package/lib/inflight.js +112 -112
  69. package/lib/jobs/terminalStore.js +45 -0
  70. package/lib/lanSessionStore.js +131 -0
  71. package/lib/localAccessPolicy.js +180 -0
  72. package/lib/localLanAccess.js +281 -0
  73. package/lib/logger.js +22 -13
  74. package/lib/mcp/downloadMediaResult.js +72 -78
  75. package/lib/multimodePipeline.js +39 -126
  76. package/lib/nodeGeneration.js +44 -101
  77. package/lib/nodeStore.js +25 -11
  78. package/lib/nodeValidation.js +5 -1
  79. package/lib/pinnedHttpGet.js +174 -0
  80. package/lib/promptImport/githubFolder.js +32 -18
  81. package/lib/promptImport/githubSource.js +14 -10
  82. package/lib/promptImport/parsePromptCandidates.js +32 -2
  83. package/lib/providers/adapters/agyOperations.js +179 -0
  84. package/lib/providers/adapters/comfy.js +1 -0
  85. package/lib/providers/adapters/geminiOperations.js +214 -0
  86. package/lib/providers/adapters/googleExecution.js +97 -0
  87. package/lib/providers/adapters/grokExecution.js +137 -0
  88. package/lib/providers/adapters/grokMultimodeOperations.js +83 -0
  89. package/lib/providers/adapters/grokOperations.js +63 -0
  90. package/lib/providers/adapters/openaiExecution.js +118 -0
  91. package/lib/providers/adapters/openaiOperations.js +201 -0
  92. package/lib/providers/adapters/openaiTypes.js +1 -0
  93. package/lib/providers/derive.js +4 -0
  94. package/lib/providers/deriveCore.js +2 -7
  95. package/lib/providers/execution/admission.js +25 -0
  96. package/lib/providers/execution/index.js +34 -0
  97. package/lib/providers/execution/legacy.js +17 -0
  98. package/lib/providers/execution/legacyClassic.js +66 -0
  99. package/lib/providers/execution/legacyEdit.js +30 -0
  100. package/lib/providers/execution/legacyMultimode.js +34 -0
  101. package/lib/providers/execution/legacyNode.js +41 -0
  102. package/lib/providers/execution/types.js +1 -0
  103. package/lib/providers/registry.js +19 -14
  104. package/lib/providers/surfaceSupport.js +22 -0
  105. package/lib/responsesImageAdapter.js +1 -389
  106. package/lib/responsesTransport.js +190 -0
  107. package/lib/spriteJobEvents.js +20 -2
  108. package/lib/ssePublish.js +5 -4
  109. package/lib/videoExtendI2vOperation.js +76 -0
  110. package/package.json +5 -2
  111. package/routes/edit.js +31 -123
  112. package/routes/events.js +140 -63
  113. package/routes/mcpMedia.js +10 -2
  114. package/routes/models.js +28 -25
  115. package/routes/videoExtended.js +9 -68
  116. package/server.js +24 -46
  117. package/ui/dist/.vite/manifest.json +90 -63
  118. package/ui/dist/assets/{AgentWorkspace-PbBytTeV.js → AgentWorkspace-CL3MLv3l.js} +2 -2
  119. package/ui/dist/assets/App-B9cEM6dJ.js +9 -0
  120. package/ui/dist/assets/AssetGenWorkspace-swWPvXrx.js +2 -0
  121. package/ui/dist/assets/AssetsWorkspace-CRVeYU2F.js +1 -0
  122. package/ui/dist/assets/{CardNewsWorkspace-BS3jKOja.js → CardNewsWorkspace-BAOxWLKf.js} +2 -2
  123. package/ui/dist/assets/GenerationRequestLogPanel-DZtYZm-D.js +1 -0
  124. package/ui/dist/assets/HomeWorkspace-BeZ1s0Jm.js +1 -0
  125. package/ui/dist/assets/KeyingPanel-ZyBSXioh.js +1 -0
  126. package/ui/dist/assets/NodeCanvas-BvvcCErf.js +7 -0
  127. package/ui/dist/assets/{PromptBuilderPanel-D97A4pwB.js → PromptBuilderPanel-DBycmShA.js} +2 -2
  128. package/ui/dist/assets/PromptImportDialog-ChvMv7ek.js +2 -0
  129. package/ui/dist/assets/PromptImportDiscoverySection-60py7whH.js +1 -0
  130. package/ui/dist/assets/PromptImportFolderSection-CCRh_-_7.js +1 -0
  131. package/ui/dist/assets/PromptLibraryPanel-BFWQO8OT.js +2 -0
  132. package/ui/dist/assets/SettingsWorkspace-B5Ghsgdj.js +1 -0
  133. package/ui/dist/assets/SpriteRecipeWorkspace-DRWdkmwU.js +1 -0
  134. package/ui/dist/assets/index-7ngEOVxt.js +5 -0
  135. package/ui/dist/assets/index-B5jy7ePg.css +1 -0
  136. package/ui/dist/assets/index-gxh_02DQ.js +22 -0
  137. package/ui/dist/assets/{pptxgen.es-D4pedbwM.js → pptxgen.es-OUNhQLEV.js} +1 -1
  138. package/ui/dist/assets/promptBuilderStore-BTUUzbBT.js +1 -0
  139. package/ui/dist/assets/useAgentDialogFocus-DSc2qY8p.js +1 -0
  140. package/ui/dist/index.html +3 -2
  141. package/ui/dist/assets/AssetGenWorkspace-B-TGlvVb.js +0 -2
  142. package/ui/dist/assets/AssetsWorkspace-LnGaW151.js +0 -1
  143. package/ui/dist/assets/GenerationRequestLogPanel-DuDWD_ic.js +0 -1
  144. package/ui/dist/assets/HomeWorkspace-DVP7UG6O.js +0 -1
  145. package/ui/dist/assets/KeyingPanel-5bRUFQaq.js +0 -1
  146. package/ui/dist/assets/NodeCanvas-BBEpERTb.js +0 -7
  147. package/ui/dist/assets/PromptImportDialog-C89MN5JY.js +0 -2
  148. package/ui/dist/assets/PromptImportDiscoverySection-BgRfDOs2.js +0 -1
  149. package/ui/dist/assets/PromptImportFolderSection-zTMyriT1.js +0 -1
  150. package/ui/dist/assets/PromptLibraryPanel-BD96qZu_.js +0 -2
  151. package/ui/dist/assets/SettingsWorkspace-BAOk3X_8.js +0 -1
  152. package/ui/dist/assets/SpriteRecipeWorkspace-ClIM5QRo.js +0 -1
  153. package/ui/dist/assets/index-0fpfY8vu.css +0 -1
  154. package/ui/dist/assets/index-BFciPGTn.js +0 -5
  155. package/ui/dist/assets/index-BbykeXR9.js +0 -30
  156. package/ui/dist/assets/promptBuilderStore-Do7M8g7J.js +0 -1
  157. package/ui/dist/assets/useAgentDialogFocus-B6-QgEVI.js +0 -1
package/.env.example CHANGED
@@ -5,6 +5,10 @@
5
5
  # ── Server ─────────────────────────────────────────────────────────────
6
6
  # IMA2_PORT=3333 # also accepted: PORT
7
7
  # IMA2_HOST=127.0.0.1
8
+ # Non-loopback access requires your own strong, private token (never commit it).
9
+ # IMA2_LAN_TOKEN=
10
+ # Exact serving origins for a TLS proxy, LAN DNS name or changed Docker port:
11
+ # IMA2_PUBLIC_ORIGINS=["https://studio.example"]
8
12
  # IMA2_BODY_LIMIT=50mb
9
13
 
10
14
  # ── OAuth proxy (openai-oauth subprocess) ──────────────────────────────
@@ -50,6 +54,12 @@
50
54
  # ── Logging ────────────────────────────────────────────────────────────
51
55
  # IMA2_LOG_LEVEL=info # trace|debug|info|warn|error|fatal
52
56
 
57
+ # Explicit doctor checks only; defaults in milliseconds, maximum 30000.
58
+ # --verify-keys checks remote authentication without generating images.
59
+ # IMA2_DIAGNOSTIC_KEY_TIMEOUT_MS=5000
60
+ # --runtime requires an explicit loopback server origin.
61
+ # IMA2_DIAGNOSTIC_RUNTIME_TIMEOUT_MS=1500
62
+
53
63
  # ── Secrets (not merged from config.json, env only) ────────────────────
54
64
  # OPENAI_API_KEY=sk-...
55
65
  # XAI_API_KEY=xai-...
package/CHANGELOG.md CHANGED
@@ -28,6 +28,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
28
28
 
29
29
  ## [Unreleased]
30
30
 
31
+ ### Production readiness
32
+
33
+ - Correct NovelAI positive/negative prompt pane sizing, scrolling and toolbar spacing across sidebar, bottom, home and mobile composers; retain drafts when changing providers.
34
+ - Keep provider/model labels, capabilities and selection consistent across the UI, CLI and generation routes, including unavailable or malformed provider catalogs.
35
+ - Preserve accepted-job tracking through disconnects, cancellation, restart and LAN reauthentication without automatically submitting the job again.
36
+ - Bound image/video artifact reads and downloads, reject escaped artifact paths, and keep cancellation from persisting an incomplete result.
37
+ - Add LAN sign-in with origin-bound, HttpOnly session cookies; protect generated media, SSE, Range and HEAD requests. Default loopback use remains token-free, and explicit header/query API credentials remain supported.
38
+ - Share failed-token cooldown across API, media, status and browser bootstrap; retain existing cookie sessions during a bearer-token cooldown.
39
+ - Bind CLI LAN credentials only to an explicitly selected server, reject redirects and credential-bearing URLs, and preserve provider authentication errors separately from LAN access failures.
40
+ - Add offline `doctor --installation --json`, safe diagnostic reports and metadata-derived installation/runtime documentation; improve Windows lock handling without terminating unrelated processes.
41
+ - Use a patched, development-only Node SDK downloader for source builds; keep runtime dependencies and install-script approvals unchanged.
42
+
43
+ LAN HTTP is intended for trusted networks and does not encrypt tokens or cookies; use an operator-configured HTTPS proxy or VPN on untrusted networks. Verification uses isolated synthetic providers, not a claim that live provider credentials, billing or every remote deployment have been exercised.
44
+
31
45
  ### Added
32
46
 
33
47
  - **Current model defaults** — Grok search, image/video planning, and frame analysis now default to `grok-4.5` while retaining `grok-4.3` as an explicit compatibility override. GPT image generation, stylesheet analysis, Card News planning, and Prompt Builder default to `gpt-5.6-luna`; Grok video model discovery projects `grok-imagine-video-1.5` from runtime config.
package/README.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # ima2-gen
2
2
 
3
+ <!-- runtime-install:generated:start -->
4
+ | Contract | Value |
5
+ |---|---|
6
+ | Node engine | `>=22` |
7
+ | npm toolchain | `npm@11.18.0` |
8
+ | Release Node | `24.17.0` |
9
+ | CLI entry | `bin/ima2.js` |
10
+ | OpenAI SDK | `^7.4.0` |
11
+ | Express | `^5.1.0` |
12
+ <!-- runtime-install:generated:end -->
13
+
14
+ The installer derives its Node.js floor from package metadata and runs an offline installation check before launching the server.
15
+
3
16
  <p align="center">
4
17
  <img src="assets/logo.png" alt="ima2-gen logo" width="240">
5
18
  </p>
@@ -75,7 +88,7 @@ irm https://lidge-jun.github.io/ima2-gen/install-windows.ps1 | iex
75
88
  curl -fsSL https://lidge-jun.github.io/ima2-gen/install-linux.sh | bash
76
89
  ```
77
90
 
78
- Each script checks for nvm/fnm/brew/winget, installs Node LTS through the best available method, and handles stale process cleanup automatically.
91
+ Each script checks the package-derived Node.js floor, installs Node LTS through the best available method, installs once, and runs the offline installation check before launching `ima2 serve`. It does not stop unrelated processes or clear global locks.
79
92
 
80
93
  ### Setup
81
94
 
@@ -97,7 +110,7 @@ Stop the running server with Ctrl+C, then:
97
110
  npm install -g ima2-gen@latest
98
111
  ```
99
112
 
100
- Ctrl+C now performs a clean shutdown — closing the database, stopping child processes, and releasing file locks. On older versions (< 1.1.22) or if you see `EBUSY` on Windows, use the install script which handles stale process cleanup automatically.
113
+ Ctrl+C now performs a clean shutdown — closing the database, stopping child processes, and releasing file locks. If an installation fails, inspect the reported npm permissions or stop the specific `ima2` process yourself; the installer does not perform broad process cleanup.
101
114
 
102
115
  ## What It Does
103
116
 
@@ -5,7 +5,7 @@ import { config } from "../../config.js";
5
5
  import { buildIma2Capabilities } from "../../lib/capabilities.js";
6
6
  import { parseArgs } from "../lib/args.js";
7
7
  import { resolveServer, request } from "../lib/client.js";
8
- import { color, dieWithError, json, out } from "../lib/output.js";
8
+ import { color, exitCodeForError, fail, json, out } from "../lib/output.js";
9
9
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
10
10
  const PACKAGE_PATH = join(ROOT, "package.json");
11
11
  const HELP = `
@@ -47,7 +47,8 @@ async function readCapabilities(args) {
47
47
  return await request(server.base, "/api/capabilities", { timeoutMs: 5000 });
48
48
  }
49
49
  catch (error) {
50
- if (args.server || args["require-server"])
50
+ if (args.server !== undefined || process.env.IMA2_SERVER !== undefined || args["require-server"]
51
+ || error?.code !== "SERVER_UNREACHABLE")
51
52
  throw error;
52
53
  return localCapabilities();
53
54
  }
@@ -131,6 +132,7 @@ export default async function capabilitiesCmd(argv) {
131
132
  printText(capabilities);
132
133
  }
133
134
  catch (error) {
134
- dieWithError(error);
135
+ fail({ json: Boolean(args.json), code: error?.code ?? "SERVER_REQUEST_FAILED",
136
+ message: error?.message || "server request failed", exitCode: exitCodeForError(error) });
135
137
  }
136
138
  }
@@ -2,6 +2,7 @@ import { createInterface } from "readline/promises";
2
2
  import { parseArgs } from "../lib/args.js";
3
3
  import { confirmDestructiveAction } from "../lib/destructive-confirm.js";
4
4
  import { out, die, color, json, exitFlushed } from "../lib/output.js";
5
+ import { parsePublicOrigins } from "../../lib/localAccessPolicy.js";
5
6
  import { CONFIG_FILE, KEY_TO_ENV, WRITABLE_CONFIG_KEYS, buildEffectiveConfig, deleteNestedKey, displayPath, envOverrideForKey, getNestedKey, isAuthConfigKey, isSensitiveConfigKey, isWritableConfigKey, loadFileCfg, parseConfigValue, redactValue, restartNotice, saveFileCfg, setNestedKey, } from "../lib/config-store.js";
6
7
  const HELP = `
7
8
  ima2 config <subcommand> [options]
@@ -33,7 +34,7 @@ async function pathSub(_argv) {
33
34
  async function lsSub(argv) {
34
35
  const args = parseArgs(argv, { flags: FLAGS });
35
36
  if (args.effective) {
36
- const eff = buildEffectiveConfig();
37
+ const eff = redactValue("", buildEffectiveConfig());
37
38
  if (args.json) {
38
39
  json(eff);
39
40
  return;
@@ -41,7 +42,7 @@ async function lsSub(argv) {
41
42
  out(JSON.stringify(eff, null, 2));
42
43
  }
43
44
  else {
44
- const fileCfg = loadFileCfg();
45
+ const fileCfg = redactValue("", loadFileCfg());
45
46
  if (args.json) {
46
47
  json(fileCfg);
47
48
  return;
@@ -93,11 +94,21 @@ async function setSub(argv) {
93
94
  if (!isWritableConfigKey(key)) {
94
95
  die(2, `unknown config key: "${key}". Run 'ima2 config ls --effective' to see the config structure.`);
95
96
  }
96
- const value = parseConfigValue(rawValue);
97
+ let value = parseConfigValue(rawValue);
98
+ if (key === "server.publicOrigins") {
99
+ try {
100
+ value = parsePublicOrigins(value);
101
+ }
102
+ catch {
103
+ die(2, "INVALID_PUBLIC_ORIGINS: expected an array of exact HTTP(S) origins without credentials, paths, query or fragments.");
104
+ }
105
+ }
97
106
  // Warn if env var is overriding this key
98
107
  const override = envOverrideForKey(key);
99
108
  if (override) {
100
- out(color.yellow(`warning: env ${override.envVar}=${override.value} is currently overriding this value.`));
109
+ out(color.yellow(key === "server.publicOrigins"
110
+ ? `warning: env ${override.envVar} is currently overriding this value.`
111
+ : `warning: env ${override.envVar}=${override.value} is currently overriding this value.`));
101
112
  out(`The file change will only apply after unsetting the env var and restarting the server.`);
102
113
  }
103
114
  // Confirm if writing a sensitive key
@@ -2,7 +2,7 @@ import { config } from "../../config.js";
2
2
  import { parseArgs } from "../lib/args.js";
3
3
  import { resolveServer, request } from "../lib/client.js";
4
4
  import { buildEffectiveConfig, deleteNestedKey, displayPath, envOverrideForKey, getNestedKey, loadCliDefaults, loadFileCfg, restartNotice, saveFileCfg, setNestedKey, CONFIG_FILE, } from "../lib/config-store.js";
5
- import { color, die, dieWithError, fail, json, out } from "../lib/output.js";
5
+ import { color, die, exitCodeForError, fail, json, out } from "../lib/output.js";
6
6
  const MODEL_KEYS = ["imageModels.default", "apiProvider.defaultImageModel"];
7
7
  const REASONING_KEYS = ["imageModels.reasoningEffort", "apiProvider.defaultReasoningEffort"];
8
8
  const HELP = `
@@ -67,7 +67,8 @@ async function readDefaults(args) {
67
67
  };
68
68
  }
69
69
  catch (error) {
70
- if (args.server)
70
+ if (args.server !== undefined || process.env.IMA2_SERVER !== undefined
71
+ || error?.code !== "SERVER_UNREACHABLE")
71
72
  throw error;
72
73
  return localDefaults();
73
74
  }
@@ -143,8 +144,8 @@ async function fetchModelCatalog(args) {
143
144
  }
144
145
  catch (error) {
145
146
  fail({
146
- json: Boolean(args.json), code: "SERVER_UNREACHABLE",
147
- message: error?.message || "server unreachable", exitCode: 3,
147
+ json: Boolean(args.json), code: error?.code ?? "SERVER_REQUEST_FAILED",
148
+ message: error?.message || "server request failed", exitCode: exitCodeForError(error),
148
149
  });
149
150
  }
150
151
  }
@@ -197,7 +198,8 @@ async function listSub(argv) {
197
198
  printDefaults(payload);
198
199
  }
199
200
  catch (error) {
200
- dieWithError(error);
201
+ fail({ json: Boolean(args.json), code: error?.code ?? "SERVER_REQUEST_FAILED",
202
+ message: error?.message || "server request failed", exitCode: exitCodeForError(error) });
201
203
  }
202
204
  }
203
205
  async function setSub(argv) {
@@ -1,25 +1,26 @@
1
- import { createRequire } from "module";
2
1
  import { existsSync, readFileSync } from "fs";
3
2
  import { dirname, join } from "path";
4
3
  import { fileURLToPath } from "url";
5
4
  import { buildHardeningDoctorLines } from "../lib/doctor-checks.js";
6
5
  import { buildStorageDoctorLines } from "../lib/storage-doctor.js";
7
6
  import { detectCodexAuth } from "../../lib/codexDetect.js";
8
- import { resolvePackageBin } from "../../lib/packageCli.js";
9
7
  import { runImageDoctorProbe } from "../../lib/responsesDoctor.js";
10
8
  import { config as runtimeConfig } from "../../config.js";
11
9
  import { exitFlushed } from "../lib/output.js";
12
10
  import { buildProviderDoctorLines, verifyConfiguredKeys } from "../lib/doctor-providers.js";
13
11
  import { buildMediaDoctorLines } from "../lib/doctor-media.js";
14
12
  import { buildDoctorBundle } from "../lib/doctor-bundle.js";
13
+ import { buildDoctorReport, renderDoctorReport } from "../lib/doctor-report.js";
14
+ import { buildInstallationDoctorLines, checkNodeEngine, missingRuntimeDeps, probeDoctorRuntime } from "../lib/doctor-runtime.js";
15
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
16
  const ROOT = join(__dirname, "../..");
17
- const requireFromRoot = createRequire(join(ROOT, "package.json"));
18
17
  const CONFIG_FILE = runtimeConfig.storage.configFile;
19
18
  const LEGACY_CONFIG_FILE = join(ROOT, ".ima2", "config.json");
20
19
  let pkg = { version: "?", name: "ima2-gen" };
21
20
  try {
22
- pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
21
+ const metadata = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
22
+ if (metadata && typeof metadata.version === "string" && typeof metadata.name === "string")
23
+ pkg = metadata;
23
24
  }
24
25
  catch { }
25
26
  function loadConfig() {
@@ -34,32 +35,6 @@ function loadConfig() {
34
35
  }
35
36
  return {};
36
37
  }
37
- function missingRuntimeDeps() {
38
- const deps = ["express", "better-sqlite3", "openai", "openai-oauth", "progrok/package.json", "@openai/codex/package.json", "zod"];
39
- const missing = deps.filter((dep) => {
40
- try {
41
- requireFromRoot.resolve(dep);
42
- return false;
43
- }
44
- catch {
45
- return true;
46
- }
47
- }).map((dep) => dep.endsWith("/package.json") ? dep.slice(0, -"/package.json".length) : dep);
48
- for (const pair of [["openai-oauth", "openai-oauth"], ["@openai/codex", "codex"]]) {
49
- const packageName = pair[0];
50
- const binName = pair[1];
51
- if (!packageName || !binName)
52
- continue;
53
- try {
54
- resolvePackageBin(packageName, binName);
55
- }
56
- catch {
57
- if (!missing.includes(packageName))
58
- missing.push(packageName);
59
- }
60
- }
61
- return missing;
62
- }
63
38
  function valueAfter(args, name) {
64
39
  const index = args.indexOf(name);
65
40
  if (index === -1)
@@ -92,6 +67,13 @@ export function showDoctorHelp() {
92
67
  Usage: ima2 doctor [image-probe] [options]
93
68
 
94
69
  Diagnose environment, storage, dependencies, and authentication.
70
+ --json Emit one machine-readable diagnostic report
71
+ --bundle Emit a safe compatibility bundle (--json for JSON only)
72
+ --installation Offline installation checks; no account/config reads
73
+ --verify-keys Explicit non-generating remote authentication checks
74
+ --runtime <origin> Explicit loopback health/version check; no credentials
75
+ Installation mode cannot combine with bundle, verify-keys or runtime.
76
+ image-probe performs live billed generation; ordinary doctor never runs it.
95
77
  Run 'ima2 doctor image-probe --help' for live image probe options.
96
78
  `);
97
79
  }
@@ -136,16 +118,12 @@ async function standardDoctor(args = []) {
136
118
  console.log(`\n ${pkg.name} v${pkg.version} — Doctor\n`);
137
119
  let ok = 0;
138
120
  let fail = 0;
139
- const nodeVersion = process.version;
140
- const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0] ?? "0", 10);
141
- if (nodeMajor >= 20) {
142
- console.log(` ✓ Node.js ${nodeVersion} (>= 20)`);
121
+ const node = checkNodeEngine(process.version, pkg.engines?.node);
122
+ console.log(` ${node.kind === "pass" ? "✓" : "✗"} ${node.text}`);
123
+ if (node.kind === "pass")
143
124
  ok++;
144
- }
145
- else {
146
- console.log(` ✗ Node.js ${nodeVersion} (requires >= 20)`);
125
+ else
147
126
  fail++;
148
- }
149
127
  if (existsSync(join(ROOT, "package.json"))) {
150
128
  console.log(" ✓ package.json found");
151
129
  ok++;
@@ -154,7 +132,7 @@ async function standardDoctor(args = []) {
154
132
  console.log(" ✗ package.json missing");
155
133
  fail++;
156
134
  }
157
- const missingDeps = missingRuntimeDeps();
135
+ const missingDeps = missingRuntimeDeps(ROOT);
158
136
  if (missingDeps.length === 0) {
159
137
  console.log(" ✓ runtime dependencies resolvable");
160
138
  ok++;
@@ -276,5 +254,76 @@ export async function doctor(args = []) {
276
254
  await imageProbe(args.slice(1));
277
255
  return;
278
256
  }
257
+ if (!validDoctorArguments(args)) {
258
+ console.error("Invalid doctor options; use ima2 doctor --help.");
259
+ exitFlushed(2);
260
+ }
261
+ if (args.includes("--json") || args.includes("--bundle") || args.includes("--runtime") || args.includes("--installation")) {
262
+ await machineDoctor(args);
263
+ return;
264
+ }
279
265
  await standardDoctor(args);
280
266
  }
267
+ function validDoctorArguments(args) {
268
+ const seen = new Set();
269
+ for (let i = 0; i < args.length; i++) {
270
+ const arg = args[i];
271
+ if (seen.has(arg) || !["--json", "--bundle", "--installation", "--verify-keys", "--runtime"].includes(arg))
272
+ return false;
273
+ seen.add(arg);
274
+ if (arg === "--runtime" && (!args[++i] || args[i].startsWith("--")))
275
+ return false;
276
+ }
277
+ return !seen.has("--installation") || !["--bundle", "--verify-keys", "--runtime"].some((flag) => seen.has(flag));
278
+ }
279
+ function diagnosticConfig(lines) {
280
+ try {
281
+ const path = existsSync(CONFIG_FILE) ? CONFIG_FILE : existsSync(LEGACY_CONFIG_FILE) ? LEGACY_CONFIG_FILE : null;
282
+ const value = path ? JSON.parse(readFileSync(path, "utf8")) : {};
283
+ if (!value || typeof value !== "object" || Array.isArray(value))
284
+ throw Error("CONFIG_INVALID");
285
+ return value;
286
+ }
287
+ catch {
288
+ lines.push({ code: "CONFIG_INVALID", kind: "fail", text: "Configuration is not a JSON object" });
289
+ return {};
290
+ }
291
+ }
292
+ async function machineDoctor(args) {
293
+ const installation = args.includes("--installation"), lines = buildInstallationDoctorLines(ROOT);
294
+ let providerLines = [];
295
+ try {
296
+ if (!installation) {
297
+ const fileConfig = diagnosticConfig(lines);
298
+ lines.push({ code: fileConfig.provider ? "CONFIG_PRESENT" : "CONFIG_MISSING", kind: fileConfig.provider ? "info" : "warn", text: "Configuration selection" });
299
+ try {
300
+ if (existsSync(runtimeConfig.storage.advertiseFile)) {
301
+ const value = JSON.parse(readFileSync(runtimeConfig.storage.advertiseFile, "utf8"));
302
+ if (!value || typeof value !== "object" || Array.isArray(value))
303
+ throw Error("invalid");
304
+ }
305
+ }
306
+ catch {
307
+ lines.push({ code: "ADVERTISEMENT_INVALID", kind: "warn", text: "Invalid server advertisement" });
308
+ }
309
+ lines.push(...await buildHardeningDoctorLines({ root: ROOT, configFile: CONFIG_FILE, fileConfig, includeInstallationChecks: false }));
310
+ providerLines = buildProviderDoctorLines(fileConfig);
311
+ lines.push(...providerLines);
312
+ if (args.includes("--verify-keys"))
313
+ lines.push(...await verifyConfiguredKeys(fileConfig));
314
+ lines.push(...await buildMediaDoctorLines());
315
+ const url = valueAfter(args, "--runtime");
316
+ if (url)
317
+ lines.push(...await probeDoctorRuntime({ url, expectedVersion: pkg.version, timeoutMs: runtimeConfig.diagnostics.runtimeTimeoutMs }));
318
+ }
319
+ }
320
+ catch {
321
+ lines.push({ code: "DIAGNOSTIC_UNKNOWN", kind: "fail", text: "A diagnostic could not complete" });
322
+ }
323
+ const report = buildDoctorReport({ version: pkg.version, mode: installation ? "installation" : "standard", lines });
324
+ if (args.includes("--json"))
325
+ console.log(JSON.stringify(args.includes("--bundle") ? buildDoctorBundle({ version: pkg.version, providerLines, report }) : report));
326
+ else
327
+ console.log(renderDoctorReport(report));
328
+ exitFlushed(report.summary.exitCode);
329
+ }
@@ -6,13 +6,13 @@ import { sizeDrifted } from "../../lib/sizeNudge.js";
6
6
  import { errInfo } from "../../lib/errInfo.js";
7
7
  import { parseArgs } from "../lib/args.js";
8
8
  import { wasFlagPassed } from "../lib/argsExplicit.js";
9
- import { resolveServer, request, normalizeGenerate } from "../lib/client.js";
9
+ import { resolveServer, request, normalizeGenerate, fetchServer } from "../lib/client.js";
10
10
  import { fileToDataUri, dataUriToFile, defaultOutName, readStdin } from "../lib/files.js";
11
11
  import { loadCliDefaults } from "../lib/config-store.js";
12
12
  import { runMcpJob } from "../lib/mcpJob.js";
13
13
  import { characterElementIdForMcp } from "../lib/characterResolve.js";
14
14
  import { resolveTarget } from "../lib/modelResolver.js";
15
- import { out, die, dieWithError, color, err, fail, json } from "../lib/output.js";
15
+ import { out, die, dieWithError, color, err, exitCodeForError, fail, json } from "../lib/output.js";
16
16
  import { createCliRequestId, recoverGeneratedOutputs, formatRecoveryHint } from "../lib/recover-output.js";
17
17
  import { deriveProviderIds } from "../../lib/providers/derive.js";
18
18
  import { listProviders } from "../../lib/mcp/providerRegistry.js";
@@ -140,7 +140,10 @@ function savedLine(path, actual, requested) {
140
140
  return [head, color.dim(` ! requested ${String(requested)}; the provider returned a different size`)];
141
141
  }
142
142
  function failServer(jsonMode, error) {
143
+ const code = error?.code ?? "SERVER_REQUEST_FAILED";
143
144
  const message = error?.message || "server unreachable";
145
+ if (code !== "SERVER_UNREACHABLE")
146
+ fail({ json: jsonMode, code, message, exitCode: exitCodeForError(error) });
144
147
  if (jsonMode)
145
148
  err("Hint: start the server with `ima2 serve`.");
146
149
  fail({ json: jsonMode, code: "SERVER_UNREACHABLE", message: `${message}\nHint: run ima2 serve`, exitCode: 3 });
@@ -231,7 +234,7 @@ function validateMcpRefs(refs, context, roles, jsonMode) {
231
234
  return parsed;
232
235
  }
233
236
  async function downloadMcpResult(serverBase, url, target) {
234
- const response = await fetch(`${serverBase}${url}`);
237
+ const response = await fetchServer(serverBase, url);
235
238
  if (!response.ok)
236
239
  die(1, `failed to download image: HTTP ${response.status}`);
237
240
  await mkdir(dirname(target), { recursive: true });
@@ -267,7 +270,8 @@ async function runMcpImage(argv, args, context) {
267
270
  }
268
271
  catch (error) {
269
272
  const typed = error;
270
- fail({ json: Boolean(args.json), code: typed.code ?? "MCP_GENERATION_FAILED", message: typed.message, exitCode: 1 });
273
+ fail({ json: Boolean(args.json), code: typed.code ?? "MCP_GENERATION_FAILED", message: typed.message,
274
+ exitCode: exitCodeForError(error) === 4 ? 4 : 1 });
271
275
  }
272
276
  }
273
277
  function validateCoreFlags(args) {
@@ -384,11 +388,24 @@ async function runCoreImage(args, context) {
384
388
  }
385
389
  }
386
390
  export default async function genCmd(argv) {
391
+ try {
392
+ await generate(argv);
393
+ }
394
+ catch (error) {
395
+ if (!(error instanceof Error))
396
+ throw error; // Preserve the output owner's exit marker.
397
+ fail({ json: argv.includes("--json"), code: error.code ?? "GENERATION_FAILED",
398
+ message: error.message, exitCode: exitCodeForError(error) });
399
+ }
400
+ }
401
+ async function generate(argv) {
387
402
  const args = parseArgs(argv, SPEC);
388
403
  if (args.help) {
389
404
  out(HELP);
390
405
  return;
391
406
  }
407
+ if (args._unknown.length)
408
+ die(2, `unknown option: ${args._unknown[0]}`);
392
409
  const naiPreflight = unwrapNaiCliResult(parseNaiCliOptions(args, "allow-unknown"), Boolean(args.json));
393
410
  let prompt = args.positional.join(" ");
394
411
  if (!prompt && !args.stdin)
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "../lib/args.js";
2
2
  import { request, resolveServer } from "../lib/client.js";
3
- import { fail, json, out, table } from "../lib/output.js";
3
+ import { exitCodeForError, fail, json, out, table } from "../lib/output.js";
4
4
  const SPEC = {
5
5
  flags: {
6
6
  kind: { type: "string" },
@@ -61,7 +61,7 @@ async function fetchCatalog(serverFlag, isJson) {
61
61
  }
62
62
  catch (error) {
63
63
  const message = error?.message || "server unreachable";
64
- fail({ json: isJson, code: "SERVER_UNREACHABLE", message, exitCode: 3 });
64
+ fail({ json: isJson, code: error?.code ?? "SERVER_REQUEST_FAILED", message, exitCode: exitCodeForError(error) });
65
65
  }
66
66
  }
67
67
  export default async function modelsCmd(argv) {
@@ -28,7 +28,7 @@ export default async function pingCmd(argv) {
28
28
  catch (e) {
29
29
  const err = errInfo(e);
30
30
  if (args.json) {
31
- json({ ok: false, error: err.message });
31
+ json({ ok: false, error: err.message, code: err.code });
32
32
  exitFlushed(exitCodeForError(e));
33
33
  }
34
34
  die(exitCodeForError(e), err.message);
@@ -356,9 +356,11 @@ async function importSources(argv) {
356
356
  const args = parseArgs(argv, { flags: COMMON_FLAGS });
357
357
  const server = await getServer(args);
358
358
  const [curated, discovery] = await Promise.all([
359
- request(server.base, "/api/prompts/import/curated-sources").catch(() => ({})),
360
- request(server.base, "/api/prompts/import/discovery").catch(() => ({})),
361
- ]);
359
+ request(server.base, "/api/prompts/import/curated-sources").catch((error) => { if (exitCodeForError(error) === 4)
360
+ throw error; return {}; }),
361
+ request(server.base, "/api/prompts/import/discovery").catch((error) => { if (exitCodeForError(error) === 4)
362
+ throw error; return {}; }),
363
+ ]).catch(handle);
362
364
  if (args.json) {
363
365
  json({ curated, discovery });
364
366
  return;
@@ -3,6 +3,8 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { clearServerBinding, fetchServerUrl, resolveServer } from "../lib/client.js";
7
+ import { dieWithError } from "../lib/output.js";
6
8
  import { LAUNCHD_LABEL, SYSTEMD_UNIT, launchctlOutputIndicatesFailure, renderLaunchdPlist, renderSystemdUnit, serviceStateStale, } from "../lib/serviceTemplates.js";
7
9
  import { corroborateByStartTime, escalateKill, gracefulStop, isProcessAlive, verifyServerIdentity, waitForExit, } from "../../lib/processControl.js";
8
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -82,19 +84,35 @@ function readAdvertise() {
82
84
  }
83
85
  }
84
86
  async function waitForHealth(timeoutMs) {
87
+ clearServerBinding();
88
+ const explicitTarget = process.env.IMA2_SERVER;
89
+ let lastSelectionError;
85
90
  const deadline = Date.now() + timeoutMs;
86
91
  while (Date.now() < deadline) {
87
92
  const entry = readAdvertise();
88
- if (entry?.url) {
89
- try {
90
- const r = await fetch(`${String(entry.url).replace(/\/$/, "")}/api/health`, { headers: { connection: "close" } });
93
+ try {
94
+ if (explicitTarget !== undefined) {
95
+ const selected = await resolveServer({ serverFlag: explicitTarget });
96
+ // Only a matching advertisement can describe the selected service.
97
+ return { ok: true, entry: entry?.url?.replace(/\/$/, "") === selected.base ? entry : { url: selected.base } };
98
+ }
99
+ if (entry?.url) {
100
+ const r = await fetchServerUrl(`${String(entry.url).replace(/\/$/, "")}/api/health`, { headers: { connection: "close" } });
91
101
  if (r.ok)
92
102
  return { ok: true, entry };
93
103
  }
94
- catch { /* keep polling */ }
104
+ }
105
+ catch (error) {
106
+ const code = error?.code;
107
+ if (code !== "NETWORK_FAILED" && code !== "SERVER_UNREACHABLE")
108
+ throw error;
109
+ if (explicitTarget !== undefined)
110
+ lastSelectionError = error;
95
111
  }
96
112
  await new Promise((r) => setTimeout(r, 500));
97
113
  }
114
+ if (lastSelectionError)
115
+ throw lastSelectionError;
98
116
  return { ok: false, entry: readAdvertise() };
99
117
  }
100
118
  function reportProviderLiveness(entry) {
@@ -415,6 +433,16 @@ const HELP = `
415
433
  repair Re-render artifacts after node/npm path moves
416
434
  `;
417
435
  export async function service(args = []) {
436
+ try {
437
+ await dispatchService(args);
438
+ }
439
+ catch (error) {
440
+ if (!(error instanceof Error))
441
+ throw error;
442
+ dieWithError(error);
443
+ }
444
+ }
445
+ async function dispatchService(args) {
418
446
  const sub = args[0];
419
447
  switch (sub) {
420
448
  case "install":
@@ -11,7 +11,7 @@ import { buildToolShow, buildToolsList, catalogVersion, errorEnvelope, execution
11
11
  import { loadAllBundledSnapshots, readLocalSnapshot } from "../../lib/mcp/snapshotStore.js";
12
12
  import { parseArgs } from "../lib/args.js";
13
13
  import { resolveServer, request } from "../lib/client.js";
14
- import { json, out } from "../lib/output.js";
14
+ import { json, out, exitCodeForError } from "../lib/output.js";
15
15
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
16
16
  const HELP = `
17
17
  ima2 tools <list|show|schema|call> [id] [--json] [--offline] [--server <url>] [--input '<json>']
@@ -65,7 +65,7 @@ async function fromServer(args, path) {
65
65
  return await request(server.base, path, { timeoutMs: 8000 });
66
66
  }
67
67
  catch (error) {
68
- if (error.code === "SERVER_UNREACHABLE" && !args.server)
68
+ if (error.code === "SERVER_UNREACHABLE" && !args.server && !process.env.IMA2_SERVER)
69
69
  return null; // offline fallback
70
70
  throw error;
71
71
  }
@@ -77,7 +77,7 @@ function emit(payload, asJson) {
77
77
  }
78
78
  json(payload); // machine surface: JSON is the primary output either way
79
79
  }
80
- export default async function toolsCommand(argv) {
80
+ async function runToolsCommand(argv) {
81
81
  const args = parseArgs(argv, { flags: FLAGS });
82
82
  if (args.help) {
83
83
  out(HELP);
@@ -124,6 +124,22 @@ export default async function toolsCommand(argv) {
124
124
  out(HELP);
125
125
  process.exitCode = sub ? 1 : 0;
126
126
  }
127
+ export default async function toolsCommand(argv) {
128
+ try {
129
+ await runToolsCommand(argv);
130
+ }
131
+ catch (error) {
132
+ if (!(error instanceof Error))
133
+ throw error;
134
+ const code = error.code;
135
+ const exit = exitCodeForError(error);
136
+ if (exit !== 4 && code !== "SERVER_UNREACHABLE")
137
+ throw error;
138
+ const envelope = errorEnvelope("server_error", error.message, { catalogVersion: "unavailable", cliVersion: cliVersion() });
139
+ json({ ...envelope, error: { ...envelope.error, code: code ?? "SERVER_ACCESS_DENIED" } });
140
+ process.exitCode = exit;
141
+ }
142
+ }
127
143
  async function callTool(args, id, asJson) {
128
144
  const entries = localEntries();
129
145
  const meta = localMeta(entries);