ima2-gen 3.13.1 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/CHANGELOG.md +14 -0
- package/README.md +15 -2
- package/bin/commands/capabilities.js +5 -3
- package/bin/commands/config.js +15 -4
- package/bin/commands/defaults.js +7 -5
- package/bin/commands/doctor.js +88 -39
- package/bin/commands/gen.js +21 -4
- package/bin/commands/models.js +2 -2
- package/bin/commands/ping.js +1 -1
- package/bin/commands/prompt.js +5 -3
- package/bin/commands/service.js +32 -4
- package/bin/commands/tools.js +19 -3
- package/bin/commands/upscale.js +18 -4
- package/bin/commands/video.js +37 -50
- package/bin/ima2.js +28 -9
- package/bin/lib/characterResolve.js +2 -1
- package/bin/lib/client.js +155 -20
- package/bin/lib/config-store.js +40 -7
- package/bin/lib/doctor-bundle.js +6 -2
- package/bin/lib/doctor-checks.js +13 -29
- package/bin/lib/doctor-media.js +3 -3
- package/bin/lib/doctor-providers.js +48 -35
- package/bin/lib/doctor-report.js +78 -0
- package/bin/lib/doctor-runtime.js +173 -0
- package/bin/lib/mcpJob.js +19 -7
- package/bin/lib/output.js +3 -0
- package/bin/lib/platform.js +12 -9
- package/bin/lib/recover-output.js +9 -2
- package/bin/lib/sse.js +9 -1
- package/bin/lib/videoMcp.js +16 -8
- package/config.js +41 -5
- package/docs/API.md +154 -16
- package/docs/CLI.md +65 -1
- package/docs/DOCKER.md +19 -4
- package/docs/DOCKER.zh-CN.md +11 -4
- package/docs/DOCKER.zh-TW.md +11 -4
- package/docs/FAQ.md +13 -0
- package/docs/NPX_QUICKSTART.md +6 -0
- package/docs/README.ja.md +15 -2
- package/docs/README.ko.md +15 -2
- package/docs/README.zh-CN.md +15 -2
- package/docs/README.zh-TW.md +15 -2
- package/docs/migration/runtime-test-inventory.md +79 -8
- package/lib/agyArtifact.js +82 -0
- package/lib/agyArtifactRead.js +249 -0
- package/lib/agyImageAdapter.js +2 -329
- package/lib/agyProcess.js +122 -0
- package/lib/apiRequestBudget.js +48 -0
- package/lib/assetLifecycle.js +42 -9
- package/lib/canvasVersionStore.js +46 -14
- package/lib/capabilities.js +7 -1
- package/lib/configKeys.js +2 -0
- package/lib/errors/providerMap.js +26 -0
- package/lib/eventBus.js +10 -6
- package/lib/eventsPolicy.js +9 -0
- package/lib/geminiApiImageAdapter.js +1 -214
- package/lib/generatePipeline.js +39 -165
- package/lib/generatedMediaAccess.js +74 -0
- package/lib/generationErrors.js +3 -22
- package/lib/grokImageAdapter.js +2 -353
- package/lib/grokImageCore.js +1 -57
- package/lib/grokImageDownload.js +159 -0
- package/lib/grokImageDownloadPolicy.js +124 -0
- package/lib/grokImagePlanner.js +301 -0
- package/lib/grokMultimodeAdapter.js +1 -75
- package/lib/grokVideoDownload.js +137 -34
- package/lib/inflight.js +112 -112
- package/lib/jobs/terminalStore.js +45 -0
- package/lib/lanSessionStore.js +131 -0
- package/lib/localAccessPolicy.js +180 -0
- package/lib/localLanAccess.js +281 -0
- package/lib/logger.js +22 -13
- package/lib/mcp/downloadMediaResult.js +72 -78
- package/lib/multimodePipeline.js +39 -126
- package/lib/nodeGeneration.js +44 -101
- package/lib/nodeStore.js +25 -11
- package/lib/nodeValidation.js +5 -1
- package/lib/pinnedHttpGet.js +174 -0
- package/lib/promptImport/githubFolder.js +32 -18
- package/lib/promptImport/githubSource.js +14 -10
- package/lib/promptImport/parsePromptCandidates.js +32 -2
- package/lib/providers/adapters/agyOperations.js +179 -0
- package/lib/providers/adapters/comfy.js +1 -0
- package/lib/providers/adapters/geminiOperations.js +214 -0
- package/lib/providers/adapters/googleExecution.js +97 -0
- package/lib/providers/adapters/grokExecution.js +137 -0
- package/lib/providers/adapters/grokMultimodeOperations.js +83 -0
- package/lib/providers/adapters/grokOperations.js +63 -0
- package/lib/providers/adapters/openaiExecution.js +118 -0
- package/lib/providers/adapters/openaiOperations.js +201 -0
- package/lib/providers/adapters/openaiTypes.js +1 -0
- package/lib/providers/derive.js +4 -0
- package/lib/providers/deriveCore.js +2 -7
- package/lib/providers/execution/admission.js +25 -0
- package/lib/providers/execution/index.js +34 -0
- package/lib/providers/execution/legacy.js +17 -0
- package/lib/providers/execution/legacyClassic.js +66 -0
- package/lib/providers/execution/legacyEdit.js +30 -0
- package/lib/providers/execution/legacyMultimode.js +34 -0
- package/lib/providers/execution/legacyNode.js +41 -0
- package/lib/providers/execution/types.js +1 -0
- package/lib/providers/registry.js +19 -14
- package/lib/providers/surfaceSupport.js +22 -0
- package/lib/responsesImageAdapter.js +1 -389
- package/lib/responsesTransport.js +190 -0
- package/lib/spriteJobEvents.js +20 -2
- package/lib/ssePublish.js +5 -4
- package/lib/videoExtendI2vOperation.js +76 -0
- package/package.json +5 -2
- package/routes/edit.js +31 -123
- package/routes/events.js +140 -63
- package/routes/mcpMedia.js +10 -2
- package/routes/models.js +28 -25
- package/routes/videoExtended.js +9 -68
- package/server.js +24 -46
- package/ui/dist/.vite/manifest.json +90 -63
- package/ui/dist/assets/{AgentWorkspace-PbBytTeV.js → AgentWorkspace-CL3MLv3l.js} +2 -2
- package/ui/dist/assets/App-B9cEM6dJ.js +9 -0
- package/ui/dist/assets/AssetGenWorkspace-swWPvXrx.js +2 -0
- package/ui/dist/assets/AssetsWorkspace-CRVeYU2F.js +1 -0
- package/ui/dist/assets/{CardNewsWorkspace-BS3jKOja.js → CardNewsWorkspace-BAOxWLKf.js} +2 -2
- package/ui/dist/assets/GenerationRequestLogPanel-DZtYZm-D.js +1 -0
- package/ui/dist/assets/HomeWorkspace-BeZ1s0Jm.js +1 -0
- package/ui/dist/assets/KeyingPanel-ZyBSXioh.js +1 -0
- package/ui/dist/assets/NodeCanvas-BvvcCErf.js +7 -0
- package/ui/dist/assets/{PromptBuilderPanel-D97A4pwB.js → PromptBuilderPanel-DBycmShA.js} +2 -2
- package/ui/dist/assets/PromptImportDialog-ChvMv7ek.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-60py7whH.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-CCRh_-_7.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-BFWQO8OT.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5Ghsgdj.js +1 -0
- package/ui/dist/assets/SpriteRecipeWorkspace-DRWdkmwU.js +1 -0
- package/ui/dist/assets/index-7ngEOVxt.js +5 -0
- package/ui/dist/assets/index-B5jy7ePg.css +1 -0
- package/ui/dist/assets/index-gxh_02DQ.js +22 -0
- package/ui/dist/assets/{pptxgen.es-D4pedbwM.js → pptxgen.es-OUNhQLEV.js} +1 -1
- package/ui/dist/assets/promptBuilderStore-BTUUzbBT.js +1 -0
- package/ui/dist/assets/useAgentDialogFocus-DSc2qY8p.js +1 -0
- package/ui/dist/index.html +3 -2
- package/ui/dist/assets/AssetGenWorkspace-B-TGlvVb.js +0 -2
- package/ui/dist/assets/AssetsWorkspace-LnGaW151.js +0 -1
- package/ui/dist/assets/GenerationRequestLogPanel-DuDWD_ic.js +0 -1
- package/ui/dist/assets/HomeWorkspace-DVP7UG6O.js +0 -1
- package/ui/dist/assets/KeyingPanel-5bRUFQaq.js +0 -1
- package/ui/dist/assets/NodeCanvas-BBEpERTb.js +0 -7
- package/ui/dist/assets/PromptImportDialog-C89MN5JY.js +0 -2
- package/ui/dist/assets/PromptImportDiscoverySection-BgRfDOs2.js +0 -1
- package/ui/dist/assets/PromptImportFolderSection-zTMyriT1.js +0 -1
- package/ui/dist/assets/PromptLibraryPanel-BD96qZu_.js +0 -2
- package/ui/dist/assets/SettingsWorkspace-BAOk3X_8.js +0 -1
- package/ui/dist/assets/SpriteRecipeWorkspace-ClIM5QRo.js +0 -1
- package/ui/dist/assets/index-0fpfY8vu.css +0 -1
- package/ui/dist/assets/index-BFciPGTn.js +0 -5
- package/ui/dist/assets/index-BbykeXR9.js +0 -30
- package/ui/dist/assets/promptBuilderStore-Do7M8g7J.js +0 -1
- package/ui/dist/assets/useAgentDialogFocus-B6-QgEVI.js +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, realpath, rmdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
import { AGY_ARTIFACT_POLICY } from "../config.js";
|
|
6
|
+
import { agyError } from "./agyProcess.js";
|
|
7
|
+
// Only an issued receipt authorizes cleanup; exposed metadata is not authority.
|
|
8
|
+
const receipts = new WeakMap();
|
|
9
|
+
const pathRejected = () => agyError("Agy artifact path was rejected", 502, "AGY_PATH_REJECTED");
|
|
10
|
+
const tooLarge = () => agyError("Agy artifact exceeds the size limit", 502, "AGY_ARTIFACT_TOO_LARGE");
|
|
11
|
+
function rejectPathOrIo(error) {
|
|
12
|
+
if (error instanceof Error && Reflect.get(error, "code") === "EIO")
|
|
13
|
+
throw error;
|
|
14
|
+
throw pathRejected();
|
|
15
|
+
}
|
|
16
|
+
function abortIfNeeded(signal) {
|
|
17
|
+
if (signal?.aborted)
|
|
18
|
+
throw agyError("Generation canceled", 499, "GENERATION_CANCELED");
|
|
19
|
+
}
|
|
20
|
+
function canceled(error) {
|
|
21
|
+
return error instanceof Error && Reflect.get(error, "code") === "GENERATION_CANCELED"
|
|
22
|
+
&& Reflect.get(error, "status") === 499;
|
|
23
|
+
}
|
|
24
|
+
function contained(root, target) {
|
|
25
|
+
const part = relative(root, target);
|
|
26
|
+
return part !== "" && part !== ".." && !part.startsWith(`..${sep}`) && !isAbsolute(part);
|
|
27
|
+
}
|
|
28
|
+
function identity(info) {
|
|
29
|
+
// Filesystems without usable inode identity cannot satisfy this contract.
|
|
30
|
+
if (info.ino <= 0n)
|
|
31
|
+
throw pathRejected();
|
|
32
|
+
return Object.freeze({ dev: info.dev, ino: info.ino });
|
|
33
|
+
}
|
|
34
|
+
function sameIdentity(info, expected) {
|
|
35
|
+
return info.dev === expected.dev && info.ino === expected.ino;
|
|
36
|
+
}
|
|
37
|
+
async function approvedRoots() {
|
|
38
|
+
const roots = [];
|
|
39
|
+
for (const source of [join(homedir(), ".gemini"), join(homedir(), ".cache"), tmpdir()]) {
|
|
40
|
+
try {
|
|
41
|
+
const canonical = await realpath(source);
|
|
42
|
+
const info = await lstat(canonical, { bigint: true });
|
|
43
|
+
if (!info.isDirectory())
|
|
44
|
+
throw pathRejected();
|
|
45
|
+
roots.push({ source, canonical, identity: identity(info) });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof Error && Reflect.get(error, "code") === "ENOENT")
|
|
49
|
+
continue;
|
|
50
|
+
throw pathRejected();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return roots;
|
|
54
|
+
}
|
|
55
|
+
async function validateRoot(root) {
|
|
56
|
+
const current = await realpath(root.source);
|
|
57
|
+
const info = await lstat(current, { bigint: true });
|
|
58
|
+
if (current !== root.canonical || !info.isDirectory() || !sameIdentity(info, root.identity)) {
|
|
59
|
+
throw pathRejected();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function inspectCandidate(candidate) {
|
|
63
|
+
if (!isAbsolute(candidate) || candidate.includes("\0")
|
|
64
|
+
|| (process.platform === "win32" && candidate.slice(2).includes(":")))
|
|
65
|
+
throw pathRejected();
|
|
66
|
+
const roots = await approvedRoots();
|
|
67
|
+
if (!roots.some((root) => contained(root.source, candidate) || contained(root.canonical, candidate))) {
|
|
68
|
+
throw pathRejected();
|
|
69
|
+
}
|
|
70
|
+
let info;
|
|
71
|
+
try {
|
|
72
|
+
info = await lstat(candidate, { bigint: true });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof Error && ["ENOENT", "ENOTDIR"].includes(String(Reflect.get(error, "code")))) {
|
|
76
|
+
throw agyError("Agy artifact was not found", 502, "AGY_ARTIFACT_NOT_FOUND");
|
|
77
|
+
}
|
|
78
|
+
rejectPathOrIo(error);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
if (info.isSymbolicLink() || !info.isFile())
|
|
82
|
+
throw pathRejected();
|
|
83
|
+
const canonicalPath = await realpath(candidate);
|
|
84
|
+
const root = roots.find((entry) => contained(entry.canonical, canonicalPath));
|
|
85
|
+
if (!root)
|
|
86
|
+
throw pathRejected();
|
|
87
|
+
const parent = await lstat(dirname(canonicalPath), { bigint: true });
|
|
88
|
+
if (!parent.isDirectory())
|
|
89
|
+
throw pathRejected();
|
|
90
|
+
return { candidate, canonicalPath, roots, root, identity: identity(info), parentIdentity: identity(parent) };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
rejectPathOrIo(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function validateMapping(snapshot) {
|
|
97
|
+
try {
|
|
98
|
+
await validateRoot(snapshot.root);
|
|
99
|
+
const candidateInfo = await lstat(snapshot.candidate, { bigint: true });
|
|
100
|
+
const canonical = await realpath(snapshot.candidate);
|
|
101
|
+
const canonicalInfo = await lstat(snapshot.canonicalPath, { bigint: true });
|
|
102
|
+
if (!candidateInfo.isFile() || candidateInfo.isSymbolicLink()
|
|
103
|
+
|| !sameIdentity(candidateInfo, snapshot.identity) || canonical !== snapshot.canonicalPath
|
|
104
|
+
|| !canonicalInfo.isFile() || canonicalInfo.isSymbolicLink()
|
|
105
|
+
|| !sameIdentity(canonicalInfo, snapshot.identity)
|
|
106
|
+
|| !contained(snapshot.root.canonical, canonical))
|
|
107
|
+
throw pathRejected();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
rejectPathOrIo(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function openChecked(candidate) {
|
|
114
|
+
const snapshot = await inspectCandidate(candidate);
|
|
115
|
+
let handle;
|
|
116
|
+
try {
|
|
117
|
+
const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
118
|
+
handle = await open(snapshot.canonicalPath, flags);
|
|
119
|
+
const info = await handle.stat({ bigint: true });
|
|
120
|
+
if (!info.isFile() || !sameIdentity(info, snapshot.identity))
|
|
121
|
+
throw pathRejected();
|
|
122
|
+
await validateMapping(snapshot);
|
|
123
|
+
return { handle, snapshot, size: info.size };
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (handle)
|
|
127
|
+
await handle.close().catch(() => { }); // Preserve the failed trust check.
|
|
128
|
+
// No descriptor receipt exists until every trust check above succeeds.
|
|
129
|
+
rejectPathOrIo(error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function readBytes(handle, signal) {
|
|
133
|
+
const { maxBytes, chunkBytes } = AGY_ARTIFACT_POLICY;
|
|
134
|
+
const chunks = [];
|
|
135
|
+
let total = 0;
|
|
136
|
+
try {
|
|
137
|
+
while (total < maxBytes) {
|
|
138
|
+
abortIfNeeded(signal);
|
|
139
|
+
const block = Buffer.allocUnsafe(Math.min(chunkBytes, maxBytes - total));
|
|
140
|
+
let filled = 0;
|
|
141
|
+
while (filled < block.length) {
|
|
142
|
+
const { bytesRead } = await handle.read(block, filled, block.length - filled, total);
|
|
143
|
+
abortIfNeeded(signal);
|
|
144
|
+
if (bytesRead === 0) {
|
|
145
|
+
if (filled)
|
|
146
|
+
chunks.push(block.subarray(0, filled));
|
|
147
|
+
return Buffer.concat(chunks, total);
|
|
148
|
+
}
|
|
149
|
+
filled += bytesRead;
|
|
150
|
+
total += bytesRead;
|
|
151
|
+
}
|
|
152
|
+
chunks.push(block);
|
|
153
|
+
}
|
|
154
|
+
const { bytesRead } = await handle.read(Buffer.allocUnsafe(1), 0, 1, total);
|
|
155
|
+
abortIfNeeded(signal);
|
|
156
|
+
if (bytesRead)
|
|
157
|
+
throw tooLarge();
|
|
158
|
+
return Buffer.concat(chunks, total);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
chunks.length = 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function cleanupSnapshot(snapshot) {
|
|
168
|
+
try {
|
|
169
|
+
await validateMapping(snapshot);
|
|
170
|
+
const parent = dirname(snapshot.canonicalPath);
|
|
171
|
+
const info = await lstat(parent, { bigint: true });
|
|
172
|
+
if (!info.isDirectory() || !sameIdentity(info, snapshot.parentIdentity))
|
|
173
|
+
return;
|
|
174
|
+
await unlink(snapshot.canonicalPath);
|
|
175
|
+
if (snapshot.roots.some((root) => root.canonical === parent))
|
|
176
|
+
return;
|
|
177
|
+
await validateRoot(snapshot.root);
|
|
178
|
+
const currentParent = await realpath(parent);
|
|
179
|
+
const currentInfo = await lstat(parent, { bigint: true });
|
|
180
|
+
if (currentParent !== parent || !contained(snapshot.root.canonical, parent)
|
|
181
|
+
|| !currentInfo.isDirectory() || !sameIdentity(currentInfo, snapshot.parentIdentity))
|
|
182
|
+
return;
|
|
183
|
+
// A concurrent sibling makes this fail ENOTEMPTY; never recursively sweep it.
|
|
184
|
+
await rmdir(parent);
|
|
185
|
+
}
|
|
186
|
+
catch { /* Best effort: policy rejection or replacement grants no further deletion. */ }
|
|
187
|
+
}
|
|
188
|
+
export async function cleanupAgyArtifact(receipt) {
|
|
189
|
+
const snapshot = receipts.get(receipt);
|
|
190
|
+
if (!snapshot)
|
|
191
|
+
return;
|
|
192
|
+
receipts.delete(receipt);
|
|
193
|
+
await cleanupSnapshot(snapshot);
|
|
194
|
+
}
|
|
195
|
+
/** Static/detected path confinement, not an atomic sandbox against same-user races. */
|
|
196
|
+
export async function readAgyArtifact(candidate, signal) {
|
|
197
|
+
abortIfNeeded(signal);
|
|
198
|
+
let opened;
|
|
199
|
+
let buffer;
|
|
200
|
+
let failure;
|
|
201
|
+
let failed = false;
|
|
202
|
+
try {
|
|
203
|
+
opened = await openChecked(candidate);
|
|
204
|
+
if (opened.size > BigInt(AGY_ARTIFACT_POLICY.maxBytes))
|
|
205
|
+
throw tooLarge();
|
|
206
|
+
abortIfNeeded(signal);
|
|
207
|
+
buffer = await readBytes(opened.handle, signal);
|
|
208
|
+
await validateMapping(opened.snapshot);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
failure = error;
|
|
212
|
+
failed = true;
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
if (opened) {
|
|
216
|
+
try {
|
|
217
|
+
await opened.handle.close();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (!failed) {
|
|
221
|
+
failure = error;
|
|
222
|
+
failed = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (failed) {
|
|
228
|
+
if (opened && canceled(failure))
|
|
229
|
+
await cleanupSnapshot(opened.snapshot);
|
|
230
|
+
throw failure;
|
|
231
|
+
}
|
|
232
|
+
// The successful path necessarily acquired both; no external receipt exists yet.
|
|
233
|
+
if (!opened || !buffer)
|
|
234
|
+
throw pathRejected();
|
|
235
|
+
try {
|
|
236
|
+
abortIfNeeded(signal);
|
|
237
|
+
await validateMapping(opened.snapshot);
|
|
238
|
+
abortIfNeeded(signal);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (canceled(error))
|
|
242
|
+
await cleanupSnapshot(opened.snapshot);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
const receipt = Object.freeze({ buffer, canonicalPath: opened.snapshot.canonicalPath,
|
|
246
|
+
identity: opened.snapshot.identity, approvedRoots: Object.freeze(opened.snapshot.roots.map((root) => root.canonical)) });
|
|
247
|
+
receipts.set(receipt, opened.snapshot);
|
|
248
|
+
return receipt;
|
|
249
|
+
}
|
package/lib/agyImageAdapter.js
CHANGED
|
@@ -1,329 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { extname, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { homedir, tmpdir } from "node:os";
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { buildAgyPathEnv, resolveAgyBin } from "./agyCli.js";
|
|
7
|
-
import { logEvent } from "./logger.js";
|
|
8
|
-
import { SAFETY_INTENT_POLICY } from "./promptSafetyPolicy.js";
|
|
9
|
-
import { detectImageMimeFromB64 } from "./refs.js";
|
|
10
|
-
import { deriveReferenceLimit } from "./providers/derive.js";
|
|
11
|
-
const AGY_TIMEOUT_MS = 360_000;
|
|
12
|
-
const AGY_OUTPUT_RESOLUTION = "1024x1024";
|
|
13
|
-
const AGY_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
14
|
-
function agyError(message, status, code) {
|
|
15
|
-
const err = new Error(message);
|
|
16
|
-
err.status = status;
|
|
17
|
-
err.code = code;
|
|
18
|
-
return err;
|
|
19
|
-
}
|
|
20
|
-
function buildAgyPrompt(userPrompt, referencePaths) {
|
|
21
|
-
const imagePathsJson = referencePaths.length > 0
|
|
22
|
-
? JSON.stringify(referencePaths)
|
|
23
|
-
: "[]";
|
|
24
|
-
return [
|
|
25
|
-
"You are an image generation assistant inside a professional creative tool used by designers and artists. Users create content for legitimate creative, commercial, or educational purposes.",
|
|
26
|
-
SAFETY_INTENT_POLICY,
|
|
27
|
-
"",
|
|
28
|
-
"Generate one image by calling the tool default_api:generate_image once.",
|
|
29
|
-
"After the tool finishes, print one machine-readable result line so ima2-gen can copy the artifact.",
|
|
30
|
-
"",
|
|
31
|
-
"Tool parameters:",
|
|
32
|
-
` Prompt: ${JSON.stringify(userPrompt)}`,
|
|
33
|
-
' ImageName: "ima2_generated"',
|
|
34
|
-
` ImagePaths: ${imagePathsJson}`,
|
|
35
|
-
' toolSummary: "ima2 pipeline generation"',
|
|
36
|
-
' toolAction: "Generating ima2 image"',
|
|
37
|
-
"",
|
|
38
|
-
`Reference count: ${referencePaths.length}. The output resolution is fixed at ${AGY_OUTPUT_RESOLUTION}.`,
|
|
39
|
-
"",
|
|
40
|
-
"PROMPT HANDLING:",
|
|
41
|
-
"- Preserve the user's prompt as-is for compliant requests. Do not translate, restyle, or soften benign adult clothing requests such as swimwear.",
|
|
42
|
-
"- If the user wrote in Korean, Japanese, or any non-English language, keep visible text and dialogue in that language.",
|
|
43
|
-
"- Do not sexualize, intensify exposure, or add sexualized/fetish framing to benign swimwear, sportswear, or catalog fashion prompts.",
|
|
44
|
-
"- Do not copy safety labels, refusal reasons, or negative safety constraints into the final image prompt.",
|
|
45
|
-
"- If the request includes minors, underage-looking subjects, explicit sexual activity, or fetish framing, do not rewrite it as a benign allowed prompt.",
|
|
46
|
-
"- Content moderation is still handled downstream by the provider.",
|
|
47
|
-
"",
|
|
48
|
-
"OUTPUT:",
|
|
49
|
-
"If generation succeeds, print: RESULT|<absolute_artifact_path>|<file_extension>",
|
|
50
|
-
"If generation fails, print: ERROR|<concise error message>",
|
|
51
|
-
].join("\n");
|
|
52
|
-
}
|
|
53
|
-
function parseAgyOutput(stdout) {
|
|
54
|
-
const lines = stdout.replace(/\r/g, "").trim().split("\n").filter((l) => l.trim().length > 0);
|
|
55
|
-
const resultLine = lines.find((l) => l.startsWith("RESULT|"));
|
|
56
|
-
if (resultLine) {
|
|
57
|
-
const parts = resultLine.split("|");
|
|
58
|
-
const artifactPath = parts[1];
|
|
59
|
-
const ext = parts[2];
|
|
60
|
-
if (parts.length >= 3 && artifactPath && ext) {
|
|
61
|
-
return { artifactPath: artifactPath.trim(), ext: ext.trim() };
|
|
62
|
-
}
|
|
63
|
-
throw agyError(`Malformed RESULT line: ${resultLine}`, 502, "AGY_MALFORMED_RESULT");
|
|
64
|
-
}
|
|
65
|
-
const errorLine = lines.find((l) => l.startsWith("ERROR|"));
|
|
66
|
-
if (errorLine) {
|
|
67
|
-
const msg = errorLine.slice("ERROR|".length).trim() || "Unknown agy error";
|
|
68
|
-
const lower = msg.toLowerCase();
|
|
69
|
-
if (lower.includes("resource exhausted") || lower.includes("exhausted your capacity") || lower.includes("quota will reset")) {
|
|
70
|
-
throw agyError(`Agy generation failed: ${msg}`, 429, "AGY_QUOTA_EXHAUSTED");
|
|
71
|
-
}
|
|
72
|
-
throw agyError(`Agy generation failed: ${msg}`, 502, "AGY_GENERATION_FAILED");
|
|
73
|
-
}
|
|
74
|
-
const fullLower = stdout.toLowerCase();
|
|
75
|
-
if (fullLower.includes("resource exhausted") || fullLower.includes("exhausted your capacity")) {
|
|
76
|
-
throw agyError(`Agy quota exhausted: ${stdout.trim().slice(0, 200)}`, 429, "AGY_QUOTA_EXHAUSTED");
|
|
77
|
-
}
|
|
78
|
-
const savedPathLine = lines.find((l) => l.startsWith("SAVED_PATH="));
|
|
79
|
-
if (savedPathLine) {
|
|
80
|
-
const p = savedPathLine.slice("SAVED_PATH=".length).trim();
|
|
81
|
-
const ext = p.split(".").pop() || "png";
|
|
82
|
-
return { artifactPath: p, ext };
|
|
83
|
-
}
|
|
84
|
-
const normalizedStdout = stdout.replace(/\r/g, "").replace(/\\/g, "/");
|
|
85
|
-
const pathMatch = normalizedStdout.match(/(?:[A-Za-z]:)?\/[^\s"']+\/(brain|artifacts|\.gemini)\/[^\s"']+\.(png|jpg|jpeg|webp)/i);
|
|
86
|
-
if (pathMatch) {
|
|
87
|
-
const matched = pathMatch[0];
|
|
88
|
-
const artifactPath = process.platform === "win32" ? matched.replace(/\//g, "\\") : matched;
|
|
89
|
-
const ext = extname(artifactPath).slice(1) || "png";
|
|
90
|
-
return { artifactPath, ext };
|
|
91
|
-
}
|
|
92
|
-
throw agyError(`Could not parse artifact path from agy output (${stdout.length} chars): ${stdout.slice(0, 200)}`, 502, "AGY_PARSE_FAILED");
|
|
93
|
-
}
|
|
94
|
-
export async function findRecentAgyArtifact(sinceMs, rootOverrides) {
|
|
95
|
-
const roots = rootOverrides ?? [
|
|
96
|
-
join(homedir(), ".gemini", "antigravity-cli", "brain"),
|
|
97
|
-
join(homedir(), ".gemini"),
|
|
98
|
-
];
|
|
99
|
-
const candidates = [];
|
|
100
|
-
const seen = new Set();
|
|
101
|
-
async function walk(dir, depth) {
|
|
102
|
-
if (depth > 5 || seen.has(dir))
|
|
103
|
-
return;
|
|
104
|
-
seen.add(dir);
|
|
105
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
106
|
-
for (const entry of entries) {
|
|
107
|
-
const p = join(dir, entry.name);
|
|
108
|
-
if (!entry.isSymbolicLink() && entry.isDirectory()) {
|
|
109
|
-
await walk(p, depth + 1);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (!/^ima2_generated.*\.(png|jpg|jpeg|webp)$/i.test(entry.name)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const s = await stat(p).catch(() => null);
|
|
116
|
-
if (!s)
|
|
117
|
-
continue;
|
|
118
|
-
if (s.mtimeMs >= sinceMs - 5_000) {
|
|
119
|
-
candidates.push({ path: p, mtimeMs: s.mtimeMs });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
for (const root of roots) {
|
|
124
|
-
await walk(root, 0);
|
|
125
|
-
}
|
|
126
|
-
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
127
|
-
return candidates[0]?.path ?? null;
|
|
128
|
-
}
|
|
129
|
-
function spawnAgy(prompt, signal) {
|
|
130
|
-
return new Promise((resolve, reject) => {
|
|
131
|
-
const child = spawn(resolveAgyBin(), ["-p", "-"], {
|
|
132
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
133
|
-
env: {
|
|
134
|
-
PATH: buildAgyPathEnv(),
|
|
135
|
-
HOME: process.env.HOME,
|
|
136
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
137
|
-
TMPDIR: process.env.TMPDIR,
|
|
138
|
-
TEMP: process.env.TEMP,
|
|
139
|
-
LANG: process.env.LANG,
|
|
140
|
-
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
let stdout = "";
|
|
144
|
-
let stderr = "";
|
|
145
|
-
let settled = false;
|
|
146
|
-
const timer = setTimeout(() => {
|
|
147
|
-
if (!settled) {
|
|
148
|
-
settled = true;
|
|
149
|
-
child.kill("SIGTERM");
|
|
150
|
-
reject(agyError("Agy generation timed out", 504, "AGY_TIMEOUT"));
|
|
151
|
-
}
|
|
152
|
-
}, AGY_TIMEOUT_MS);
|
|
153
|
-
child.stdout.on("data", (chunk) => { if (stdout.length < AGY_MAX_OUTPUT_BYTES)
|
|
154
|
-
stdout += chunk.toString(); });
|
|
155
|
-
child.stderr.on("data", (chunk) => { if (stderr.length < AGY_MAX_OUTPUT_BYTES)
|
|
156
|
-
stderr += chunk.toString(); });
|
|
157
|
-
child.on("error", (err) => {
|
|
158
|
-
if (settled)
|
|
159
|
-
return;
|
|
160
|
-
settled = true;
|
|
161
|
-
clearTimeout(timer);
|
|
162
|
-
const bin = resolveAgyBin();
|
|
163
|
-
const hint = err.code === "ENOENT"
|
|
164
|
-
? `. "${bin}" was not found — install agy or set IMA2_AGY_BIN=/absolute/path/to/agy`
|
|
165
|
-
: "";
|
|
166
|
-
reject(agyError(`Agy process error: ${err.message}${hint}`, 502, "AGY_PROCESS_ERROR"));
|
|
167
|
-
});
|
|
168
|
-
child.on("close", (code) => {
|
|
169
|
-
if (settled)
|
|
170
|
-
return;
|
|
171
|
-
settled = true;
|
|
172
|
-
clearTimeout(timer);
|
|
173
|
-
if (code !== 0 && !stdout.trim()) {
|
|
174
|
-
reject(agyError(`Agy exited with code ${code}: ${stderr.slice(0, 200)}`, 502, "AGY_PROCESS_ERROR"));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
resolve({ stdout, stderr });
|
|
178
|
-
});
|
|
179
|
-
if (signal) {
|
|
180
|
-
const onAbort = () => {
|
|
181
|
-
if (!settled) {
|
|
182
|
-
settled = true;
|
|
183
|
-
clearTimeout(timer);
|
|
184
|
-
child.kill("SIGTERM");
|
|
185
|
-
reject(agyError("Generation canceled", 499, "GENERATION_CANCELED"));
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
189
|
-
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
190
|
-
}
|
|
191
|
-
if (signal?.aborted) {
|
|
192
|
-
settled = true;
|
|
193
|
-
clearTimeout(timer);
|
|
194
|
-
child.kill("SIGTERM");
|
|
195
|
-
return reject(agyError("Generation canceled", 499, "GENERATION_CANCELED"));
|
|
196
|
-
}
|
|
197
|
-
child.stdin.on("error", () => { });
|
|
198
|
-
child.stdin.write(prompt);
|
|
199
|
-
child.stdin.end();
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
const MIME_TO_EXT = {
|
|
203
|
-
"image/png": "png",
|
|
204
|
-
"image/jpeg": "jpg",
|
|
205
|
-
"image/webp": "webp",
|
|
206
|
-
};
|
|
207
|
-
async function writeRefsToTempFiles(refs) {
|
|
208
|
-
if (refs.length === 0)
|
|
209
|
-
return { paths: [], cleanup: async () => { } };
|
|
210
|
-
const dir = join(tmpdir(), `ima2-agy-refs-${randomBytes(6).toString("hex")}`);
|
|
211
|
-
await mkdir(dir, { recursive: true });
|
|
212
|
-
const paths = [];
|
|
213
|
-
for (let i = 0; i < refs.length; i++) {
|
|
214
|
-
const ref = refs[i];
|
|
215
|
-
if (!ref)
|
|
216
|
-
continue;
|
|
217
|
-
const mime = ref.detectedMime || ref.declaredMime || detectImageMimeFromB64(ref.b64) || "image/png";
|
|
218
|
-
const ext = MIME_TO_EXT[mime] || "png";
|
|
219
|
-
const p = join(dir, `ref_${i}.${ext}`);
|
|
220
|
-
await writeFile(p, Buffer.from(ref.b64, "base64"));
|
|
221
|
-
paths.push(p);
|
|
222
|
-
}
|
|
223
|
-
return {
|
|
224
|
-
paths,
|
|
225
|
-
cleanup: async () => {
|
|
226
|
-
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
async function cleanupAgyArtifact(artifactPath) {
|
|
231
|
-
try {
|
|
232
|
-
await rm(artifactPath, { force: true }).catch(() => { });
|
|
233
|
-
const dir = dirname(artifactPath);
|
|
234
|
-
const entries = await readdir(dir).catch(() => null);
|
|
235
|
-
if (entries && entries.length === 0) {
|
|
236
|
-
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch { /* best-effort */ }
|
|
240
|
-
}
|
|
241
|
-
export async function generateViaAgy(prompt, options = {}) {
|
|
242
|
-
const refDetails = (options.references || []).slice(0, deriveReferenceLimit("agy", "edit"));
|
|
243
|
-
const { paths: refPaths, cleanup } = await writeRefsToTempFiles(refDetails);
|
|
244
|
-
const agyPrompt = buildAgyPrompt(prompt, refPaths);
|
|
245
|
-
logEvent("agy", "generate:start", {
|
|
246
|
-
requestId: options.requestId,
|
|
247
|
-
promptChars: prompt.length,
|
|
248
|
-
agyPromptChars: agyPrompt.length,
|
|
249
|
-
refs: refPaths.length,
|
|
250
|
-
});
|
|
251
|
-
try {
|
|
252
|
-
const generationStartedAtMs = Date.now();
|
|
253
|
-
const { stdout, stderr } = await spawnAgy(agyPrompt, options.signal);
|
|
254
|
-
if (stderr && stderr.trim().length > 0) {
|
|
255
|
-
logEvent("agy", "generate:stderr", {
|
|
256
|
-
requestId: options.requestId,
|
|
257
|
-
stderrChars: stderr.length,
|
|
258
|
-
stderrPreview: stderr.slice(0, 200),
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
const agyCombinedOutput = [stdout, stderr].filter(Boolean).join("\n");
|
|
262
|
-
let artifactPath;
|
|
263
|
-
try {
|
|
264
|
-
artifactPath = parseAgyOutput(agyCombinedOutput).artifactPath;
|
|
265
|
-
}
|
|
266
|
-
catch (err) {
|
|
267
|
-
if (err?.code !== "AGY_PARSE_FAILED") {
|
|
268
|
-
throw err;
|
|
269
|
-
}
|
|
270
|
-
const fallbackPath = await findRecentAgyArtifact(generationStartedAtMs);
|
|
271
|
-
if (!fallbackPath) {
|
|
272
|
-
throw err;
|
|
273
|
-
}
|
|
274
|
-
logEvent("agy", "generate:fallback_artifact_found", {
|
|
275
|
-
requestId: options.requestId,
|
|
276
|
-
artifactPath: fallbackPath,
|
|
277
|
-
stdoutChars: stdout.length,
|
|
278
|
-
stderrChars: stderr.length,
|
|
279
|
-
});
|
|
280
|
-
artifactPath = fallbackPath;
|
|
281
|
-
}
|
|
282
|
-
// Validate artifact path is within allowed directories
|
|
283
|
-
const resolvedPath = resolve(artifactPath);
|
|
284
|
-
const allowedPrefixes = [
|
|
285
|
-
join(homedir(), ".gemini"),
|
|
286
|
-
join(homedir(), ".cache"),
|
|
287
|
-
tmpdir(),
|
|
288
|
-
];
|
|
289
|
-
const normalizedResolved = resolvedPath.replace(/\\/g, "/");
|
|
290
|
-
const isSafePath = allowedPrefixes.some((prefix) => {
|
|
291
|
-
const normalizedPrefix = prefix.replace(/\\/g, "/");
|
|
292
|
-
return normalizedResolved.startsWith(normalizedPrefix + "/") || normalizedResolved === normalizedPrefix;
|
|
293
|
-
});
|
|
294
|
-
if (!isSafePath) {
|
|
295
|
-
throw agyError(`Agy artifact path outside allowed directories: ${resolvedPath}`, 502, "AGY_PATH_REJECTED");
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
await stat(resolvedPath);
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
throw agyError(`Agy artifact not found at parsed path: ${resolvedPath}`, 502, "AGY_ARTIFACT_NOT_FOUND");
|
|
302
|
-
}
|
|
303
|
-
const buffer = await readFile(resolvedPath);
|
|
304
|
-
const b64 = buffer.toString("base64");
|
|
305
|
-
const mime = detectImageMimeFromB64(b64) || "image/png";
|
|
306
|
-
logEvent("agy", "generate:done", {
|
|
307
|
-
requestId: options.requestId,
|
|
308
|
-
artifactPath,
|
|
309
|
-
b64Len: b64.length,
|
|
310
|
-
mime,
|
|
311
|
-
fileBytes: buffer.length,
|
|
312
|
-
});
|
|
313
|
-
await cleanupAgyArtifact(resolvedPath);
|
|
314
|
-
return {
|
|
315
|
-
b64,
|
|
316
|
-
revisedPrompt: prompt,
|
|
317
|
-
usage: { agy_artifact_bytes: buffer.length },
|
|
318
|
-
webSearchCalls: 0,
|
|
319
|
-
mime,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
catch (err) {
|
|
323
|
-
logEvent("agy", "generate:failed_cleanup", { requestId: options.requestId });
|
|
324
|
-
throw err;
|
|
325
|
-
}
|
|
326
|
-
finally {
|
|
327
|
-
await cleanup();
|
|
328
|
-
}
|
|
329
|
-
}
|
|
1
|
+
export { generateViaAgy } from "./providers/adapters/agyOperations.js";
|
|
2
|
+
export { findRecentAgyArtifact } from "./agyArtifact.js";
|