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.
- package/README.md +44 -7
- package/bin/commands/video.js +14 -0
- package/bin/ima2.js +14 -4
- package/bin/lib/platform.js +34 -5
- package/docs/README.ko.md +43 -2
- package/lib/agentQueueWorker.js +6 -0
- package/lib/agentRuntime.js +3 -2
- package/lib/atomicWrite.js +14 -0
- package/lib/grokImageAdapter.js +6 -0
- package/lib/grokProxyLauncher.js +5 -3
- package/lib/grokVideoAdapter.js +1 -1
- package/lib/grokVideoPlannerPrompt.js +10 -0
- package/lib/inflight.js +1 -1
- package/lib/oauthLauncher.js +5 -0
- package/lib/videoFrameExtract.js +3 -3
- package/package.json +5 -7
- package/routes/capabilities.js +13 -0
- package/routes/edit.js +2 -1
- package/routes/generate.js +32 -6
- package/routes/health.js +4 -3
- package/routes/multimode.js +2 -1
- package/routes/video.js +35 -3
- package/server.js +29 -2
- package/skills/ima2/SKILL.md +48 -6
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-B_hq9CLg.js → AgentWorkspace-C21zqdTZ.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-wD12J7qk.js → CardNewsWorkspace-BN-ga1lG.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-CI_wuPMf.js → NodeCanvas-BbMa4IhI.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-CUTujJUV.js → PromptBuilderPanel-DRwBJRDQ.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-CUi66jPK.js → PromptImportDialog-Dp85kHCq.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-Cm3vrjY4.js → PromptImportDiscoverySection-BE8Q8MLD.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-DOtWTD9n.js → PromptImportFolderSection-PtH5x0sc.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-BMjQegRa.js → PromptLibraryPanel-FnM9tHI9.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +1 -0
- package/ui/dist/assets/index-BAFI6htx.js +42 -0
- package/ui/dist/assets/{index-31uVIdt4.js → index-BSXxr_Bt.js} +1 -1
- package/ui/dist/assets/index-DS-ADE7U.css +1 -0
- package/ui/dist/index.html +2 -2
- package/bin/commands/annotate.ts +0 -119
- package/bin/commands/cancel.ts +0 -48
- package/bin/commands/canvas-versions.ts +0 -80
- package/bin/commands/capabilities.ts +0 -110
- package/bin/commands/cardnews.ts +0 -249
- package/bin/commands/comfy.ts +0 -54
- package/bin/commands/config.ts +0 -186
- package/bin/commands/defaults.ts +0 -192
- package/bin/commands/doctor.ts +0 -202
- package/bin/commands/edit.ts +0 -150
- package/bin/commands/gen.ts +0 -214
- package/bin/commands/grok.ts +0 -90
- package/bin/commands/history.ts +0 -146
- package/bin/commands/ls.ts +0 -64
- package/bin/commands/metadata.ts +0 -39
- package/bin/commands/multimode.ts +0 -196
- package/bin/commands/node.ts +0 -166
- package/bin/commands/observability.ts +0 -176
- package/bin/commands/ping.ts +0 -31
- package/bin/commands/prompt-sub/build.ts +0 -101
- package/bin/commands/prompt.ts +0 -492
- package/bin/commands/ps.ts +0 -81
- package/bin/commands/session.ts +0 -266
- package/bin/commands/show.ts +0 -72
- package/bin/commands/skill.ts +0 -70
- package/bin/commands/video.ts +0 -442
- package/bin/ima2.ts +0 -430
- package/bin/lib/args.ts +0 -92
- package/bin/lib/browser-id.ts +0 -16
- package/bin/lib/client.ts +0 -122
- package/bin/lib/config-store.ts +0 -120
- package/bin/lib/destructive-confirm.ts +0 -19
- package/bin/lib/doctor-checks.ts +0 -91
- package/bin/lib/error-hints.ts +0 -23
- package/bin/lib/files.ts +0 -39
- package/bin/lib/output.ts +0 -73
- package/bin/lib/platform.ts +0 -99
- package/bin/lib/recover-output.ts +0 -139
- package/bin/lib/sse.ts +0 -73
- package/bin/lib/star-prompt.ts +0 -97
- package/bin/lib/storage-doctor.ts +0 -39
- package/bin/lib/ui-build.ts +0 -85
- package/config.ts +0 -354
- package/lib/agentCommandParser.ts +0 -69
- package/lib/agentGenerationPlanner.ts +0 -273
- package/lib/agentQuestionResponder.ts +0 -266
- package/lib/agentQueueStore.ts +0 -270
- package/lib/agentQueueWorker.ts +0 -89
- package/lib/agentRuntime.ts +0 -604
- package/lib/agentSettings.ts +0 -72
- package/lib/agentStore.ts +0 -422
- package/lib/agentStoreRows.ts +0 -136
- package/lib/agentTypes.ts +0 -154
- package/lib/apiCachePolicy.ts +0 -11
- package/lib/assetLifecycle.ts +0 -146
- package/lib/canvasVersionStore.ts +0 -223
- package/lib/capabilities.ts +0 -126
- package/lib/cardNewsGenerator.ts +0 -271
- package/lib/cardNewsJobStore.ts +0 -142
- package/lib/cardNewsManifestStore.ts +0 -154
- package/lib/cardNewsPlanner.ts +0 -236
- package/lib/cardNewsPlannerClient.ts +0 -155
- package/lib/cardNewsPlannerPrompt.ts +0 -62
- package/lib/cardNewsPlannerSchema.ts +0 -321
- package/lib/cardNewsRoleTemplateStore.ts +0 -47
- package/lib/cardNewsTemplateStore.ts +0 -252
- package/lib/codexDetect.ts +0 -71
- package/lib/comfyBridge.ts +0 -235
- package/lib/composerSnapshot.ts +0 -33
- package/lib/configKeys.ts +0 -62
- package/lib/db.ts +0 -295
- package/lib/errInfo.ts +0 -43
- package/lib/errorClassify.ts +0 -100
- package/lib/generationCancel.ts +0 -28
- package/lib/generationErrors.ts +0 -238
- package/lib/grokImageAdapter.ts +0 -513
- package/lib/grokMultimodeAdapter.ts +0 -84
- package/lib/grokProxyLauncher.ts +0 -153
- package/lib/grokRuntime.ts +0 -23
- package/lib/grokSizeMapper.ts +0 -71
- package/lib/grokVideoAdapter.ts +0 -458
- package/lib/grokVideoCanvas.ts +0 -26
- package/lib/grokVideoDownload.ts +0 -59
- package/lib/grokVideoPlannerPrompt.ts +0 -67
- package/lib/historyIndex.ts +0 -51
- package/lib/historyList.ts +0 -181
- package/lib/imageMetadata.ts +0 -113
- package/lib/imageMetadataStore.ts +0 -67
- package/lib/imageModels.ts +0 -165
- package/lib/inflight.ts +0 -281
- package/lib/localImportStore.ts +0 -114
- package/lib/logger.ts +0 -161
- package/lib/nodeStore.ts +0 -91
- package/lib/oauthLauncher.ts +0 -94
- package/lib/oauthNormalize.ts +0 -30
- package/lib/oauthProxy/errors.ts +0 -128
- package/lib/oauthProxy/generators.ts +0 -494
- package/lib/oauthProxy/index.ts +0 -28
- package/lib/oauthProxy/prompts.ts +0 -123
- package/lib/oauthProxy/references.ts +0 -45
- package/lib/oauthProxy/runtime.ts +0 -115
- package/lib/oauthProxy/streams.ts +0 -232
- package/lib/oauthProxy/types.ts +0 -9
- package/lib/oauthProxy.ts +0 -3
- package/lib/openDirectory.ts +0 -47
- package/lib/pngInfo.ts +0 -26
- package/lib/promptBuilder/attachments.ts +0 -74
- package/lib/promptBuilder/client.ts +0 -130
- package/lib/promptBuilder/constants.ts +0 -9
- package/lib/promptBuilder/context.ts +0 -36
- package/lib/promptBuilder/errors.ts +0 -12
- package/lib/promptBuilder/requestSchema.ts +0 -56
- package/lib/promptBuilder/responseParser.ts +0 -219
- package/lib/promptBuilder/systemPrompt.ts +0 -135
- package/lib/promptBuilder/transport.ts +0 -94
- package/lib/promptBuilder/types.ts +0 -109
- package/lib/promptImport/curatedSources.ts +0 -141
- package/lib/promptImport/discoveryRegistry.ts +0 -329
- package/lib/promptImport/errors.ts +0 -18
- package/lib/promptImport/githubDiscovery.ts +0 -309
- package/lib/promptImport/githubFolder.ts +0 -397
- package/lib/promptImport/githubSource.ts +0 -257
- package/lib/promptImport/gptImageHints.ts +0 -70
- package/lib/promptImport/parsePromptCandidates.ts +0 -179
- package/lib/promptImport/promptIndex.ts +0 -326
- package/lib/promptImport/rankPromptCandidates.ts +0 -65
- package/lib/promptImport/types.ts +0 -103
- package/lib/promptSafetyPolicy.ts +0 -5
- package/lib/providerOptions.ts +0 -56
- package/lib/referenceImageCompress.ts +0 -84
- package/lib/refs.ts +0 -133
- package/lib/requestLogger.ts +0 -49
- package/lib/responsesDoctor.ts +0 -456
- package/lib/responsesErrors.ts +0 -83
- package/lib/responsesFallback.ts +0 -114
- package/lib/responsesImageAdapter.ts +0 -466
- package/lib/responsesParse.ts +0 -452
- package/lib/responsesTools.ts +0 -28
- package/lib/runtimeContext.ts +0 -146
- package/lib/runtimePorts.ts +0 -105
- package/lib/sessionStore.ts +0 -308
- package/lib/storageMigration.ts +0 -310
- package/lib/styleSheet.ts +0 -139
- package/lib/systemTrash.ts +0 -20
- package/lib/videoContinuity.ts +0 -180
- package/lib/videoFrameExtract.ts +0 -78
- package/lib/videoSeriesChain.ts +0 -29
- package/lib/visibleTextLanguagePolicy.ts +0 -7
- package/routes/agent.ts +0 -308
- package/routes/annotations.ts +0 -118
- package/routes/canvasVersions.ts +0 -69
- package/routes/capabilities.ts +0 -18
- package/routes/cardNews.ts +0 -211
- package/routes/comfy.ts +0 -43
- package/routes/edit.ts +0 -352
- package/routes/generate.ts +0 -492
- package/routes/grok.ts +0 -24
- package/routes/health.ts +0 -123
- package/routes/history.ts +0 -221
- package/routes/imageImport.ts +0 -37
- package/routes/index.ts +0 -52
- package/routes/metadata.ts +0 -77
- package/routes/multimode.ts +0 -499
- package/routes/nodes.ts +0 -578
- package/routes/promptBuilder.ts +0 -37
- package/routes/promptImport.ts +0 -379
- package/routes/prompts.ts +0 -428
- package/routes/quota.ts +0 -89
- package/routes/sessions.ts +0 -317
- package/routes/storage.ts +0 -47
- package/routes/video.ts +0 -300
- package/routes/videoExtended.ts +0 -284
- package/server.ts +0 -293
- package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +0 -1
- package/ui/dist/assets/index-CjgnNtgt.css +0 -1
- package/ui/dist/assets/index-Da2s4_-5.js +0 -36
package/bin/lib/config-store.ts
DELETED
|
@@ -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
|
-
}
|
package/bin/lib/doctor-checks.ts
DELETED
|
@@ -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
|
-
}
|
package/bin/lib/error-hints.ts
DELETED
|
@@ -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
|
-
}
|
package/bin/lib/platform.ts
DELETED
|
@@ -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
|
-
}
|