ima2-gen 1.1.21 → 1.1.23

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 (214) hide show
  1. package/README.md +44 -7
  2. package/bin/commands/video.js +14 -0
  3. package/bin/ima2.js +14 -4
  4. package/bin/lib/platform.js +34 -5
  5. package/docs/README.ko.md +43 -2
  6. package/lib/agentQueueWorker.js +6 -0
  7. package/lib/agentRuntime.js +3 -2
  8. package/lib/atomicWrite.js +14 -0
  9. package/lib/grokImageAdapter.js +6 -0
  10. package/lib/grokProxyLauncher.js +5 -3
  11. package/lib/grokVideoAdapter.js +1 -1
  12. package/lib/grokVideoPlannerPrompt.js +10 -0
  13. package/lib/inflight.js +1 -1
  14. package/lib/oauthLauncher.js +5 -0
  15. package/lib/videoFrameExtract.js +3 -3
  16. package/package.json +5 -7
  17. package/routes/capabilities.js +13 -0
  18. package/routes/edit.js +2 -1
  19. package/routes/generate.js +32 -6
  20. package/routes/health.js +4 -3
  21. package/routes/multimode.js +2 -1
  22. package/routes/video.js +35 -3
  23. package/server.js +29 -2
  24. package/skills/ima2/SKILL.md +48 -6
  25. package/ui/dist/.vite/manifest.json +12 -12
  26. package/ui/dist/assets/{AgentWorkspace-B_hq9CLg.js → AgentWorkspace-C21zqdTZ.js} +1 -1
  27. package/ui/dist/assets/{CardNewsWorkspace-wD12J7qk.js → CardNewsWorkspace-BN-ga1lG.js} +1 -1
  28. package/ui/dist/assets/{NodeCanvas-CI_wuPMf.js → NodeCanvas-BbMa4IhI.js} +1 -1
  29. package/ui/dist/assets/{PromptBuilderPanel-CUTujJUV.js → PromptBuilderPanel-DRwBJRDQ.js} +1 -1
  30. package/ui/dist/assets/{PromptImportDialog-CUi66jPK.js → PromptImportDialog-Dp85kHCq.js} +2 -2
  31. package/ui/dist/assets/{PromptImportDiscoverySection-Cm3vrjY4.js → PromptImportDiscoverySection-BE8Q8MLD.js} +1 -1
  32. package/ui/dist/assets/{PromptImportFolderSection-DOtWTD9n.js → PromptImportFolderSection-PtH5x0sc.js} +1 -1
  33. package/ui/dist/assets/{PromptLibraryPanel-BMjQegRa.js → PromptLibraryPanel-FnM9tHI9.js} +2 -2
  34. package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +1 -0
  35. package/ui/dist/assets/index-BAFI6htx.js +42 -0
  36. package/ui/dist/assets/{index-31uVIdt4.js → index-BSXxr_Bt.js} +1 -1
  37. package/ui/dist/assets/index-DS-ADE7U.css +1 -0
  38. package/ui/dist/index.html +2 -2
  39. package/bin/commands/annotate.ts +0 -119
  40. package/bin/commands/cancel.ts +0 -48
  41. package/bin/commands/canvas-versions.ts +0 -80
  42. package/bin/commands/capabilities.ts +0 -110
  43. package/bin/commands/cardnews.ts +0 -249
  44. package/bin/commands/comfy.ts +0 -54
  45. package/bin/commands/config.ts +0 -186
  46. package/bin/commands/defaults.ts +0 -192
  47. package/bin/commands/doctor.ts +0 -202
  48. package/bin/commands/edit.ts +0 -150
  49. package/bin/commands/gen.ts +0 -214
  50. package/bin/commands/grok.ts +0 -90
  51. package/bin/commands/history.ts +0 -146
  52. package/bin/commands/ls.ts +0 -64
  53. package/bin/commands/metadata.ts +0 -39
  54. package/bin/commands/multimode.ts +0 -196
  55. package/bin/commands/node.ts +0 -166
  56. package/bin/commands/observability.ts +0 -176
  57. package/bin/commands/ping.ts +0 -31
  58. package/bin/commands/prompt-sub/build.ts +0 -101
  59. package/bin/commands/prompt.ts +0 -492
  60. package/bin/commands/ps.ts +0 -81
  61. package/bin/commands/session.ts +0 -266
  62. package/bin/commands/show.ts +0 -72
  63. package/bin/commands/skill.ts +0 -70
  64. package/bin/commands/video.ts +0 -442
  65. package/bin/ima2.ts +0 -430
  66. package/bin/lib/args.ts +0 -92
  67. package/bin/lib/browser-id.ts +0 -16
  68. package/bin/lib/client.ts +0 -122
  69. package/bin/lib/config-store.ts +0 -120
  70. package/bin/lib/destructive-confirm.ts +0 -19
  71. package/bin/lib/doctor-checks.ts +0 -91
  72. package/bin/lib/error-hints.ts +0 -23
  73. package/bin/lib/files.ts +0 -39
  74. package/bin/lib/output.ts +0 -73
  75. package/bin/lib/platform.ts +0 -99
  76. package/bin/lib/recover-output.ts +0 -139
  77. package/bin/lib/sse.ts +0 -73
  78. package/bin/lib/star-prompt.ts +0 -97
  79. package/bin/lib/storage-doctor.ts +0 -39
  80. package/bin/lib/ui-build.ts +0 -85
  81. package/config.ts +0 -354
  82. package/lib/agentCommandParser.ts +0 -69
  83. package/lib/agentGenerationPlanner.ts +0 -273
  84. package/lib/agentQuestionResponder.ts +0 -266
  85. package/lib/agentQueueStore.ts +0 -270
  86. package/lib/agentQueueWorker.ts +0 -89
  87. package/lib/agentRuntime.ts +0 -604
  88. package/lib/agentSettings.ts +0 -72
  89. package/lib/agentStore.ts +0 -422
  90. package/lib/agentStoreRows.ts +0 -136
  91. package/lib/agentTypes.ts +0 -154
  92. package/lib/apiCachePolicy.ts +0 -11
  93. package/lib/assetLifecycle.ts +0 -146
  94. package/lib/canvasVersionStore.ts +0 -223
  95. package/lib/capabilities.ts +0 -126
  96. package/lib/cardNewsGenerator.ts +0 -271
  97. package/lib/cardNewsJobStore.ts +0 -142
  98. package/lib/cardNewsManifestStore.ts +0 -154
  99. package/lib/cardNewsPlanner.ts +0 -236
  100. package/lib/cardNewsPlannerClient.ts +0 -155
  101. package/lib/cardNewsPlannerPrompt.ts +0 -62
  102. package/lib/cardNewsPlannerSchema.ts +0 -321
  103. package/lib/cardNewsRoleTemplateStore.ts +0 -47
  104. package/lib/cardNewsTemplateStore.ts +0 -252
  105. package/lib/codexDetect.ts +0 -71
  106. package/lib/comfyBridge.ts +0 -235
  107. package/lib/composerSnapshot.ts +0 -33
  108. package/lib/configKeys.ts +0 -62
  109. package/lib/db.ts +0 -295
  110. package/lib/errInfo.ts +0 -43
  111. package/lib/errorClassify.ts +0 -100
  112. package/lib/generationCancel.ts +0 -28
  113. package/lib/generationErrors.ts +0 -238
  114. package/lib/grokImageAdapter.ts +0 -513
  115. package/lib/grokMultimodeAdapter.ts +0 -84
  116. package/lib/grokProxyLauncher.ts +0 -153
  117. package/lib/grokRuntime.ts +0 -23
  118. package/lib/grokSizeMapper.ts +0 -71
  119. package/lib/grokVideoAdapter.ts +0 -458
  120. package/lib/grokVideoCanvas.ts +0 -26
  121. package/lib/grokVideoDownload.ts +0 -59
  122. package/lib/grokVideoPlannerPrompt.ts +0 -67
  123. package/lib/historyIndex.ts +0 -51
  124. package/lib/historyList.ts +0 -181
  125. package/lib/imageMetadata.ts +0 -113
  126. package/lib/imageMetadataStore.ts +0 -67
  127. package/lib/imageModels.ts +0 -165
  128. package/lib/inflight.ts +0 -281
  129. package/lib/localImportStore.ts +0 -114
  130. package/lib/logger.ts +0 -161
  131. package/lib/nodeStore.ts +0 -91
  132. package/lib/oauthLauncher.ts +0 -94
  133. package/lib/oauthNormalize.ts +0 -30
  134. package/lib/oauthProxy/errors.ts +0 -128
  135. package/lib/oauthProxy/generators.ts +0 -494
  136. package/lib/oauthProxy/index.ts +0 -28
  137. package/lib/oauthProxy/prompts.ts +0 -123
  138. package/lib/oauthProxy/references.ts +0 -45
  139. package/lib/oauthProxy/runtime.ts +0 -115
  140. package/lib/oauthProxy/streams.ts +0 -232
  141. package/lib/oauthProxy/types.ts +0 -9
  142. package/lib/oauthProxy.ts +0 -3
  143. package/lib/openDirectory.ts +0 -47
  144. package/lib/pngInfo.ts +0 -26
  145. package/lib/promptBuilder/attachments.ts +0 -74
  146. package/lib/promptBuilder/client.ts +0 -130
  147. package/lib/promptBuilder/constants.ts +0 -9
  148. package/lib/promptBuilder/context.ts +0 -36
  149. package/lib/promptBuilder/errors.ts +0 -12
  150. package/lib/promptBuilder/requestSchema.ts +0 -56
  151. package/lib/promptBuilder/responseParser.ts +0 -219
  152. package/lib/promptBuilder/systemPrompt.ts +0 -135
  153. package/lib/promptBuilder/transport.ts +0 -94
  154. package/lib/promptBuilder/types.ts +0 -109
  155. package/lib/promptImport/curatedSources.ts +0 -141
  156. package/lib/promptImport/discoveryRegistry.ts +0 -329
  157. package/lib/promptImport/errors.ts +0 -18
  158. package/lib/promptImport/githubDiscovery.ts +0 -309
  159. package/lib/promptImport/githubFolder.ts +0 -397
  160. package/lib/promptImport/githubSource.ts +0 -257
  161. package/lib/promptImport/gptImageHints.ts +0 -70
  162. package/lib/promptImport/parsePromptCandidates.ts +0 -179
  163. package/lib/promptImport/promptIndex.ts +0 -326
  164. package/lib/promptImport/rankPromptCandidates.ts +0 -65
  165. package/lib/promptImport/types.ts +0 -103
  166. package/lib/promptSafetyPolicy.ts +0 -5
  167. package/lib/providerOptions.ts +0 -56
  168. package/lib/referenceImageCompress.ts +0 -84
  169. package/lib/refs.ts +0 -133
  170. package/lib/requestLogger.ts +0 -49
  171. package/lib/responsesDoctor.ts +0 -456
  172. package/lib/responsesErrors.ts +0 -83
  173. package/lib/responsesFallback.ts +0 -114
  174. package/lib/responsesImageAdapter.ts +0 -466
  175. package/lib/responsesParse.ts +0 -452
  176. package/lib/responsesTools.ts +0 -28
  177. package/lib/runtimeContext.ts +0 -146
  178. package/lib/runtimePorts.ts +0 -105
  179. package/lib/sessionStore.ts +0 -308
  180. package/lib/storageMigration.ts +0 -310
  181. package/lib/styleSheet.ts +0 -139
  182. package/lib/systemTrash.ts +0 -20
  183. package/lib/videoContinuity.ts +0 -180
  184. package/lib/videoFrameExtract.ts +0 -78
  185. package/lib/videoSeriesChain.ts +0 -29
  186. package/lib/visibleTextLanguagePolicy.ts +0 -7
  187. package/routes/agent.ts +0 -308
  188. package/routes/annotations.ts +0 -118
  189. package/routes/canvasVersions.ts +0 -69
  190. package/routes/capabilities.ts +0 -18
  191. package/routes/cardNews.ts +0 -211
  192. package/routes/comfy.ts +0 -43
  193. package/routes/edit.ts +0 -352
  194. package/routes/generate.ts +0 -492
  195. package/routes/grok.ts +0 -24
  196. package/routes/health.ts +0 -123
  197. package/routes/history.ts +0 -221
  198. package/routes/imageImport.ts +0 -37
  199. package/routes/index.ts +0 -52
  200. package/routes/metadata.ts +0 -77
  201. package/routes/multimode.ts +0 -499
  202. package/routes/nodes.ts +0 -578
  203. package/routes/promptBuilder.ts +0 -37
  204. package/routes/promptImport.ts +0 -379
  205. package/routes/prompts.ts +0 -428
  206. package/routes/quota.ts +0 -89
  207. package/routes/sessions.ts +0 -317
  208. package/routes/storage.ts +0 -47
  209. package/routes/video.ts +0 -300
  210. package/routes/videoExtended.ts +0 -284
  211. package/server.ts +0 -293
  212. package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +0 -1
  213. package/ui/dist/assets/index-CjgnNtgt.css +0 -1
  214. package/ui/dist/assets/index-Da2s4_-5.js +0 -36
@@ -1,120 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { config as runtimeConfig } from "../../config.js";
3
- import {
4
- AUTH_CONFIG_KEYS,
5
- KEY_TO_ENV,
6
- WRITABLE_CONFIG_KEYS,
7
- isSensitiveConfigKey as isSensitiveConfigKeyShared,
8
- } from "../../lib/configKeys.js";
9
-
10
- export { KEY_TO_ENV, WRITABLE_CONFIG_KEYS };
11
-
12
- export const CONFIG_FILE = runtimeConfig.storage.configFile;
13
- export const CONFIG_DIR = runtimeConfig.storage.configDir;
14
-
15
- export const AUTH_KEYS = AUTH_CONFIG_KEYS;
16
-
17
- export function isAuthConfigKey(key: string): boolean {
18
- return AUTH_CONFIG_KEYS.has(key);
19
- }
20
-
21
- export function isWritableConfigKey(key: string): boolean {
22
- return WRITABLE_CONFIG_KEYS.has(key);
23
- }
24
-
25
- export function isSensitiveConfigKey(key: string): boolean {
26
- return isSensitiveConfigKeyShared(key);
27
- }
28
-
29
- export function redactValue(key: string, value: unknown): unknown {
30
- if (isSensitiveConfigKey(key)) return value ? "<redacted>" : value;
31
- return value;
32
- }
33
-
34
- export function loadFileCfg(): Record<string, unknown> {
35
- if (!existsSync(CONFIG_FILE)) return {};
36
- try {
37
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as Record<string, unknown>;
38
- } catch {
39
- return {};
40
- }
41
- }
42
-
43
- export function saveFileCfg(cfg: Record<string, unknown>): void {
44
- mkdirSync(CONFIG_DIR, { recursive: true });
45
- writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
46
- }
47
-
48
- export function getNestedKey(obj: unknown, dotKey: string): unknown {
49
- const parts = dotKey.split(".");
50
- let cur: unknown = obj;
51
- for (const p of parts) {
52
- if (cur == null || typeof cur !== "object") return undefined;
53
- cur = (cur as Record<string, unknown>)[p];
54
- }
55
- return cur;
56
- }
57
-
58
- export function setNestedKey(obj: Record<string, unknown>, dotKey: string, value: unknown): void {
59
- const parts = dotKey.split(".");
60
- let cur: Record<string, unknown> = obj;
61
- for (let i = 0; i < parts.length - 1; i++) {
62
- const part = parts[i];
63
- const next = cur[part];
64
- if (next == null || typeof next !== "object" || Array.isArray(next)) cur[part] = {};
65
- cur = cur[part] as Record<string, unknown>;
66
- }
67
- cur[parts[parts.length - 1]] = value;
68
- }
69
-
70
- export function deleteNestedKey(obj: Record<string, unknown>, dotKey: string): boolean {
71
- const parts = dotKey.split(".");
72
- let cur: unknown = obj;
73
- for (let i = 0; i < parts.length - 1; i++) {
74
- if (cur == null || typeof cur !== "object") return false;
75
- cur = (cur as Record<string, unknown>)[parts[i]];
76
- }
77
- if (cur == null || typeof cur !== "object") return false;
78
- const last = parts[parts.length - 1];
79
- if (!(last in cur)) return false;
80
- delete (cur as Record<string, unknown>)[last];
81
- return true;
82
- }
83
-
84
- export function stripSets(value: unknown): unknown {
85
- if (value instanceof Set) return [...value].map(stripSets);
86
- if (Array.isArray(value)) return value.map(stripSets);
87
- if (value && typeof value === "object") {
88
- const result: Record<string, unknown> = {};
89
- for (const [key, nested] of Object.entries(value)) result[key] = stripSets(nested);
90
- return result;
91
- }
92
- return value;
93
- }
94
-
95
- export function buildEffectiveConfig(): Record<string, unknown> {
96
- return stripSets(runtimeConfig) as Record<string, unknown>;
97
- }
98
-
99
- export function parseConfigValue(rawValue: string): unknown {
100
- try {
101
- return JSON.parse(rawValue);
102
- } catch {
103
- return rawValue;
104
- }
105
- }
106
-
107
- export function envOverrideForKey(key: string): { envVar: string; value: string } | null {
108
- const envVar = KEY_TO_ENV[key];
109
- if (!envVar || process.env[envVar] === undefined) return null;
110
- return { envVar, value: String(process.env[envVar]) };
111
- }
112
-
113
- export function displayPath(p: string): string {
114
- const home = process.env.HOME || "";
115
- return home && p.startsWith(home) ? p.replace(home, "~") : p;
116
- }
117
-
118
- export function restartNotice(): string {
119
- return "note: server must be restarted to pick up config changes (run `ima2 serve`)";
120
- }
@@ -1,19 +0,0 @@
1
- import { createInterface } from "readline/promises";
2
-
3
- export async function confirmDestructiveAction(
4
- message: string,
5
- yes: boolean,
6
- ): Promise<boolean> {
7
- if (yes) return true;
8
- if (!process.stdin.isTTY) {
9
- throw new Error("destructive action requires --yes in non-interactive mode");
10
- }
11
-
12
- const rl = createInterface({ input: process.stdin, output: process.stdout });
13
- try {
14
- const ans = await rl.question(`${message} [y/N] `);
15
- return ans.trim().toLowerCase().startsWith("y");
16
- } finally {
17
- rl.close();
18
- }
19
- }
@@ -1,91 +0,0 @@
1
- import { createRequire } from "module";
2
- import { createServer } from "net";
3
- import { existsSync, statSync } from "fs";
4
- import { join } from "path";
5
- import { config as runtimeConfig } from "../../config.js";
6
- import { isSensitiveConfigKey } from "../../lib/configKeys.js";
7
-
8
- export type DoctorCheckLine = {
9
- kind: "pass" | "fail" | "warn" | "info";
10
- text: string;
11
- };
12
-
13
- function hasSensitiveValue(value: unknown, path = ""): boolean {
14
- if (!value || typeof value !== "object") return false;
15
- for (const [key, nested] of Object.entries(value)) {
16
- const nextPath = path ? `${path}.${key}` : key;
17
- if (isSensitiveConfigKey(nextPath) && nested) return true;
18
- if (hasSensitiveValue(nested, nextPath)) return true;
19
- }
20
- return false;
21
- }
22
-
23
- async function probePort(host: string, port: number): Promise<boolean> {
24
- return new Promise((resolve) => {
25
- const server = createServer();
26
- server.once("error", () => resolve(false));
27
- server.once("listening", () => {
28
- server.close(() => resolve(true));
29
- });
30
- server.listen(port, host);
31
- });
32
- }
33
-
34
- function probeBetterSqlite(root: string): DoctorCheckLine {
35
- try {
36
- const requireFromRoot = createRequire(join(root, "package.json"));
37
- const mod = requireFromRoot("better-sqlite3") as { default?: unknown };
38
- const Database = (mod.default ?? mod) as new (path: string) => { close: () => void };
39
- const db = new Database(":memory:");
40
- db.close();
41
- return { kind: "pass", text: "better-sqlite3 native binding loads" };
42
- } catch (err) {
43
- const message = err instanceof Error ? err.message : String(err);
44
- return { kind: "fail", text: `better-sqlite3 native binding failed: ${message}` };
45
- }
46
- }
47
-
48
- function configPermissionLine(configFile: string, fileConfig: unknown): DoctorCheckLine | null {
49
- if (process.platform === "win32" || !existsSync(configFile) || !hasSensitiveValue(fileConfig)) {
50
- return null;
51
- }
52
- const mode = statSync(configFile).mode;
53
- if ((mode & 0o077) === 0) return null;
54
- return {
55
- kind: "warn",
56
- text: `${configFile} is readable by group/other; consider chmod 600`,
57
- };
58
- }
59
-
60
- export async function buildHardeningDoctorLines({
61
- root,
62
- configFile,
63
- fileConfig,
64
- }: {
65
- root: string;
66
- configFile: string;
67
- fileConfig: unknown;
68
- }): Promise<DoctorCheckLine[]> {
69
- const lines: DoctorCheckLine[] = [];
70
- const portAvailable = await probePort(runtimeConfig.server.host, runtimeConfig.server.port);
71
- lines.push({
72
- kind: "info",
73
- text: `Preferred backend port ${runtimeConfig.server.port}: ${portAvailable ? "available" : "in use"}`,
74
- });
75
- lines.push({
76
- kind: "info",
77
- text: `Card News: ${runtimeConfig.features.cardNews ? "enabled" : "disabled"}`,
78
- });
79
-
80
- const skillPath = join(root, "skills", "ima2", "SKILL.md");
81
- lines.push(
82
- existsSync(skillPath)
83
- ? { kind: "pass", text: `packaged skill found: ${skillPath}` }
84
- : { kind: "fail", text: `packaged skill missing: ${skillPath}` },
85
- );
86
- lines.push(probeBetterSqlite(root));
87
-
88
- const perm = configPermissionLine(configFile, fileConfig);
89
- if (perm) lines.push(perm);
90
- return lines;
91
- }
@@ -1,23 +0,0 @@
1
- const HINTS: Record<string, string> = {
2
- SERVER_UNREACHABLE: "Start `ima2 serve`, or pass `--server <url>`.",
3
- APIKEY_DISABLED: "API-key generation is supported in current builds; switch providers or update the configured API key.",
4
- IMAGE_MODEL_UNSUPPORTED:
5
- "This model is visible but cannot generate images here. Use gpt-5.4 or gpt-5.4-mini.",
6
- INVALID_IMAGE_MODEL: "Use one of: gpt-5.5, gpt-5.4, gpt-5.4-mini.",
7
- OAUTH_UNAVAILABLE: "GPT OAuth proxy is unavailable. Check `ima2 doctor` and restart `ima2 serve`.",
8
- NETWORK_FAILED: "Network/proxy failed. This is not a moderation refusal.",
9
- SAFETY_REFUSAL: "The image backend refused this generation.",
10
- MODERATION_REFUSED: "The prompt or image was rejected by moderation.",
11
- AUTH_CHATGPT_EXPIRED: "Re-run `ima2 setup` (option 1), then restart `ima2 serve`.",
12
- REF_TOO_LARGE: "Reference image is too large. Resize/compress it and retry.",
13
- REF_NOT_BASE64: "Reference payload is invalid. Use a normal PNG/JPEG/WebP file.",
14
- };
15
-
16
- export function hintForErrorCode(code: string | null | undefined): string | null {
17
- return code ? HINTS[code] || null : null;
18
- }
19
-
20
- export function formatErrorWithHint(message: string, code: string | null | undefined): string {
21
- const hint = hintForErrorCode(code);
22
- return hint ? `${message}\nHint: ${hint}` : message;
23
- }
package/bin/lib/files.ts DELETED
@@ -1,39 +0,0 @@
1
- import { readFile, writeFile, mkdir } from "node:fs/promises";
2
- import { dirname, extname } from "node:path";
3
-
4
- const MIME: Record<string, string> = {
5
- png: "image/png",
6
- jpg: "image/jpeg",
7
- jpeg: "image/jpeg",
8
- webp: "image/webp",
9
- };
10
-
11
- export async function fileToDataUri(path: string): Promise<string> {
12
- const b64 = (await readFile(path)).toString("base64");
13
- const ext = extname(path).slice(1).toLowerCase();
14
- const mime = MIME[ext] || "image/png";
15
- return `data:${mime};base64,${b64}`;
16
- }
17
-
18
- export async function dataUriToFile(dataUri: string, outPath: string): Promise<string> {
19
- const m = dataUri.match(/^data:([^;]+);base64,(.+)$/);
20
- const raw = m ? m[2] : dataUri;
21
- await mkdir(dirname(outPath) || ".", { recursive: true });
22
- await writeFile(outPath, Buffer.from(raw, "base64"));
23
- return outPath;
24
- }
25
-
26
- export function defaultOutName(index: number, total: number, ext = "png"): string {
27
- const now = new Date();
28
- const pad = (n: number) => String(n).padStart(2, "0");
29
- const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
30
- if (total <= 1) return `ima2-${stamp}.${ext}`;
31
- return `ima2-${stamp}-${index}.${ext}`;
32
- }
33
-
34
- export async function readStdin() {
35
- if (process.stdin.isTTY) return "";
36
- const chunks: Buffer[] = [];
37
- for await (const c of process.stdin) chunks.push(c as Buffer);
38
- return Buffer.concat(chunks).toString("utf-8").trim();
39
- }
package/bin/lib/output.ts DELETED
@@ -1,73 +0,0 @@
1
- import { formatErrorWithHint } from "./error-hints.js";
2
-
3
- const isTty = process.stdout.isTTY && !process.env.NO_COLOR;
4
-
5
- export const color = {
6
- dim: (s: unknown) => (isTty ? `\x1b[2m${s}\x1b[0m` : String(s)),
7
- bold: (s: unknown) => (isTty ? `\x1b[1m${s}\x1b[0m` : String(s)),
8
- red: (s: unknown) => (isTty ? `\x1b[31m${s}\x1b[0m` : String(s)),
9
- green: (s: unknown) => (isTty ? `\x1b[32m${s}\x1b[0m` : String(s)),
10
- yellow: (s: unknown) => (isTty ? `\x1b[33m${s}\x1b[0m` : String(s)),
11
- cyan: (s: unknown) => (isTty ? `\x1b[36m${s}\x1b[0m` : String(s)),
12
- };
13
-
14
- export function out(msg = "") { process.stdout.write(msg + "\n"); }
15
- export function err(msg = "") { process.stderr.write(msg + "\n"); }
16
-
17
- export function die(code: number, msg?: string): never {
18
- if (msg) err(color.red("✗ ") + msg);
19
- process.exit(code);
20
- }
21
-
22
- export interface ErrorLike {
23
- message?: string;
24
- code?: string | null;
25
- status?: number;
26
- name?: string;
27
- }
28
-
29
- export function dieWithError(e: unknown): never {
30
- const err = e as ErrorLike;
31
- return die(exitCodeForError(e), formatErrorWithHint(err?.message || String(e), err?.code));
32
- }
33
-
34
- export function json(obj: unknown) {
35
- process.stdout.write(JSON.stringify(obj) + "\n");
36
- }
37
-
38
- export interface TableColumn<R = Record<string, unknown>> {
39
- key: string;
40
- label: string;
41
- format?: (value: unknown, row: R) => unknown;
42
- }
43
-
44
- export function table<R extends Record<string, unknown>>(rows: R[], columns: TableColumn<R>[]): void {
45
- if (rows.length === 0) return;
46
- const widths = columns.map((c) =>
47
- Math.max(c.label.length, ...rows.map((r) => {
48
- const v = c.format ? c.format(r[c.key], r) : r[c.key];
49
- return String(v ?? "").length;
50
- })),
51
- );
52
- const pad = (s: unknown, w: number) => String(s ?? "").padEnd(w);
53
- out(color.dim(columns.map((c, i) => pad(c.label, widths[i])).join(" ")));
54
- out(color.dim(widths.map((w) => "─".repeat(w)).join(" ")));
55
- for (const r of rows) {
56
- out(columns.map((c, i) => pad(c.format ? c.format(r[c.key], r) : r[c.key], widths[i])).join(" "));
57
- }
58
- }
59
-
60
- export function exitCodeForError(e: unknown): number {
61
- const err = e as ErrorLike;
62
- if (err?.code === "SERVER_UNREACHABLE") return 3;
63
- if (err?.code === "APIKEY_DISABLED") return 4;
64
- if (err?.code === "AUTH_CHATGPT_EXPIRED" || err?.code === "OAUTH_UNAVAILABLE") return 4;
65
- if (err?.code === "NETWORK_FAILED") return 6;
66
- if (err?.code === "REF_TOO_LARGE" || err?.code === "REF_NOT_BASE64") return 5;
67
- if (err?.code === "SAFETY_REFUSAL") return 7;
68
- if (err?.code === "MODERATION_REFUSED") return 7;
69
- if (err?.name === "TimeoutError" || /abort/i.test(err?.message || "")) return 8;
70
- if ((err?.status ?? 0) >= 500) return 6;
71
- if ((err?.status ?? 0) >= 400) return 5;
72
- return 1;
73
- }
@@ -1,99 +0,0 @@
1
- // Cross-platform helpers (Windows / macOS / Linux / WSL).
2
- // Keep this file tiny & dependency-free. Node 18+ only.
3
-
4
- import { spawn, execSync } from "node:child_process";
5
- import { readFileSync } from "node:fs";
6
-
7
- import { errInfo } from "../../lib/errInfo.js";
8
- export const isWin = process.platform === "win32";
9
- export const isMac = process.platform === "darwin";
10
- export const isLinux = !isWin && !isMac;
11
-
12
- let _wslCached: boolean | null = null;
13
- export function isWsl() {
14
- if (_wslCached !== null) return _wslCached;
15
- if (!isLinux) return (_wslCached = false);
16
- try {
17
- _wslCached = readFileSync("/proc/version", "utf-8").toLowerCase().includes("microsoft");
18
- } catch {
19
- _wslCached = false;
20
- }
21
- return _wslCached;
22
- }
23
-
24
- export function hasDesktopSession() {
25
- return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
26
- }
27
-
28
- /**
29
- * Resolve an executable name that differs between Windows and Unix.
30
- * On Windows, npm global shims are .cmd files; spawn() without shell:true
31
- * cannot resolve them and fails with ENOENT.
32
- */
33
- export function resolveBin(name: string) {
34
- return isWin ? `${name}.cmd` : name;
35
- }
36
-
37
- /**
38
- * spawn() wrapper that works for npm/npx/any PATH-resolved exe on Windows.
39
- */
40
- export function spawnBin(name: string, args: string[], opts: Parameters<typeof spawn>[2] = {}) {
41
- if (isWin) {
42
- // Node 24 on Windows can throw EINVAL when spawning PATH-resolved .cmd
43
- // shims directly with piped stdio. Routing through cmd.exe avoids that.
44
- return spawn("cmd.exe", ["/d", "/s", "/c", `${name} ${args.join(" ")}`], {
45
- windowsHide: true,
46
- ...opts,
47
- });
48
- }
49
- return spawn(resolveBin(name), args, { windowsHide: true, ...opts });
50
- }
51
-
52
- /**
53
- * Open a URL in the user's default browser.
54
- * Returns { ok: boolean, error?: string }.
55
- * Handles WSL (via powershell.exe) and refuses on headless Linux without DISPLAY.
56
- */
57
- export function openUrl(url: string): { ok: boolean; error?: string } {
58
- try {
59
- if (isMac) {
60
- execSync(`open ${JSON.stringify(url)}`, { stdio: "ignore" });
61
- } else if (isWin) {
62
- execSync(`cmd /c start "" ${JSON.stringify(url)}`, { stdio: "ignore" });
63
- } else if (isWsl()) {
64
- // WSL: hand off to Windows via powershell
65
- execSync(`powershell.exe -NoProfile -Command Start-Process ${JSON.stringify(url)}`, { stdio: "ignore" });
66
- } else {
67
- if (!hasDesktopSession()) {
68
- return { ok: false, error: "no desktop session (DISPLAY/WAYLAND_DISPLAY unset)" };
69
- }
70
- execSync(`xdg-open ${JSON.stringify(url)}`, { stdio: "ignore" });
71
- }
72
- return { ok: true };
73
- } catch (e) {
74
- const err = errInfo(e);
75
- return { ok: false, error: err.message || String(e) };
76
- }
77
- }
78
-
79
- /**
80
- * Register graceful shutdown handlers.
81
- * Windows does NOT raise SIGTERM from the OS — SIGINT (Ctrl+C) and SIGBREAK
82
- * (Ctrl+Break) are the observable signals. We still register SIGTERM so that
83
- * Node-internal `child.kill("SIGTERM")` calls work in tests.
84
- */
85
- export function onShutdown(handler: (signal: NodeJS.Signals) => void) {
86
- const signals: NodeJS.Signals[] = isWin
87
- ? ["SIGINT", "SIGTERM", "SIGBREAK"]
88
- : ["SIGINT", "SIGTERM", "SIGHUP"];
89
- for (const sig of signals) {
90
- try {
91
- process.on(sig, () => {
92
- try { handler(sig); } finally { process.exit(0); }
93
- });
94
- } catch {
95
- // Some signals aren't installable on certain platforms; ignore.
96
- }
97
- }
98
- }
99
-
@@ -1,139 +0,0 @@
1
- import { copyFile, mkdir } from "node:fs/promises";
2
- import { join, dirname, basename } from "node:path";
3
- import { config } from "../../config.js";
4
- import { request } from "./client.js";
5
-
6
- export type RecoverOutputTarget = {
7
- explicitOut?: string | null;
8
- outDir?: string | null;
9
- expectedCount?: number;
10
- json?: boolean;
11
- };
12
-
13
- export type RecoverOutputResult = {
14
- recovered: boolean;
15
- paths: string[];
16
- requestId: string;
17
- source: "terminal" | "history" | "active" | "none";
18
- message?: string;
19
- };
20
-
21
- export function createCliRequestId(prefix = "req_cli"): string {
22
- return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
23
- }
24
-
25
- function expectedRecoveryCount(target: RecoverOutputTarget): number {
26
- const count = Number.isFinite(target.expectedCount) ? Number(target.expectedCount) : 1;
27
- return Math.max(1, Math.min(500, count));
28
- }
29
-
30
- function filenamesFromMeta(meta: unknown): string[] {
31
- if (!meta || typeof meta !== "object") return [];
32
- const record = meta as Record<string, unknown>;
33
- if (Array.isArray(record.filenames)) {
34
- return record.filenames.filter((name): name is string => typeof name === "string" && name.length > 0);
35
- }
36
- if (typeof record.filename === "string" && record.filename.length > 0) return [record.filename];
37
- return [];
38
- }
39
-
40
- async function copyFilesToTarget(
41
- filenames: string[],
42
- target: RecoverOutputTarget,
43
- generatedDir: string,
44
- ): Promise<string[]> {
45
- const saved: string[] = [];
46
- for (let i = 0; i < filenames.length; i++) {
47
- const name = basename(filenames[i]);
48
- const src = join(generatedDir, name);
49
- let dest: string;
50
- if (target.explicitOut && i === 0) {
51
- dest = target.explicitOut;
52
- } else if (target.outDir) {
53
- dest = join(target.outDir, name);
54
- } else {
55
- saved.push(src);
56
- continue;
57
- }
58
- await mkdir(dirname(dest), { recursive: true });
59
- await copyFile(src, dest);
60
- saved.push(dest);
61
- }
62
- return saved;
63
- }
64
-
65
- async function tryTerminalRecovery(
66
- base: string,
67
- requestId: string,
68
- target: RecoverOutputTarget,
69
- ): Promise<RecoverOutputResult | null> {
70
- const inflight = await request(base, "/api/inflight?includeTerminal=1");
71
- const jobs = Array.isArray(inflight.jobs) ? (inflight.jobs as any[]) : [];
72
- const terminalJobs = Array.isArray(inflight.terminalJobs) ? (inflight.terminalJobs as any[]) : [];
73
-
74
- const activeMatch = jobs.find((j) => j.requestId === requestId);
75
- if (activeMatch) {
76
- return {
77
- recovered: false,
78
- paths: [],
79
- requestId,
80
- source: "active",
81
- message: `Generation in progress (requestId: ${requestId}). Check: ima2 ps --json`,
82
- };
83
- }
84
-
85
- const termMatch = terminalJobs.find((j) => j.requestId === requestId);
86
- if (!termMatch) return null;
87
-
88
- const filenames = filenamesFromMeta(termMatch.meta);
89
- if (filenames.length === 0) return null;
90
-
91
- const paths = await copyFilesToTarget(filenames, target, config.storage.generatedDir);
92
- return { recovered: paths.length > 0, paths, requestId, source: "terminal" };
93
- }
94
-
95
- async function tryHistoryRecovery(
96
- base: string,
97
- requestId: string,
98
- target: RecoverOutputTarget,
99
- ): Promise<RecoverOutputResult | null> {
100
- const limit = expectedRecoveryCount(target);
101
- const hist = await request(base, `/api/history?limit=${limit}&requestId=${encodeURIComponent(requestId)}`);
102
- const items = Array.isArray(hist.items) ? (hist.items as any[]) : [];
103
- const filenames = items
104
- .filter((it) => it.requestId === requestId && typeof it.filename === "string")
105
- .map((it) => String(it.filename))
106
- .slice(0, limit);
107
- if (filenames.length === 0) return null;
108
-
109
- const paths = await copyFilesToTarget(filenames, target, config.storage.generatedDir);
110
- return { recovered: paths.length > 0, paths, requestId, source: "history" };
111
- }
112
-
113
- export async function recoverGeneratedOutputs(
114
- base: string,
115
- requestId: string,
116
- target: RecoverOutputTarget,
117
- ): Promise<RecoverOutputResult> {
118
- try {
119
- const result = await tryTerminalRecovery(base, requestId, target);
120
- if (result) return result;
121
- } catch {}
122
-
123
- try {
124
- const result = await tryHistoryRecovery(base, requestId, target);
125
- if (result) return result;
126
- } catch {}
127
-
128
- return { recovered: false, paths: [], requestId, source: "none" };
129
- }
130
-
131
- export function formatRecoveryHint(result: RecoverOutputResult): string {
132
- if (result.source === "active") {
133
- return result.message ?? `Generation in progress (requestId: ${result.requestId})`;
134
- }
135
- if (result.recovered) {
136
- return `Recovered ${result.paths.length} file(s) via ${result.source} (requestId: ${result.requestId})`;
137
- }
138
- return `Could not recover output (requestId: ${result.requestId}). Check ${config.storage.generatedDir}/`;
139
- }
package/bin/lib/sse.ts DELETED
@@ -1,73 +0,0 @@
1
- // SSE consumer for CLI streaming endpoints. Plain fetch + line-based parser, no external libs.
2
-
3
- let CLI_VERSION = "0.0.0";
4
- export function setCliVersion(v: string) { CLI_VERSION = v; }
5
-
6
- export type SseEvent = { event: string; data: any };
7
-
8
- export interface SseInit {
9
- method?: string;
10
- body?: any;
11
- headers?: Record<string, string>;
12
- signal?: AbortSignal;
13
- }
14
-
15
- /**
16
- * Stream events from an SSE endpoint, yielding parsed events as `{ event, data }`.
17
- * - method defaults to "POST"
18
- * - JSON body is auto-stringified
19
- * - sets `Accept: text/event-stream` and `Content-Type: application/json` automatically
20
- * - parses chunk boundaries; partial events at EOF are dropped (not yielded)
21
- * - aborts cleanly on AbortSignal
22
- */
23
- export async function* streamSse(url: string, init: SseInit = {}): AsyncGenerator<SseEvent> {
24
- const headers: Record<string, string> = {
25
- Accept: "text/event-stream",
26
- "Content-Type": "application/json",
27
- "X-ima2-client": `cli/${CLI_VERSION}`,
28
- ...(init.headers || {}),
29
- };
30
- const res = await fetch(url, {
31
- method: init.method || "POST",
32
- headers,
33
- body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
34
- signal: init.signal,
35
- });
36
- if (!res.ok) {
37
- const text = await res.text().catch(() => "");
38
- let parsed: any = null;
39
- try { parsed = JSON.parse(text); } catch {}
40
- const err: any = new Error(parsed?.error || `SSE failed: HTTP ${res.status}`);
41
- err.status = res.status;
42
- err.code = parsed?.code || null;
43
- throw err;
44
- }
45
- if (!res.body) return;
46
-
47
- const decoder = new TextDecoder();
48
- let buf = "";
49
- for await (const chunk of res.body as any) {
50
- buf += decoder.decode(chunk, { stream: true });
51
- let idx;
52
- while ((idx = buf.indexOf("\n\n")) !== -1) {
53
- const frame = buf.slice(0, idx);
54
- buf = buf.slice(idx + 2);
55
- const ev = parseFrame(frame);
56
- if (ev) yield ev;
57
- }
58
- }
59
- }
60
-
61
- function parseFrame(frame: string): SseEvent | null {
62
- let event = "message";
63
- const dataLines: string[] = [];
64
- for (const line of frame.split(/\r?\n/)) {
65
- if (line.startsWith(":")) continue;
66
- if (line.startsWith("event:")) event = line.slice(6).trim();
67
- else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^\s/, ""));
68
- }
69
- if (dataLines.length === 0) return null;
70
- const raw = dataLines.join("\n");
71
- try { return { event, data: JSON.parse(raw) }; }
72
- catch { return { event, data: raw }; }
73
- }