dreamlayer 0.1.0 → 0.2.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/README.md +4 -4
- package/dist/cli.js +58 -13
- package/dist/client.d.ts +16 -1
- package/dist/client.js +103 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,10 +12,6 @@ dreamlayer generate "A glass greenhouse at dusk" --out greenhouse.png
|
|
|
12
12
|
Get a key at [platform.dreamlayer.io](https://platform.dreamlayer.io). A new account
|
|
13
13
|
starts at zero credits, and each finished image costs one.
|
|
14
14
|
|
|
15
|
-
> **Not yet publishable.** This package sends an `operation` field that requires
|
|
16
|
-
> the gateway build adding it to `ExecuteRequest`. Against the currently deployed
|
|
17
|
-
> API every call returns `422 extra_forbidden`. Deploy that build first.
|
|
18
|
-
|
|
19
15
|
## Commands
|
|
20
16
|
|
|
21
17
|
```bash
|
|
@@ -31,6 +27,10 @@ dreamlayer capabilities # spends nothing
|
|
|
31
27
|
`cutout` and `upscale` name their operation rather than hoping a sentence is read the
|
|
32
28
|
way you meant, so they run a dedicated chain and never stop to ask a question.
|
|
33
29
|
|
|
30
|
+
Image inputs may be PNG, JPEG, WebP, or supported camera RAW files up to 200 MB.
|
|
31
|
+
DreamLayer develops RAW previews, applies camera orientation, and resizes oversized
|
|
32
|
+
sources on the server before any operation runs.
|
|
33
|
+
|
|
34
34
|
`upscale` doubles each side and finished images are capped at 4096 per side, so the
|
|
35
35
|
longest side of your input must be 2048 or less. Anything larger is refused before it
|
|
36
36
|
costs you a credit.
|
package/dist/cli.js
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* 6 the run ended asking a question instead of producing an image
|
|
16
16
|
*/
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
-
import {
|
|
18
|
+
import { openAsBlob, readFileSync } from "node:fs";
|
|
19
|
+
import { stat, writeFile } from "node:fs/promises";
|
|
19
20
|
import path from "node:path";
|
|
20
|
-
import { ApiError, ManagedClient, StreamIdleError, } from "./client.js";
|
|
21
|
+
import { ApiError, KNOWN_OPERATIONS, ManagedClient, StreamIdleError, UploadTimeoutError, } from "./client.js";
|
|
21
22
|
import { Progress, consume } from "./render.js";
|
|
23
|
+
const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
22
24
|
const USAGE = `dreamlayer - generate and edit images from your terminal
|
|
23
25
|
|
|
24
26
|
USAGE
|
|
@@ -104,24 +106,21 @@ function client() {
|
|
|
104
106
|
}
|
|
105
107
|
return new ManagedClient(key, (process.env.DREAMLAYER_API_URL ?? "https://api.dreamlayer.io").trim());
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
const MAX_SOURCE_BYTES = 200 * 1024 * 1024;
|
|
110
|
+
/** Upload a local file; the server owns RAW, EXIF, alpha, and resize normalization. */
|
|
108
111
|
async function upload(api, file) {
|
|
109
112
|
const resolved = path.resolve(file);
|
|
110
|
-
|
|
111
|
-
if (![".png", ".jpg", ".jpeg", ".webp"].includes(extension)) {
|
|
112
|
-
throw new UsageError(`${file} is not a PNG, JPEG, or WEBP`);
|
|
113
|
-
}
|
|
114
|
-
let bytes;
|
|
113
|
+
let fileStat;
|
|
115
114
|
try {
|
|
116
|
-
|
|
115
|
+
fileStat = await stat(resolved);
|
|
117
116
|
}
|
|
118
117
|
catch {
|
|
119
118
|
throw new UsageError(`cannot read ${file}`);
|
|
120
119
|
}
|
|
121
|
-
if (
|
|
122
|
-
throw new UsageError(`${file} is ${Math.round(
|
|
120
|
+
if (fileStat.size > MAX_SOURCE_BYTES) {
|
|
121
|
+
throw new UsageError(`${file} is ${Math.round(fileStat.size / 1024 / 1024)} MB; the limit is 200 MB`);
|
|
123
122
|
}
|
|
124
|
-
const asset = await api.uploadInput(
|
|
123
|
+
const asset = await api.uploadInput(await openAsBlob(resolved), path.basename(resolved));
|
|
125
124
|
return asset.input_asset_id;
|
|
126
125
|
}
|
|
127
126
|
function defaultOut() {
|
|
@@ -187,6 +186,45 @@ function recoveryHint(error) {
|
|
|
187
186
|
: null;
|
|
188
187
|
return id ? `The job may still be running. Check it with:\n dreamlayer status ${id}\n` : "";
|
|
189
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Say so when this build and the server disagree about what exists.
|
|
191
|
+
*
|
|
192
|
+
* The MCP package solves this by asking the server at startup and shaping its tool
|
|
193
|
+
* schema from the answer. The CLI cannot: `ManagedOperation` is a compile-time union and
|
|
194
|
+
* `cutout` / `upscale` are compile-time commands, so deriving the list at runtime would
|
|
195
|
+
* buy consistency by giving up type safety at every call site.
|
|
196
|
+
*
|
|
197
|
+
* So it reports instead of adapting, and it does so HERE because `capabilities` is free,
|
|
198
|
+
* spends nothing, and is the command people are told to run first. Both directions are
|
|
199
|
+
* worth naming:
|
|
200
|
+
*
|
|
201
|
+
* - the server offers something this build cannot reach -> the user is missing a
|
|
202
|
+
* feature they are paying for and would never know
|
|
203
|
+
* - this build names something the server will not run -> the failure that shipped on
|
|
204
|
+
* 2026-08-21, where a call looked like a client bug rather than a version skew
|
|
205
|
+
*
|
|
206
|
+
* stderr, never stdout: `dreamlayer capabilities` is piped into jq.
|
|
207
|
+
*/
|
|
208
|
+
function warnIfOperationsDrifted(capabilities) {
|
|
209
|
+
const listed = capabilities.operations;
|
|
210
|
+
if (!Array.isArray(listed) || listed.some((o) => typeof o !== "string"))
|
|
211
|
+
return;
|
|
212
|
+
const server = new Set(listed);
|
|
213
|
+
const mine = new Set(KNOWN_OPERATIONS);
|
|
214
|
+
const serverOnly = [...server].filter((o) => !mine.has(o));
|
|
215
|
+
const clientOnly = [...mine].filter((o) => !server.has(o));
|
|
216
|
+
if (serverOnly.length === 0 && clientOnly.length === 0)
|
|
217
|
+
return;
|
|
218
|
+
process.stderr.write("\nThis CLI and the server disagree about the operation list.\n");
|
|
219
|
+
if (serverOnly.length > 0) {
|
|
220
|
+
process.stderr.write(` The server offers, this version cannot use: ${serverOnly.join(", ")}\n` +
|
|
221
|
+
" Upgrade with: npm i -g dreamlayer\n");
|
|
222
|
+
}
|
|
223
|
+
if (clientOnly.length > 0) {
|
|
224
|
+
process.stderr.write(` This version names, the server will not run: ${clientOnly.join(", ")}\n` +
|
|
225
|
+
" Those commands will fail validation until the server catches up.\n");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
190
228
|
function exitCodeFor(error) {
|
|
191
229
|
if (error.status === 401 || error.status === 403)
|
|
192
230
|
return 2;
|
|
@@ -203,7 +241,7 @@ async function main(argv) {
|
|
|
203
241
|
return command ? 0 : 1;
|
|
204
242
|
}
|
|
205
243
|
if (command === "--version" || command === "-v") {
|
|
206
|
-
process.stdout.write(
|
|
244
|
+
process.stdout.write(`${PACKAGE_VERSION}\n`);
|
|
207
245
|
return 0;
|
|
208
246
|
}
|
|
209
247
|
const { positional, options } = parseOptions(rest);
|
|
@@ -259,6 +297,7 @@ async function main(argv) {
|
|
|
259
297
|
case "capabilities": {
|
|
260
298
|
const capabilities = await client().getCapabilities();
|
|
261
299
|
process.stdout.write(`${JSON.stringify(capabilities, null, 2)}\n`);
|
|
300
|
+
warnIfOperationsDrifted(capabilities);
|
|
262
301
|
return 0;
|
|
263
302
|
}
|
|
264
303
|
default:
|
|
@@ -297,6 +336,12 @@ main(process.argv.slice(2))
|
|
|
297
336
|
process.exitCode = 5;
|
|
298
337
|
return;
|
|
299
338
|
}
|
|
339
|
+
if (error instanceof UploadTimeoutError) {
|
|
340
|
+
process.stderr.write(`${error.message}\n`);
|
|
341
|
+
process.stderr.write("Temporary. Retry with --idempotency-key to avoid paying twice.\n");
|
|
342
|
+
process.exitCode = 5;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
300
345
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
301
346
|
process.stderr.write(recoveryHint(error));
|
|
302
347
|
process.exitCode = 1;
|
package/dist/client.d.ts
CHANGED
|
@@ -34,7 +34,17 @@ export type ManagedEvent = {
|
|
|
34
34
|
* anyway. Confirmed against the deployment, not the source: the live /openapi.json
|
|
35
35
|
* advertises exactly these four in both ExecuteRequest and ImageJobCreate.
|
|
36
36
|
*/
|
|
37
|
-
export
|
|
37
|
+
export declare const KNOWN_OPERATIONS: readonly ["text_to_image", "image_to_image", "background_remove", "upscale"];
|
|
38
|
+
/**
|
|
39
|
+
* Derived from the array above, not written twice.
|
|
40
|
+
*
|
|
41
|
+
* The first version of this declared the union by hand and pinned an array to it with
|
|
42
|
+
* `satisfies`. That catches a WRONG entry and not a MISSING one, because a shorter array
|
|
43
|
+
* still satisfies a wider union, so the exact drift this file exists to detect could
|
|
44
|
+
* slip through the check meant to prevent it. Deriving the type makes the array the
|
|
45
|
+
* single definition and the question unaskable.
|
|
46
|
+
*/
|
|
47
|
+
export type ManagedOperation = (typeof KNOWN_OPERATIONS)[number];
|
|
38
48
|
export type ManagedExecuteInput = {
|
|
39
49
|
prompt?: string;
|
|
40
50
|
respond?: string;
|
|
@@ -79,6 +89,10 @@ export declare class StreamIdleError extends Error {
|
|
|
79
89
|
readonly idleMs: number;
|
|
80
90
|
constructor();
|
|
81
91
|
}
|
|
92
|
+
export declare class UploadTimeoutError extends Error {
|
|
93
|
+
constructor();
|
|
94
|
+
}
|
|
95
|
+
export declare function uploadTimeoutMs(bytes: number): number;
|
|
82
96
|
/**
|
|
83
97
|
* Validate one sanitized event against the published contract.
|
|
84
98
|
*
|
|
@@ -90,6 +104,7 @@ export declare function managedEvent(event: string, id: string | null, value: un
|
|
|
90
104
|
export declare class ManagedClient {
|
|
91
105
|
private readonly apiKey;
|
|
92
106
|
private readonly baseUrl;
|
|
107
|
+
private capabilitiesPromise;
|
|
93
108
|
constructor(apiKey: string, baseUrl?: string);
|
|
94
109
|
/**
|
|
95
110
|
* Run or continue an execution, yielding each validated event as it arrives.
|
package/dist/client.js
CHANGED
|
@@ -12,6 +12,35 @@
|
|
|
12
12
|
* client instead of the dead local-proxy class, and that timeouts and an explicit
|
|
13
13
|
* redirect policy were added, which the original lacked.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Every operation the Agent API can execute.
|
|
17
|
+
*
|
|
18
|
+
* REQUIRES the gateway build that added `operation` to ExecuteRequest. Against an older
|
|
19
|
+
* deployment this field is rejected with 422 extra_forbidden, because the request model
|
|
20
|
+
* is closed. That is a sequencing constraint, not a reason to drop it: naming the
|
|
21
|
+
* operation is what stops a cutout or an upscale being re-read from the prompt and
|
|
22
|
+
* coming back as a clarifying question instead of an image.
|
|
23
|
+
*
|
|
24
|
+
* SATISFIED 2026-08-21. prodbeta176 carries the field in the gateway AND the dispatch
|
|
25
|
+
* in the workflow engine, which had been split across two releases: the gateway
|
|
26
|
+
* accepted `operation` from prodbeta174 while the half that acts on it was still on
|
|
27
|
+
* prodbeta172, so naming an operation returned 200 and was then inferred from prose
|
|
28
|
+
* anyway. Confirmed against the deployment, not the source: the live /openapi.json
|
|
29
|
+
* advertises exactly these four in both ExecuteRequest and ImageJobCreate.
|
|
30
|
+
*/
|
|
31
|
+
export const KNOWN_OPERATIONS = [
|
|
32
|
+
"text_to_image",
|
|
33
|
+
"image_to_image",
|
|
34
|
+
"background_remove",
|
|
35
|
+
"upscale",
|
|
36
|
+
];
|
|
37
|
+
const DIRECT_INPUT_BYTES = 20 * 1024 * 1024;
|
|
38
|
+
const RASTER_INPUT_SUFFIXES = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
39
|
+
const LEGACY_INPUT_SUFFIXES = new Set([
|
|
40
|
+
...RASTER_INPUT_SUFFIXES,
|
|
41
|
+
".arw", ".cr2", ".cr3", ".crw", ".dng", ".nef", ".nrw", ".orf", ".pef",
|
|
42
|
+
".raf", ".rw2", ".sr2", ".srw",
|
|
43
|
+
]);
|
|
15
44
|
export class ApiError extends Error {
|
|
16
45
|
status;
|
|
17
46
|
detail;
|
|
@@ -48,12 +77,27 @@ export class StreamIdleError extends Error {
|
|
|
48
77
|
this.name = "StreamIdleError";
|
|
49
78
|
}
|
|
50
79
|
}
|
|
80
|
+
export class UploadTimeoutError extends Error {
|
|
81
|
+
constructor() {
|
|
82
|
+
super("the staged upload stopped before it completed; no image job was started");
|
|
83
|
+
this.name = "UploadTimeoutError";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
51
86
|
const ERROR_BODY_LIMIT = 16 * 1024;
|
|
52
87
|
const ERROR_DETAIL_LIMIT = 300;
|
|
53
88
|
/**
|
|
54
89
|
* A plain request: send, get a body back. Bounded work, so a total cap is right.
|
|
55
90
|
*/
|
|
56
91
|
const REQUEST_TIMEOUT_MS = 130_000;
|
|
92
|
+
const UPLOAD_MIN_BYTES_PER_SECOND = 256 * 1024;
|
|
93
|
+
const UPLOAD_MAX_TIMEOUT_MS = 15 * 60_000;
|
|
94
|
+
export function uploadTimeoutMs(bytes) {
|
|
95
|
+
const override = Number(process.env.DREAMLAYER_UPLOAD_TIMEOUT_MS);
|
|
96
|
+
if (Number.isFinite(override) && override > 0) {
|
|
97
|
+
return Math.min(Math.max(override, 100), UPLOAD_MAX_TIMEOUT_MS);
|
|
98
|
+
}
|
|
99
|
+
return Math.min(Math.max(REQUEST_TIMEOUT_MS, 60_000 + Math.ceil(bytes / UPLOAD_MIN_BYTES_PER_SECOND) * 1000), UPLOAD_MAX_TIMEOUT_MS);
|
|
100
|
+
}
|
|
57
101
|
/**
|
|
58
102
|
* A STREAM is different, and conflating the two shipped a broken `upscale`.
|
|
59
103
|
*
|
|
@@ -292,6 +336,7 @@ function requireEventStream(response) {
|
|
|
292
336
|
export class ManagedClient {
|
|
293
337
|
apiKey;
|
|
294
338
|
baseUrl;
|
|
339
|
+
capabilitiesPromise = null;
|
|
295
340
|
constructor(apiKey, baseUrl = "https://api.dreamlayer.io") {
|
|
296
341
|
this.apiKey = apiKey;
|
|
297
342
|
if (!apiKey.trim())
|
|
@@ -326,7 +371,13 @@ export class ManagedClient {
|
|
|
326
371
|
yield* this.parse(stream);
|
|
327
372
|
}
|
|
328
373
|
async getCapabilities() {
|
|
329
|
-
|
|
374
|
+
this.capabilitiesPromise ??= this.request("/v1/capabilities").catch((error) => {
|
|
375
|
+
// Cache a successful contract for the process, but never pin a transient
|
|
376
|
+
// capabilities failure as a permanent result.
|
|
377
|
+
this.capabilitiesPromise = null;
|
|
378
|
+
throw error;
|
|
379
|
+
});
|
|
380
|
+
const capabilities = await this.capabilitiesPromise;
|
|
330
381
|
if (capabilities.api_version !== "1") {
|
|
331
382
|
throw new Error("Unsupported DreamLayer Agent API version");
|
|
332
383
|
}
|
|
@@ -348,7 +399,57 @@ export class ManagedClient {
|
|
|
348
399
|
method: "DELETE",
|
|
349
400
|
});
|
|
350
401
|
}
|
|
351
|
-
uploadInput(file, filename = "input.png") {
|
|
402
|
+
async uploadInput(file, filename = "input.png") {
|
|
403
|
+
const suffix = filename.slice(filename.lastIndexOf(".")).toLowerCase();
|
|
404
|
+
const capabilities = await this.getCapabilities();
|
|
405
|
+
const advertised = capabilities.supported_input_extensions;
|
|
406
|
+
const supported = Array.isArray(advertised)
|
|
407
|
+
? new Set(advertised.filter((item) => typeof item === "string"))
|
|
408
|
+
: LEGACY_INPUT_SUFFIXES;
|
|
409
|
+
if (!supported.has(suffix)) {
|
|
410
|
+
throw new Error(`${filename} is not a supported image or camera RAW file`);
|
|
411
|
+
}
|
|
412
|
+
if (file.size > DIRECT_INPUT_BYTES || !RASTER_INPUT_SUFFIXES.has(suffix)) {
|
|
413
|
+
const contentType = file.type || "application/octet-stream";
|
|
414
|
+
const upload = await this.request("/v1/input-assets/uploads", {
|
|
415
|
+
method: "POST",
|
|
416
|
+
headers: { "Content-Type": "application/json" },
|
|
417
|
+
body: JSON.stringify({ filename, size_bytes: file.size, content_type: contentType }),
|
|
418
|
+
});
|
|
419
|
+
const targetUrl = new URL(upload.upload_url, `${this.baseUrl}/`);
|
|
420
|
+
const headers = new Headers({ "Content-Type": upload.content_type });
|
|
421
|
+
if (upload.mode === "signed") {
|
|
422
|
+
headers.set("x-goog-content-length-range", `0,${upload.maximum_bytes}`);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
if (targetUrl.origin !== new URL(this.baseUrl).origin) {
|
|
426
|
+
throw new ApiError(502, "DreamLayer input upload", "refused an off-origin upload URL");
|
|
427
|
+
}
|
|
428
|
+
headers.set("Authorization", `Bearer ${this.apiKey}`);
|
|
429
|
+
headers.set("DreamLayer-Version", "1");
|
|
430
|
+
}
|
|
431
|
+
let response;
|
|
432
|
+
try {
|
|
433
|
+
response = await fetch(targetUrl, {
|
|
434
|
+
method: upload.http_method,
|
|
435
|
+
headers,
|
|
436
|
+
body: file,
|
|
437
|
+
redirect: "manual",
|
|
438
|
+
signal: AbortSignal.timeout(uploadTimeoutMs(file.size)),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
443
|
+
throw new UploadTimeoutError();
|
|
444
|
+
}
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
if (!response.ok)
|
|
448
|
+
throw await apiError(response, "DreamLayer input upload");
|
|
449
|
+
return this.request(`/v1/input-assets/uploads/${encodeURIComponent(upload.upload_id)}/finalize`, {
|
|
450
|
+
method: "POST",
|
|
451
|
+
});
|
|
452
|
+
}
|
|
352
453
|
const body = new FormData();
|
|
353
454
|
body.append("file", file, filename);
|
|
354
455
|
return this.request("/v1/input-assets", { method: "POST", body });
|