ima2-gen 1.1.21 → 1.1.22

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 (208) hide show
  1. package/README.md +30 -4
  2. package/bin/ima2.js +14 -4
  3. package/bin/lib/platform.js +34 -5
  4. package/docs/README.ko.md +31 -0
  5. package/lib/agentQueueWorker.js +6 -0
  6. package/lib/agentRuntime.js +3 -2
  7. package/lib/atomicWrite.js +14 -0
  8. package/lib/grokProxyLauncher.js +5 -3
  9. package/lib/inflight.js +1 -1
  10. package/lib/oauthLauncher.js +5 -0
  11. package/lib/videoFrameExtract.js +3 -3
  12. package/package.json +5 -7
  13. package/routes/edit.js +2 -1
  14. package/routes/generate.js +4 -3
  15. package/routes/health.js +4 -3
  16. package/routes/multimode.js +2 -1
  17. package/routes/video.js +4 -2
  18. package/server.js +29 -2
  19. package/ui/dist/.vite/manifest.json +12 -12
  20. package/ui/dist/assets/{AgentWorkspace-B_hq9CLg.js → AgentWorkspace-COxQ5TjU.js} +1 -1
  21. package/ui/dist/assets/{CardNewsWorkspace-wD12J7qk.js → CardNewsWorkspace-B0OkcuVz.js} +1 -1
  22. package/ui/dist/assets/{NodeCanvas-CI_wuPMf.js → NodeCanvas-BSsclEBh.js} +1 -1
  23. package/ui/dist/assets/{PromptBuilderPanel-CUTujJUV.js → PromptBuilderPanel-DpC9A5Rz.js} +1 -1
  24. package/ui/dist/assets/{PromptImportDialog-CUi66jPK.js → PromptImportDialog-CVwT0rLd.js} +2 -2
  25. package/ui/dist/assets/{PromptImportDiscoverySection-Cm3vrjY4.js → PromptImportDiscoverySection-BDCkRCRs.js} +1 -1
  26. package/ui/dist/assets/{PromptImportFolderSection-DOtWTD9n.js → PromptImportFolderSection-QoKbZD83.js} +1 -1
  27. package/ui/dist/assets/{PromptLibraryPanel-BMjQegRa.js → PromptLibraryPanel-BhFgeKnY.js} +2 -2
  28. package/ui/dist/assets/SettingsWorkspace-CfjrlH5R.js +1 -0
  29. package/ui/dist/assets/index-C-mur7pa.css +1 -0
  30. package/ui/dist/assets/index-CCP5nUOj.js +42 -0
  31. package/ui/dist/assets/{index-31uVIdt4.js → index-Cxhzi3bs.js} +1 -1
  32. package/ui/dist/index.html +2 -2
  33. package/bin/commands/annotate.ts +0 -119
  34. package/bin/commands/cancel.ts +0 -48
  35. package/bin/commands/canvas-versions.ts +0 -80
  36. package/bin/commands/capabilities.ts +0 -110
  37. package/bin/commands/cardnews.ts +0 -249
  38. package/bin/commands/comfy.ts +0 -54
  39. package/bin/commands/config.ts +0 -186
  40. package/bin/commands/defaults.ts +0 -192
  41. package/bin/commands/doctor.ts +0 -202
  42. package/bin/commands/edit.ts +0 -150
  43. package/bin/commands/gen.ts +0 -214
  44. package/bin/commands/grok.ts +0 -90
  45. package/bin/commands/history.ts +0 -146
  46. package/bin/commands/ls.ts +0 -64
  47. package/bin/commands/metadata.ts +0 -39
  48. package/bin/commands/multimode.ts +0 -196
  49. package/bin/commands/node.ts +0 -166
  50. package/bin/commands/observability.ts +0 -176
  51. package/bin/commands/ping.ts +0 -31
  52. package/bin/commands/prompt-sub/build.ts +0 -101
  53. package/bin/commands/prompt.ts +0 -492
  54. package/bin/commands/ps.ts +0 -81
  55. package/bin/commands/session.ts +0 -266
  56. package/bin/commands/show.ts +0 -72
  57. package/bin/commands/skill.ts +0 -70
  58. package/bin/commands/video.ts +0 -442
  59. package/bin/ima2.ts +0 -430
  60. package/bin/lib/args.ts +0 -92
  61. package/bin/lib/browser-id.ts +0 -16
  62. package/bin/lib/client.ts +0 -122
  63. package/bin/lib/config-store.ts +0 -120
  64. package/bin/lib/destructive-confirm.ts +0 -19
  65. package/bin/lib/doctor-checks.ts +0 -91
  66. package/bin/lib/error-hints.ts +0 -23
  67. package/bin/lib/files.ts +0 -39
  68. package/bin/lib/output.ts +0 -73
  69. package/bin/lib/platform.ts +0 -99
  70. package/bin/lib/recover-output.ts +0 -139
  71. package/bin/lib/sse.ts +0 -73
  72. package/bin/lib/star-prompt.ts +0 -97
  73. package/bin/lib/storage-doctor.ts +0 -39
  74. package/bin/lib/ui-build.ts +0 -85
  75. package/config.ts +0 -354
  76. package/lib/agentCommandParser.ts +0 -69
  77. package/lib/agentGenerationPlanner.ts +0 -273
  78. package/lib/agentQuestionResponder.ts +0 -266
  79. package/lib/agentQueueStore.ts +0 -270
  80. package/lib/agentQueueWorker.ts +0 -89
  81. package/lib/agentRuntime.ts +0 -604
  82. package/lib/agentSettings.ts +0 -72
  83. package/lib/agentStore.ts +0 -422
  84. package/lib/agentStoreRows.ts +0 -136
  85. package/lib/agentTypes.ts +0 -154
  86. package/lib/apiCachePolicy.ts +0 -11
  87. package/lib/assetLifecycle.ts +0 -146
  88. package/lib/canvasVersionStore.ts +0 -223
  89. package/lib/capabilities.ts +0 -126
  90. package/lib/cardNewsGenerator.ts +0 -271
  91. package/lib/cardNewsJobStore.ts +0 -142
  92. package/lib/cardNewsManifestStore.ts +0 -154
  93. package/lib/cardNewsPlanner.ts +0 -236
  94. package/lib/cardNewsPlannerClient.ts +0 -155
  95. package/lib/cardNewsPlannerPrompt.ts +0 -62
  96. package/lib/cardNewsPlannerSchema.ts +0 -321
  97. package/lib/cardNewsRoleTemplateStore.ts +0 -47
  98. package/lib/cardNewsTemplateStore.ts +0 -252
  99. package/lib/codexDetect.ts +0 -71
  100. package/lib/comfyBridge.ts +0 -235
  101. package/lib/composerSnapshot.ts +0 -33
  102. package/lib/configKeys.ts +0 -62
  103. package/lib/db.ts +0 -295
  104. package/lib/errInfo.ts +0 -43
  105. package/lib/errorClassify.ts +0 -100
  106. package/lib/generationCancel.ts +0 -28
  107. package/lib/generationErrors.ts +0 -238
  108. package/lib/grokImageAdapter.ts +0 -513
  109. package/lib/grokMultimodeAdapter.ts +0 -84
  110. package/lib/grokProxyLauncher.ts +0 -153
  111. package/lib/grokRuntime.ts +0 -23
  112. package/lib/grokSizeMapper.ts +0 -71
  113. package/lib/grokVideoAdapter.ts +0 -458
  114. package/lib/grokVideoCanvas.ts +0 -26
  115. package/lib/grokVideoDownload.ts +0 -59
  116. package/lib/grokVideoPlannerPrompt.ts +0 -67
  117. package/lib/historyIndex.ts +0 -51
  118. package/lib/historyList.ts +0 -181
  119. package/lib/imageMetadata.ts +0 -113
  120. package/lib/imageMetadataStore.ts +0 -67
  121. package/lib/imageModels.ts +0 -165
  122. package/lib/inflight.ts +0 -281
  123. package/lib/localImportStore.ts +0 -114
  124. package/lib/logger.ts +0 -161
  125. package/lib/nodeStore.ts +0 -91
  126. package/lib/oauthLauncher.ts +0 -94
  127. package/lib/oauthNormalize.ts +0 -30
  128. package/lib/oauthProxy/errors.ts +0 -128
  129. package/lib/oauthProxy/generators.ts +0 -494
  130. package/lib/oauthProxy/index.ts +0 -28
  131. package/lib/oauthProxy/prompts.ts +0 -123
  132. package/lib/oauthProxy/references.ts +0 -45
  133. package/lib/oauthProxy/runtime.ts +0 -115
  134. package/lib/oauthProxy/streams.ts +0 -232
  135. package/lib/oauthProxy/types.ts +0 -9
  136. package/lib/oauthProxy.ts +0 -3
  137. package/lib/openDirectory.ts +0 -47
  138. package/lib/pngInfo.ts +0 -26
  139. package/lib/promptBuilder/attachments.ts +0 -74
  140. package/lib/promptBuilder/client.ts +0 -130
  141. package/lib/promptBuilder/constants.ts +0 -9
  142. package/lib/promptBuilder/context.ts +0 -36
  143. package/lib/promptBuilder/errors.ts +0 -12
  144. package/lib/promptBuilder/requestSchema.ts +0 -56
  145. package/lib/promptBuilder/responseParser.ts +0 -219
  146. package/lib/promptBuilder/systemPrompt.ts +0 -135
  147. package/lib/promptBuilder/transport.ts +0 -94
  148. package/lib/promptBuilder/types.ts +0 -109
  149. package/lib/promptImport/curatedSources.ts +0 -141
  150. package/lib/promptImport/discoveryRegistry.ts +0 -329
  151. package/lib/promptImport/errors.ts +0 -18
  152. package/lib/promptImport/githubDiscovery.ts +0 -309
  153. package/lib/promptImport/githubFolder.ts +0 -397
  154. package/lib/promptImport/githubSource.ts +0 -257
  155. package/lib/promptImport/gptImageHints.ts +0 -70
  156. package/lib/promptImport/parsePromptCandidates.ts +0 -179
  157. package/lib/promptImport/promptIndex.ts +0 -326
  158. package/lib/promptImport/rankPromptCandidates.ts +0 -65
  159. package/lib/promptImport/types.ts +0 -103
  160. package/lib/promptSafetyPolicy.ts +0 -5
  161. package/lib/providerOptions.ts +0 -56
  162. package/lib/referenceImageCompress.ts +0 -84
  163. package/lib/refs.ts +0 -133
  164. package/lib/requestLogger.ts +0 -49
  165. package/lib/responsesDoctor.ts +0 -456
  166. package/lib/responsesErrors.ts +0 -83
  167. package/lib/responsesFallback.ts +0 -114
  168. package/lib/responsesImageAdapter.ts +0 -466
  169. package/lib/responsesParse.ts +0 -452
  170. package/lib/responsesTools.ts +0 -28
  171. package/lib/runtimeContext.ts +0 -146
  172. package/lib/runtimePorts.ts +0 -105
  173. package/lib/sessionStore.ts +0 -308
  174. package/lib/storageMigration.ts +0 -310
  175. package/lib/styleSheet.ts +0 -139
  176. package/lib/systemTrash.ts +0 -20
  177. package/lib/videoContinuity.ts +0 -180
  178. package/lib/videoFrameExtract.ts +0 -78
  179. package/lib/videoSeriesChain.ts +0 -29
  180. package/lib/visibleTextLanguagePolicy.ts +0 -7
  181. package/routes/agent.ts +0 -308
  182. package/routes/annotations.ts +0 -118
  183. package/routes/canvasVersions.ts +0 -69
  184. package/routes/capabilities.ts +0 -18
  185. package/routes/cardNews.ts +0 -211
  186. package/routes/comfy.ts +0 -43
  187. package/routes/edit.ts +0 -352
  188. package/routes/generate.ts +0 -492
  189. package/routes/grok.ts +0 -24
  190. package/routes/health.ts +0 -123
  191. package/routes/history.ts +0 -221
  192. package/routes/imageImport.ts +0 -37
  193. package/routes/index.ts +0 -52
  194. package/routes/metadata.ts +0 -77
  195. package/routes/multimode.ts +0 -499
  196. package/routes/nodes.ts +0 -578
  197. package/routes/promptBuilder.ts +0 -37
  198. package/routes/promptImport.ts +0 -379
  199. package/routes/prompts.ts +0 -428
  200. package/routes/quota.ts +0 -89
  201. package/routes/sessions.ts +0 -317
  202. package/routes/storage.ts +0 -47
  203. package/routes/video.ts +0 -300
  204. package/routes/videoExtended.ts +0 -284
  205. package/server.ts +0 -293
  206. package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +0 -1
  207. package/ui/dist/assets/index-CjgnNtgt.css +0 -1
  208. 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
- }