dreamlayer 0.3.0 → 0.4.0-beta.2
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 +72 -12
- package/dist/cli.js +217 -77
- package/dist/client.d.ts +20 -76
- package/dist/client.js +87 -123
- package/dist/render.d.ts +0 -8
- package/dist/render.js +0 -8
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ dreamlayer cutout <image> [--out file.png] # background removal
|
|
|
21
21
|
dreamlayer upscale <image> [--out file.png] # 2x
|
|
22
22
|
dreamlayer answer <conversation-id> <text> [--image file.png]
|
|
23
23
|
dreamlayer status <execution-id>
|
|
24
|
+
dreamlayer download <execution-id> --out file.png
|
|
24
25
|
dreamlayer balance # spends nothing
|
|
25
26
|
dreamlayer capabilities # spends nothing
|
|
26
27
|
```
|
|
@@ -77,28 +78,28 @@ dreamlayer balance --json
|
|
|
77
78
|
|
|
78
79
|
## Retries are safe if you reuse the key
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
Save a key before starting text generation. After an uncertain response, reuse that key
|
|
82
|
+
and the identical prompt/options, or check the existing execution first:
|
|
82
83
|
|
|
83
84
|
```bash
|
|
84
85
|
dreamlayer generate "a fox logo" --idempotency-key fox-001
|
|
85
86
|
```
|
|
86
87
|
|
|
87
|
-
##
|
|
88
|
+
## Continue a question from another client
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
The CLI's `generate`, `edit`, `cutout`, `upscale`, and `sprite` commands select their
|
|
91
|
+
operation explicitly. `generate "remove the background"` therefore remains a
|
|
92
|
+
text-to-image request; use `cutout image.png` to remove a background.
|
|
93
|
+
|
|
94
|
+
A conversational request made through the API or MCP can instead ask for missing
|
|
95
|
+
input. Continue that conversation from the CLI using its saved conversation ID:
|
|
90
96
|
|
|
91
97
|
```bash
|
|
92
|
-
dreamlayer
|
|
93
|
-
# Which image should I use? Upload or attach one, then respond.
|
|
94
|
-
# dreamlayer answer <id> "your answer" --image <file>
|
|
98
|
+
dreamlayer answer <conversation-id> "Use this image" --image reference.png
|
|
95
99
|
```
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Naming an operation avoids the round trip entirely, which is why `cutout`, `upscale`,
|
|
101
|
-
`edit`, and `generate` all do.
|
|
101
|
+
An answer that needs an image must attach one. Exit code 6 means the returned
|
|
102
|
+
conversation needs input; it does not mean a generation failed.
|
|
102
103
|
|
|
103
104
|
## Requirements
|
|
104
105
|
|
|
@@ -107,3 +108,62 @@ Node.js 22.12 or later.
|
|
|
107
108
|
## License
|
|
108
109
|
|
|
109
110
|
MIT. See LICENSE and NOTICE.
|
|
111
|
+
|
|
112
|
+
## Sprite-sheet beta
|
|
113
|
+
|
|
114
|
+
Sprite requests accept exactly one of `options.animation_prompt` (1–4000 characters) or an `options.action` preset (`walk`, `run`, `idle`). Custom prompts can describe characters, creatures, objects, effects or 360° turntables. `animation_mode` is `loop` or `once`; presets default to loop, custom prompts to once. For a turntable, request a stationary camera and rotating subject. Broad requests do not guarantee correct motion, unseen details or successful effect transparency.
|
|
115
|
+
|
|
116
|
+
Request integer `frame_count` 7–100 (default 12) and `frame_size` 32, 64, 128, 256, 512, 720 or 1080 (default 512). These are square export canvases; a larger export does not guarantee additional detail. Aspect ratio and shared alignment are preserved with transparent padding. A bundle contains transparent PNG frames, sheet, atlas, preview and import instructions, including each frame's playback duration. Choose a repeating loop or a one-time action with a beginning and ending. If the requested number of distinct frames cannot be delivered, the job fails and held credits are returned. Translucent effects can lose detail or fail; small exports are not automatically pixel art.
|
|
117
|
+
|
|
118
|
+
Pricing is unchanged across sizes: frames 1–14 cost $0.14 each; additional frames $0.07 each. One credit is $0.17. Round the complete order upward once to a tenth of a credit. Check `sprite_pricing` in capabilities and approve the quote with `max_credits`. Credits are held during processing, settled after complete delivery and restored on failure/timeout. There is no customer cancellation. Keep the execution ID to resume status. Custom requests need the matching broad-animation server release; older servers reject them. New live generation quality, 100-frame duration and actual cost remain unverified.
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
dreamlayer sprite character.png --action walk --frames 12 --max-credits 9.9 --out walk.zip
|
|
122
|
+
dreamlayer status EXECUTION_ID
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Set `--max-credits` to the amount you approve after checking the current price. The CLI reconnects to existing work if an event stream closes.
|
|
126
|
+
|
|
127
|
+
For affordability, compare the complete rounded quote in **credits** with `available`. One tenth of a credit is $0.017. Promotional and purchased amounts are displayed rounded down separately, so their displayed sum can be 0.1 credit below `available`; stored fractions are preserved. Compare against the combined total, not that sum. The order charge rounds only once, never per frame or per tier.
|
|
128
|
+
|
|
129
|
+
## Automating recovery
|
|
130
|
+
|
|
131
|
+
Commands never prompt. `--json` sends results to stdout and structured errors to stderr,
|
|
132
|
+
including usage and transport errors. Exit 0 from `status` means the read succeeded;
|
|
133
|
+
inspect its `status` field to learn whether the execution completed.
|
|
134
|
+
|
|
135
|
+
After a lost download, recover the existing execution without generation:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
dreamlayer status EXECUTION_ID --json
|
|
139
|
+
dreamlayer download EXECUTION_ID --out recovered.png --json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
All output commands refuse to overwrite existing files, directories, or symlinks.
|
|
143
|
+
Paid commands check `--out` before uploads or `/v1/execute`: an existing destination
|
|
144
|
+
returns `output_exists` (exit 1) without submitting or charging a new job. A missing or
|
|
145
|
+
unwritable parent returns `output_unavailable` (exit 1). Re-running a batch with the
|
|
146
|
+
same output paths therefore stops on completed files before paid work. Choose a new
|
|
147
|
+
path only for intentionally new work. The final write is still exclusive: if another
|
|
148
|
+
process creates the destination during generation, use the saved execution ID to
|
|
149
|
+
recover the completed output with `download`.
|
|
150
|
+
|
|
151
|
+
`download` also refuses to overwrite a file. Use `.zip` for sprite results. Generation errors
|
|
152
|
+
in JSON include the available execution ID and idempotency key for recovery. Treat them
|
|
153
|
+
as private identifiers. For file-based commands, rerunning uploads a new asset: the same
|
|
154
|
+
local file is not an identical API request. Prefer `status` and `download`, or the
|
|
155
|
+
[execution recovery guide](https://docs.dreamlayer.io/agent-api/jobs-and-events).
|
|
156
|
+
|
|
157
|
+
[API overview](https://docs.dreamlayer.io/agent-api) ·
|
|
158
|
+
[CLI guide](https://docs.dreamlayer.io/cli) ·
|
|
159
|
+
[MCP setup](https://docs.dreamlayer.io/mcp/index)
|
|
160
|
+
|
|
161
|
+
Local client failures are separate from API generation failures. `local_output_failed`
|
|
162
|
+
(exit 1) means the completed output could not be written; fix the destination and run
|
|
163
|
+
`download` with the saved execution ID. `download_failed` or `output_not_ready` (exit 5)
|
|
164
|
+
also require recovery of existing work, not a new generation. `retryable: true` means
|
|
165
|
+
retry the indicated recovery action, never blindly repeat a paid command.
|
|
166
|
+
`local_input_failed` (exit 1) means no readable input was supplied; missing credentials
|
|
167
|
+
use `authentication_failed` (exit 2). A cancelled run uses `execution_cancelled`, exit 4,
|
|
168
|
+
and a JSON error on stderr. Unknown client failures use `client_error`; they do not
|
|
169
|
+
prove that the server-side generation failed.
|
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* DreamLayer CLI.
|
|
4
|
-
*
|
|
5
|
-
* Generate and edit images from a terminal, over local files. The API is the same one
|
|
6
|
-
* the MCP server and the web app use, and it spends from the same credit balance.
|
|
7
|
-
*
|
|
8
|
-
* Exit codes are meaningful, so this composes in a script:
|
|
9
|
-
* 0 success
|
|
10
|
-
* 1 usage error
|
|
11
|
-
* 2 authentication or account problem (401, 403)
|
|
12
|
-
* 3 out of credits (402)
|
|
13
|
-
* 4 the request was rejected (409, 422)
|
|
14
|
-
* 5 temporary, worth retrying (429, 5xx)
|
|
15
|
-
* 6 the run ended asking a question instead of producing an image
|
|
16
|
-
*/
|
|
2
|
+
import { spriteCreditPrice } from "./client.js";
|
|
17
3
|
import { randomUUID } from "node:crypto";
|
|
18
|
-
import { openAsBlob, readFileSync } from "node:fs";
|
|
19
|
-
import { stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { constants, openAsBlob, readFileSync } from "node:fs";
|
|
5
|
+
import { access, lstat, stat, writeFile } from "node:fs/promises";
|
|
20
6
|
import path from "node:path";
|
|
21
|
-
import { ApiError, KNOWN_OPERATIONS, ManagedClient, StreamIdleError, UploadTimeoutError, terminalExecutionError, } from "./client.js";
|
|
7
|
+
import { ApiError, InputValidationError, RecoveryRequiredError, KNOWN_OPERATIONS, ManagedClient, StreamIdleError, UploadTimeoutError, terminalExecutionError, } from "./client.js";
|
|
22
8
|
import { Progress, consume } from "./render.js";
|
|
23
9
|
const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
24
10
|
const USAGE = `dreamlayer - generate and edit images from your terminal
|
|
@@ -28,30 +14,103 @@ USAGE
|
|
|
28
14
|
dreamlayer edit <image> <prompt> [--out <file>]
|
|
29
15
|
dreamlayer cutout <image> [--out <file>]
|
|
30
16
|
dreamlayer upscale <image> [--out <file>]
|
|
17
|
+
dreamlayer sprite <image> --action <walk|run|idle> [--frames <7–100>] --max-credits <n> [--out <zip>]
|
|
31
18
|
dreamlayer answer <conversation-id> <text> [--image <file>] [--out <file>]
|
|
19
|
+
dreamlayer download <execution-id> --out <file>
|
|
32
20
|
dreamlayer status <execution-id>
|
|
33
21
|
dreamlayer balance
|
|
34
22
|
dreamlayer capabilities
|
|
35
23
|
|
|
36
24
|
OPTIONS
|
|
37
|
-
--out <file>
|
|
25
|
+
--out <file> New output file; never overwrites. Default: dreamlayer-<n>.png
|
|
26
|
+
Existing destinations are refused before paid submission.
|
|
38
27
|
--image <file> Attach an image when answering a question that asks for one
|
|
39
28
|
--aspect <ratio> 1:1, 16:9, 9:16, 4:3, 3:4. Default 1:1
|
|
40
|
-
--
|
|
29
|
+
--action <name> Sprite preset: walk, run, idle (walk if no custom prompt)
|
|
30
|
+
--animation-prompt <text> Custom animation; cannot combine with --action
|
|
31
|
+
--animation-mode <loop|once> Default: loop for presets, once for custom
|
|
32
|
+
--frame-size <px> Square export: 32, 64, 128, 256, 512 (default), 720, 1080
|
|
33
|
+
--frames <n> Frame count: integer 7–100, default 12
|
|
34
|
+
--max-credits <n> Maximum approved charge for the sprite job
|
|
35
|
+
--json JSON results on stdout; JSON errors on stderr
|
|
41
36
|
--quiet No progress on stderr
|
|
42
37
|
--idempotency-key <key> Reuse to retry safely after an uncertain response
|
|
43
38
|
|
|
39
|
+
EXIT CODES
|
|
40
|
+
0 success, 1 usage/local error, 2 authentication/access, 3 credits/quota,
|
|
41
|
+
4 permanent API failure, 5 temporary failure, 6 input required
|
|
42
|
+
|
|
43
|
+
AUTOMATION
|
|
44
|
+
Commands never prompt. Save a unique --idempotency-key before paid work.
|
|
45
|
+
After uncertainty, use status then download; do not start a replacement job.
|
|
46
|
+
CLI guide: https://docs.dreamlayer.io/cli
|
|
47
|
+
|
|
44
48
|
ENVIRONMENT
|
|
45
49
|
DREAMLAYER_API_KEY Required. Get one at https://platform.dreamlayer.io
|
|
46
50
|
DREAMLAYER_API_URL Override the endpoint. Default https://api.dreamlayer.io
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
Image operations cost one credit. Sprite pricing is listed in capabilities. A new account starts at zero.
|
|
49
53
|
`;
|
|
50
54
|
class UsageError extends Error {
|
|
51
55
|
}
|
|
56
|
+
class CommandError extends Error {
|
|
57
|
+
reason;
|
|
58
|
+
exitCode;
|
|
59
|
+
retryable;
|
|
60
|
+
guidance;
|
|
61
|
+
constructor(reason, message, exitCode, retryable = false, guidance) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.reason = reason;
|
|
64
|
+
this.exitCode = exitCode;
|
|
65
|
+
this.retryable = retryable;
|
|
66
|
+
this.guidance = guidance;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function preflightOutput(target) {
|
|
70
|
+
let exists = false;
|
|
71
|
+
try {
|
|
72
|
+
await lstat(target);
|
|
73
|
+
exists = true;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== "ENOENT") {
|
|
77
|
+
throw new CommandError("output_unavailable", "The output destination could not be checked. No generation was submitted.", 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (exists) {
|
|
81
|
+
throw new CommandError("output_exists", "The output destination already exists. Refusing to overwrite; no generation was submitted.", 1, false, "Use the existing output or choose a new --out path for intentionally new work.");
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const parent = path.dirname(path.resolve(target));
|
|
85
|
+
if (!(await stat(parent)).isDirectory())
|
|
86
|
+
throw new Error("not a directory");
|
|
87
|
+
await access(parent, constants.W_OK | constants.X_OK);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new CommandError("output_unavailable", "The output parent must be an existing writable directory. No generation was submitted.", 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function saveOutput(target, bytes) {
|
|
94
|
+
try {
|
|
95
|
+
await writeFile(target, bytes, { flag: "wx" });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new CommandError("local_output_failed", "The output could not be saved locally. Generation has already completed.", 1, true, "Fix the destination or choose a new path, then use dreamlayer download with the saved execution_id. Do not generate again.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function downloadOutput(api, url) {
|
|
102
|
+
try {
|
|
103
|
+
return await api.download(url);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new CommandError("download_failed", "The completed output could not be downloaded.", 5, true, "Retry dreamlayer download with the saved execution_id. Do not generate again.");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
52
109
|
function parseOptions(argv) {
|
|
53
110
|
const positional = [];
|
|
54
111
|
const options = {
|
|
112
|
+
maxCredits: 1,
|
|
113
|
+
frameCount: 12,
|
|
55
114
|
out: null,
|
|
56
115
|
image: null,
|
|
57
116
|
aspect: "1:1",
|
|
@@ -71,6 +130,42 @@ function parseOptions(argv) {
|
|
|
71
130
|
throw new UsageError("--out needs a file path");
|
|
72
131
|
options.out = value;
|
|
73
132
|
}
|
|
133
|
+
else if (token === "--action") {
|
|
134
|
+
const value = argv[++i];
|
|
135
|
+
if (value !== "walk" && value !== "run" && value !== "idle")
|
|
136
|
+
throw new UsageError("--action must be walk, run, or idle");
|
|
137
|
+
options.action = value;
|
|
138
|
+
}
|
|
139
|
+
else if (token === "--animation-prompt") {
|
|
140
|
+
const value = argv[++i];
|
|
141
|
+
if (!value?.trim() || [...value].length > 4000)
|
|
142
|
+
throw new UsageError("--animation-prompt needs 1–4000 characters");
|
|
143
|
+
options.animationPrompt = value;
|
|
144
|
+
}
|
|
145
|
+
else if (token === "--animation-mode") {
|
|
146
|
+
const value = argv[++i];
|
|
147
|
+
if (value !== "loop" && value !== "once")
|
|
148
|
+
throw new UsageError("--animation-mode must be loop or once");
|
|
149
|
+
options.animationMode = value;
|
|
150
|
+
}
|
|
151
|
+
else if (token === "--frame-size") {
|
|
152
|
+
const value = Number(argv[++i]);
|
|
153
|
+
if (![32, 64, 128, 256, 512, 720, 1080].includes(value))
|
|
154
|
+
throw new UsageError("--frame-size must be 32, 64, 128, 256, 512, 720 or 1080");
|
|
155
|
+
options.frameSize = value;
|
|
156
|
+
}
|
|
157
|
+
else if (token === "--frames") {
|
|
158
|
+
const value = Number(argv[++i]);
|
|
159
|
+
if (!Number.isInteger(value) || value < 7 || value > 100)
|
|
160
|
+
throw new UsageError("--frames must be an integer from 7 to 100");
|
|
161
|
+
options.frameCount = value;
|
|
162
|
+
}
|
|
163
|
+
else if (token === "--max-credits") {
|
|
164
|
+
const value = Number(argv[++i]);
|
|
165
|
+
if (!Number.isFinite(value) || value < 0.1 || value > 100)
|
|
166
|
+
throw new UsageError("--max-credits must be 0.1 to 100");
|
|
167
|
+
options.maxCredits = value;
|
|
168
|
+
}
|
|
74
169
|
else if (token === "--image") {
|
|
75
170
|
const value = argv[++i];
|
|
76
171
|
if (!value)
|
|
@@ -101,14 +196,11 @@ function parseOptions(argv) {
|
|
|
101
196
|
function client() {
|
|
102
197
|
const key = (process.env.DREAMLAYER_API_KEY ?? "").trim();
|
|
103
198
|
if (!key) {
|
|
104
|
-
throw new
|
|
105
|
-
" export DREAMLAYER_API_KEY=dlr_live_...\n" +
|
|
106
|
-
" Get a key at https://platform.dreamlayer.io");
|
|
199
|
+
throw new CommandError("authentication_failed", "DREAMLAYER_API_KEY is not set. Get a key at https://platform.dreamlayer.io", 2);
|
|
107
200
|
}
|
|
108
201
|
return new ManagedClient(key, (process.env.DREAMLAYER_API_URL ?? "https://api.dreamlayer.io").trim());
|
|
109
202
|
}
|
|
110
203
|
const MAX_SOURCE_BYTES = 200 * 1024 * 1024;
|
|
111
|
-
/** Upload a local file; the server owns RAW, EXIF, alpha, and resize normalization. */
|
|
112
204
|
async function upload(api, file) {
|
|
113
205
|
const resolved = path.resolve(file);
|
|
114
206
|
let fileStat;
|
|
@@ -116,30 +208,38 @@ async function upload(api, file) {
|
|
|
116
208
|
fileStat = await stat(resolved);
|
|
117
209
|
}
|
|
118
210
|
catch {
|
|
119
|
-
throw new
|
|
211
|
+
throw new CommandError("local_input_failed", "The local input file could not be read.", 1);
|
|
120
212
|
}
|
|
121
213
|
if (fileStat.size > MAX_SOURCE_BYTES) {
|
|
122
214
|
throw new UsageError(`${file} is ${Math.round(fileStat.size / 1024 / 1024)} MB; the limit is 200 MB`);
|
|
123
215
|
}
|
|
124
|
-
|
|
216
|
+
let blob;
|
|
217
|
+
try {
|
|
218
|
+
blob = await openAsBlob(resolved);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new CommandError("local_input_failed", "The local input file could not be read.", 1);
|
|
222
|
+
}
|
|
223
|
+
const asset = await api.uploadInput(blob, path.basename(resolved));
|
|
125
224
|
return asset.input_asset_id;
|
|
126
225
|
}
|
|
127
226
|
function defaultOut() {
|
|
128
227
|
return `dreamlayer-${Date.now()}.png`;
|
|
129
228
|
}
|
|
229
|
+
let recovery = {};
|
|
130
230
|
async function run(api, input, options) {
|
|
131
231
|
const progress = new Progress(!options.quiet && process.stderr.isTTY === true);
|
|
132
232
|
const idempotencyKey = options.idempotencyKey ?? randomUUID();
|
|
133
|
-
|
|
233
|
+
recovery = { idempotency_key: idempotencyKey };
|
|
234
|
+
const outcome = await consume(api.follow(input, { idempotencyKey }), progress);
|
|
235
|
+
recovery.execution_id = outcome.execution_id;
|
|
134
236
|
if (outcome.question) {
|
|
135
237
|
progress.stop();
|
|
136
238
|
if (options.json) {
|
|
137
|
-
process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
|
|
239
|
+
process.stdout.write(`${JSON.stringify({ ...outcome, idempotency_key: idempotencyKey }, null, 2)}\n`);
|
|
138
240
|
}
|
|
139
241
|
else {
|
|
140
242
|
process.stderr.write(`\nDreamLayer needs one more thing:\n ${outcome.question.text}\n\n`);
|
|
141
|
-
// The server's needs_input question always asks for an image, so point at the
|
|
142
|
-
// flag that can supply one rather than the bare form that will 422.
|
|
143
243
|
const wantsImage = /image/i.test(outcome.question.text);
|
|
144
244
|
process.stderr.write(`Answer it with:\n dreamlayer answer ${outcome.conversation_id} "your answer"` +
|
|
145
245
|
`${wantsImage ? " --image <file>" : ""}\n`);
|
|
@@ -153,22 +253,21 @@ async function run(api, input, options) {
|
|
|
153
253
|
if (terminal)
|
|
154
254
|
throw terminal;
|
|
155
255
|
}
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
256
|
+
if (outcome.status === "cancelled")
|
|
257
|
+
throw new CommandError("execution_cancelled", "The execution was cancelled.", 4);
|
|
258
|
+
if (outcome.status === "failed")
|
|
259
|
+
throw new CommandError("generation_failed", "The execution failed. Read canonical state for details.", 4);
|
|
260
|
+
throw new RecoveryRequiredError("The execution has no completed output yet. Read its saved state.");
|
|
161
261
|
}
|
|
162
262
|
progress.set("Downloading");
|
|
163
|
-
const bytes = await api
|
|
164
|
-
const target = options.out ?? defaultOut();
|
|
165
|
-
await
|
|
263
|
+
const bytes = await downloadOutput(api, outcome.asset.download_url);
|
|
264
|
+
const target = options.out ?? (input.operation === "sprite_sheet" ? `dreamlayer-${Date.now()}.zip` : defaultOut());
|
|
265
|
+
await saveOutput(target, bytes);
|
|
166
266
|
progress.stop();
|
|
167
267
|
if (options.json) {
|
|
168
|
-
process.stdout.write(`${JSON.stringify({ ...outcome, file: path.resolve(target) }, null, 2)}\n`);
|
|
268
|
+
process.stdout.write(`${JSON.stringify({ ...outcome, idempotency_key: idempotencyKey, file: path.resolve(target) }, null, 2)}\n`);
|
|
169
269
|
}
|
|
170
270
|
else {
|
|
171
|
-
// The path on stdout and nothing else, so `$(dreamlayer generate ...)` is the file.
|
|
172
271
|
process.stdout.write(`${target}\n`);
|
|
173
272
|
}
|
|
174
273
|
return 0;
|
|
@@ -178,13 +277,6 @@ async function imageCommand(operation, prompt, file, options) {
|
|
|
178
277
|
const inputAssetId = await upload(api, file);
|
|
179
278
|
return run(api, { prompt, operation, input_asset_id: inputAssetId }, options);
|
|
180
279
|
}
|
|
181
|
-
/**
|
|
182
|
-
* Point a user at the job they may have paid for.
|
|
183
|
-
*
|
|
184
|
-
* Without this, a timed-out upscale left nothing to go on: no id, no command, and no
|
|
185
|
-
* key-authenticated way to check a balance. "It might have charged you, good luck" is
|
|
186
|
-
* not an acceptable end state for a paid call.
|
|
187
|
-
*/
|
|
188
280
|
function recoveryHint(error) {
|
|
189
281
|
const id = error !== null && typeof error === "object"
|
|
190
282
|
? error.partialOutcome
|
|
@@ -192,25 +284,6 @@ function recoveryHint(error) {
|
|
|
192
284
|
: null;
|
|
193
285
|
return id ? `The job may still be running. Check it with:\n dreamlayer status ${id}\n` : "";
|
|
194
286
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Say so when this build and the server disagree about what exists.
|
|
197
|
-
*
|
|
198
|
-
* The MCP package solves this by asking the server at startup and shaping its tool
|
|
199
|
-
* schema from the answer. The CLI cannot: `ManagedOperation` is a compile-time union and
|
|
200
|
-
* `cutout` / `upscale` are compile-time commands, so deriving the list at runtime would
|
|
201
|
-
* buy consistency by giving up type safety at every call site.
|
|
202
|
-
*
|
|
203
|
-
* So it reports instead of adapting, and it does so HERE because `capabilities` is free,
|
|
204
|
-
* spends nothing, and is the command people are told to run first. Both directions are
|
|
205
|
-
* worth naming:
|
|
206
|
-
*
|
|
207
|
-
* - the server offers something this build cannot reach -> the user is missing a
|
|
208
|
-
* feature they are paying for and would never know
|
|
209
|
-
* - this build names something the server will not run -> the failure that shipped on
|
|
210
|
-
* 2026-08-21, where a call looked like a client bug rather than a version skew
|
|
211
|
-
*
|
|
212
|
-
* stderr, never stdout: `dreamlayer capabilities` is piped into jq.
|
|
213
|
-
*/
|
|
214
287
|
function warnIfOperationsDrifted(capabilities) {
|
|
215
288
|
const listed = capabilities.operations;
|
|
216
289
|
if (!Array.isArray(listed) || listed.some((o) => typeof o !== "string"))
|
|
@@ -218,7 +291,7 @@ function warnIfOperationsDrifted(capabilities) {
|
|
|
218
291
|
const server = new Set(listed);
|
|
219
292
|
const mine = new Set(KNOWN_OPERATIONS);
|
|
220
293
|
const serverOnly = [...server].filter((o) => !mine.has(o));
|
|
221
|
-
const clientOnly = [...mine].filter((o) => !server.has(o));
|
|
294
|
+
const clientOnly = [...mine].filter((o) => !server.has(o) && o !== "sprite_sheet");
|
|
222
295
|
if (serverOnly.length === 0 && clientOnly.length === 0)
|
|
223
296
|
return;
|
|
224
297
|
process.stderr.write("\nThis CLI and the server disagree about the operation list.\n");
|
|
@@ -244,12 +317,37 @@ async function main(argv) {
|
|
|
244
317
|
process.stdout.write(USAGE);
|
|
245
318
|
return command ? 0 : 1;
|
|
246
319
|
}
|
|
320
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
321
|
+
process.stdout.write(USAGE);
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
247
324
|
if (command === "--version" || command === "-v") {
|
|
248
325
|
process.stdout.write(`${PACKAGE_VERSION}\n`);
|
|
249
326
|
return 0;
|
|
250
327
|
}
|
|
251
328
|
const { positional, options } = parseOptions(rest);
|
|
329
|
+
if (["generate", "edit", "cutout", "upscale", "sprite", "answer"].includes(command)) {
|
|
330
|
+
options.out ??= command === "sprite" ? `dreamlayer-${Date.now()}.zip` : defaultOut();
|
|
331
|
+
await preflightOutput(options.out);
|
|
332
|
+
}
|
|
252
333
|
switch (command) {
|
|
334
|
+
case "sprite": {
|
|
335
|
+
if (options.action && options.animationPrompt)
|
|
336
|
+
throw new UsageError("Use either --action or --animation-prompt, not both");
|
|
337
|
+
const file = positional[0];
|
|
338
|
+
if (!file)
|
|
339
|
+
throw new UsageError("sprite needs a reference image");
|
|
340
|
+
const api = client();
|
|
341
|
+
const caps = await api.getCapabilities();
|
|
342
|
+
if (!Array.isArray(caps.operations) || !caps.operations.includes("sprite_sheet"))
|
|
343
|
+
throw new UsageError("sprite beta access is not enabled for this account");
|
|
344
|
+
if (!caps.sprite_pricing)
|
|
345
|
+
throw new UsageError("The server does not support configurable sprite pricing yet");
|
|
346
|
+
const price = spriteCreditPrice(options.frameCount);
|
|
347
|
+
if (options.maxCredits < price)
|
|
348
|
+
throw new UsageError(`Sprite jobs require ${price} credits. Set --max-credits to approve that amount.`);
|
|
349
|
+
return run(api, { operation: "sprite_sheet", input_asset_id: await upload(api, file), options: { ...(options.animationPrompt ? { animation_prompt: options.animationPrompt } : { action: options.action ?? "walk" }), ...(options.animationMode ? { animation_mode: options.animationMode } : {}), ...(options.frameSize ? { frame_size: options.frameSize } : {}), frame_count: options.frameCount }, max_credits: options.maxCredits }, options);
|
|
350
|
+
}
|
|
253
351
|
case "generate": {
|
|
254
352
|
const prompt = positional[0];
|
|
255
353
|
if (!prompt)
|
|
@@ -279,10 +377,6 @@ async function main(argv) {
|
|
|
279
377
|
if (!conversationId || !text)
|
|
280
378
|
throw new UsageError("answer needs a conversation id and text");
|
|
281
379
|
const api = client();
|
|
282
|
-
// A question that asks for an image cannot be answered with words alone: the
|
|
283
|
-
// server refuses a reply with no asset when the pending question required one.
|
|
284
|
-
// Without --image this command reached a clean 422 and the documented path
|
|
285
|
-
// dead-ended, telling the user to attach an image and offering no way to do it.
|
|
286
380
|
const input = options.image ? await upload(api, options.image) : undefined;
|
|
287
381
|
return run(api, {
|
|
288
382
|
respond: text,
|
|
@@ -290,6 +384,21 @@ async function main(argv) {
|
|
|
290
384
|
...(input ? { input_asset_id: input } : {}),
|
|
291
385
|
}, options);
|
|
292
386
|
}
|
|
387
|
+
case "download": {
|
|
388
|
+
const executionId = positional[0];
|
|
389
|
+
if (!executionId || positional.length !== 1 || !options.out)
|
|
390
|
+
throw new UsageError("download needs one execution id and --out <file>");
|
|
391
|
+
const api = client();
|
|
392
|
+
recovery = { execution_id: executionId };
|
|
393
|
+
const execution = await api.getExecution(executionId);
|
|
394
|
+
const assets = execution.image_job?.finished_assets;
|
|
395
|
+
if (execution.status !== "completed" || !Array.isArray(assets) || assets.length !== 1 || typeof assets[0]?.download_url !== "string")
|
|
396
|
+
throw new CommandError("output_not_ready", "The execution has no single finished asset. Check status before downloading.", 5, true, "Read the saved execution; do not generate again.");
|
|
397
|
+
const bytes = await downloadOutput(api, assets[0].download_url);
|
|
398
|
+
await saveOutput(options.out, bytes);
|
|
399
|
+
process.stdout.write(options.json ? `${JSON.stringify({ execution_id: executionId, file: path.resolve(options.out), bytes: bytes.length })}\n` : `${options.out}\n`);
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
293
402
|
case "status": {
|
|
294
403
|
const executionId = positional[0];
|
|
295
404
|
if (!executionId)
|
|
@@ -309,6 +418,9 @@ async function main(argv) {
|
|
|
309
418
|
process.stdout.write(`${balance.available} credits available ` +
|
|
310
419
|
`(${balance.promotional} promotional, ${balance.purchased} purchased)\n`);
|
|
311
420
|
}
|
|
421
|
+
if (!options.json && Math.round(balance.available * 10) > Math.round(balance.promotional * 10) + Math.round(balance.purchased * 10)) {
|
|
422
|
+
process.stdout.write("Use the available total for affordability. Funding balances are rounded down separately; stored fractions are preserved.\n");
|
|
423
|
+
}
|
|
312
424
|
return 0;
|
|
313
425
|
}
|
|
314
426
|
case "capabilities": {
|
|
@@ -326,6 +438,39 @@ main(process.argv.slice(2))
|
|
|
326
438
|
process.exitCode = code;
|
|
327
439
|
})
|
|
328
440
|
.catch((error) => {
|
|
441
|
+
const known = error instanceof CommandError ? error : error instanceof InputValidationError
|
|
442
|
+
? new CommandError("invalid_request", error.message, 1)
|
|
443
|
+
: error instanceof RecoveryRequiredError
|
|
444
|
+
? new CommandError("temporarily_unavailable", "Execution state is uncertain. Read saved state before retrying.", 5, true, "Use status and download for the saved execution. If no ID was received, replay identical inputs with the original idempotency key.") : null;
|
|
445
|
+
if (known) {
|
|
446
|
+
const partial = error.partialOutcome;
|
|
447
|
+
const identity = { ...recovery, ...(partial?.execution_id ? { execution_id: partial.execution_id } : {}) };
|
|
448
|
+
const envelope = { error: { code: "CLIENT_ERROR", reason: known.reason, message: known.message, retryable: known.retryable, request_id: null, guidance: known.guidance, ...identity } };
|
|
449
|
+
if (process.argv.slice(2).includes("--json"))
|
|
450
|
+
process.stderr.write(`${JSON.stringify(envelope)}\n`);
|
|
451
|
+
else {
|
|
452
|
+
process.stderr.write(`${known.message}\n${known.guidance ?? ""}\n`);
|
|
453
|
+
if (identity.execution_id)
|
|
454
|
+
process.stderr.write(`Execution: ${identity.execution_id}\n dreamlayer status ${identity.execution_id}\n dreamlayer download ${identity.execution_id} --out <new-file>\n`);
|
|
455
|
+
if (identity.idempotency_key)
|
|
456
|
+
process.stderr.write(`Idempotency key: ${identity.idempotency_key}\n`);
|
|
457
|
+
}
|
|
458
|
+
process.exitCode = known.exitCode;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (process.argv.slice(2).includes("--json")) {
|
|
462
|
+
const partial = error?.partialOutcome;
|
|
463
|
+
const temporary = error instanceof StreamIdleError || error instanceof UploadTimeoutError;
|
|
464
|
+
const envelope = error instanceof ApiError ? error.toPublicEnvelope() : {
|
|
465
|
+
error: { code: error instanceof UsageError ? "VALIDATION_FAILED" : temporary ? "SERVICE_UNAVAILABLE" : "INTERNAL_ERROR",
|
|
466
|
+
reason: error instanceof UsageError ? "invalid_request" : temporary ? "temporarily_unavailable" : "client_error",
|
|
467
|
+
message: error instanceof UsageError ? "Check command arguments and local input or output files; use --help." : temporary ? "The request timed out. Check the saved execution before retrying." : "The command could not complete. Check saved execution state and local output access.",
|
|
468
|
+
retryable: temporary, request_id: null },
|
|
469
|
+
};
|
|
470
|
+
process.stderr.write(`${JSON.stringify({ error: { ...envelope.error, ...recovery, ...(partial?.execution_id ? { execution_id: partial.execution_id } : {}) } })}\n`);
|
|
471
|
+
process.exitCode = error instanceof ApiError ? exitCodeFor(error) : temporary ? 5 : 1;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
329
474
|
if (error instanceof UsageError) {
|
|
330
475
|
process.stderr.write(`${error.message}\n`);
|
|
331
476
|
process.exitCode = 1;
|
|
@@ -346,11 +491,6 @@ main(process.argv.slice(2))
|
|
|
346
491
|
process.exitCode = exitCodeFor(error);
|
|
347
492
|
return;
|
|
348
493
|
}
|
|
349
|
-
// A stream that went silent is retryable, and it is the failure MOST likely to
|
|
350
|
-
// have been charged for: the server may have finished the job we stopped listening
|
|
351
|
-
// to. It reached this generic branch as a bare DOMException, so it exited 1 with no
|
|
352
|
-
// guidance, and the --idempotency-key advice that exists precisely to prevent
|
|
353
|
-
// double payment never printed on the one case that needs it.
|
|
354
494
|
if (error instanceof StreamIdleError) {
|
|
355
495
|
process.stderr.write(`${error.message}\n`);
|
|
356
496
|
process.stderr.write("Temporary. Retry with --idempotency-key to avoid paying twice.\n");
|
package/dist/client.d.ts
CHANGED
|
@@ -1,49 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hosted client for the DreamLayer Agent API.
|
|
3
|
-
*
|
|
4
|
-
* Deliberately a COPY of the same file in dreamlayer-mcp rather than a shared package.
|
|
5
|
-
* The alternative makes every capability three releases in strict order (client, then
|
|
6
|
-
* CLI, then MCP) instead of one release per repo. For a client over eight endpoints
|
|
7
|
-
* that trade is not worth the friction.
|
|
8
|
-
*
|
|
9
|
-
* Lifted from the DreamLayer runtime's TypeScript client, which is retired. The
|
|
10
|
-
* validation, error sanitisation, and origin hardening are kept verbatim because they
|
|
11
|
-
* were already correct; what changed is that the SSE reader is now wired to the hosted
|
|
12
|
-
* client instead of the dead local-proxy class, and that timeouts and an explicit
|
|
13
|
-
* redirect policy were added, which the original lacked.
|
|
14
|
-
*/
|
|
15
1
|
export type ManagedEventName = "started" | "thinking" | "progress" | "job" | "question" | "asset" | "done";
|
|
16
2
|
export type ManagedEvent = {
|
|
17
3
|
id: string | null;
|
|
18
4
|
event: ManagedEventName;
|
|
19
5
|
data: Record<string, unknown>;
|
|
20
6
|
};
|
|
21
|
-
|
|
22
|
-
* Every operation the Agent API can execute.
|
|
23
|
-
*
|
|
24
|
-
* REQUIRES the gateway build that added `operation` to ExecuteRequest. Against an older
|
|
25
|
-
* deployment this field is rejected with 422 extra_forbidden, because the request model
|
|
26
|
-
* is closed. That is a sequencing constraint, not a reason to drop it: naming the
|
|
27
|
-
* operation is what stops a cutout or an upscale being re-read from the prompt and
|
|
28
|
-
* coming back as a clarifying question instead of an image.
|
|
29
|
-
*
|
|
30
|
-
* SATISFIED 2026-08-21. prodbeta176 carries the field in the gateway AND the dispatch
|
|
31
|
-
* in the workflow engine, which had been split across two releases: the gateway
|
|
32
|
-
* accepted `operation` from prodbeta174 while the half that acts on it was still on
|
|
33
|
-
* prodbeta172, so naming an operation returned 200 and was then inferred from prose
|
|
34
|
-
* anyway. Confirmed against the deployment, not the source: the live /openapi.json
|
|
35
|
-
* advertises exactly these four in both ExecuteRequest and ImageJobCreate.
|
|
36
|
-
*/
|
|
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
|
-
*/
|
|
7
|
+
export declare const KNOWN_OPERATIONS: readonly ["text_to_image", "image_to_image", "background_remove", "upscale", "sprite_sheet"];
|
|
47
8
|
export type ManagedOperation = (typeof KNOWN_OPERATIONS)[number];
|
|
48
9
|
export type ManagedExecuteInput = {
|
|
49
10
|
prompt?: string;
|
|
@@ -51,9 +12,22 @@ export type ManagedExecuteInput = {
|
|
|
51
12
|
conversation_id?: string;
|
|
52
13
|
input_asset_id?: string;
|
|
53
14
|
aspect_ratio?: string;
|
|
54
|
-
/** Requires the gateway build that added it. See ManagedOperation. */
|
|
55
15
|
operation?: ManagedOperation;
|
|
16
|
+
options?: {
|
|
17
|
+
action?: "walk" | "run" | "idle";
|
|
18
|
+
animation_prompt?: string;
|
|
19
|
+
animation_mode?: "loop" | "once";
|
|
20
|
+
frame_count?: number;
|
|
21
|
+
frame_size?: 32 | 64 | 128 | 256 | 512 | 720 | 1080;
|
|
22
|
+
};
|
|
23
|
+
max_credits?: number;
|
|
56
24
|
};
|
|
25
|
+
export declare class InputValidationError extends Error {
|
|
26
|
+
}
|
|
27
|
+
export declare class RecoveryRequiredError extends Error {
|
|
28
|
+
}
|
|
29
|
+
export declare function spriteCreditPrice(frameCount: number): number;
|
|
30
|
+
export declare function validateSpriteInput(input: ManagedExecuteInput): void;
|
|
57
31
|
export type ManagedInputAsset = {
|
|
58
32
|
input_asset_id: string;
|
|
59
33
|
width: number;
|
|
@@ -66,7 +40,7 @@ export type ManagedExecution = {
|
|
|
66
40
|
status: string;
|
|
67
41
|
image_job: Record<string, unknown> | null;
|
|
68
42
|
};
|
|
69
|
-
export declare const PUBLIC_ERROR_REASONS: readonly ["invalid_request", "authentication_failed", "access_denied", "resource_not_found", "insufficient_credits", "conflict", "too_many_active_jobs", "rate_limited", "quota_exceeded", "content_refused", "temporarily_unavailable", "generation_failed"];
|
|
43
|
+
export declare const PUBLIC_ERROR_REASONS: readonly ["invalid_request", "authentication_failed", "access_denied", "resource_not_found", "insufficient_credits", "conflict", "too_many_active_jobs", "rate_limited", "quota_exceeded", "content_refused", "temporarily_unavailable", "generation_failed", "insufficient_frames"];
|
|
70
44
|
export type PublicErrorReason = (typeof PUBLIC_ERROR_REASONS)[number];
|
|
71
45
|
export declare class ApiError extends Error {
|
|
72
46
|
readonly status: number;
|
|
@@ -74,12 +48,8 @@ export declare class ApiError extends Error {
|
|
|
74
48
|
readonly requestId: string | null;
|
|
75
49
|
readonly code: string;
|
|
76
50
|
readonly reason: PublicErrorReason;
|
|
77
|
-
constructor(status: number, surface?: string, detail?: string | null,
|
|
78
|
-
/** Server-assigned id for this failure. The only handle support can search on. */
|
|
79
|
-
requestId?: string | null, code?: string | null, reason?: PublicErrorReason);
|
|
80
|
-
/** Whether retrying with the same idempotency key is worth doing. */
|
|
51
|
+
constructor(status: number, surface?: string, detail?: string | null, requestId?: string | null, code?: string | null, reason?: PublicErrorReason);
|
|
81
52
|
get retryable(): boolean;
|
|
82
|
-
/** The same stable fields exposed by REST and MCP, with no private response text. */
|
|
83
53
|
toPublicEnvelope(): Record<string, unknown>;
|
|
84
54
|
}
|
|
85
55
|
export type ManagedBalance = {
|
|
@@ -88,14 +58,6 @@ export type ManagedBalance = {
|
|
|
88
58
|
available: number;
|
|
89
59
|
credit_usd: "0.17";
|
|
90
60
|
};
|
|
91
|
-
/**
|
|
92
|
-
* A stream that went silent, as distinct from a slow one.
|
|
93
|
-
*
|
|
94
|
-
* Thrown as a real error type because the CLI's exit codes and its retry advice are
|
|
95
|
-
* driven off the error, and a bare DOMException from AbortSignal fell through to the
|
|
96
|
-
* generic handler: exit 1 with no guidance, on the one failure most likely to have been
|
|
97
|
-
* charged for. See ManagedApiError.retryable.
|
|
98
|
-
*/
|
|
99
61
|
export declare class StreamIdleError extends Error {
|
|
100
62
|
readonly idleMs: number;
|
|
101
63
|
constructor();
|
|
@@ -105,32 +67,19 @@ export declare class UploadTimeoutError extends Error {
|
|
|
105
67
|
}
|
|
106
68
|
export declare function uploadTimeoutMs(bytes: number): number;
|
|
107
69
|
export declare function managedBalance(value: unknown): ManagedBalance;
|
|
108
|
-
/** Convert a terminal job failure into the same safe contract used by HTTP errors. */
|
|
109
70
|
export declare function terminalExecutionError(execution: ManagedExecution): ApiError | null;
|
|
110
|
-
/**
|
|
111
|
-
* Validate one sanitized event against the published contract.
|
|
112
|
-
*
|
|
113
|
-
* Deliberately strict, including rejecting UNKNOWN fields: the point of the closed
|
|
114
|
-
* schema is that a field appearing where none is documented means something changed
|
|
115
|
-
* server-side that a client should not silently consume.
|
|
116
|
-
*/
|
|
117
71
|
export declare function managedEvent(event: string, id: string | null, value: unknown): ManagedEvent;
|
|
118
72
|
export declare class ManagedClient {
|
|
119
73
|
private readonly apiKey;
|
|
120
74
|
private readonly baseUrl;
|
|
121
75
|
private capabilitiesPromise;
|
|
122
76
|
constructor(apiKey: string, baseUrl?: string);
|
|
123
|
-
/**
|
|
124
|
-
* Run or continue an execution, yielding each validated event as it arrives.
|
|
125
|
-
*
|
|
126
|
-
* Streams rather than buffers. The Python server this replaces collected events into
|
|
127
|
-
* a list and threw the whole list away on overflow, taking the execution ID with it,
|
|
128
|
-
* so a caller could not even resume what it had already paid for.
|
|
129
|
-
*/
|
|
130
77
|
execute(input: ManagedExecuteInput, options: {
|
|
131
78
|
idempotencyKey: string;
|
|
132
79
|
}): AsyncGenerator<ManagedEvent>;
|
|
133
|
-
|
|
80
|
+
follow(input: ManagedExecuteInput, options: {
|
|
81
|
+
idempotencyKey: string;
|
|
82
|
+
}): AsyncGenerator<ManagedEvent>;
|
|
134
83
|
events(executionId: string, lastEventId?: string): AsyncGenerator<ManagedEvent>;
|
|
135
84
|
getCapabilities(): Promise<Record<string, unknown>>;
|
|
136
85
|
getExecution(executionId: string): Promise<ManagedExecution>;
|
|
@@ -139,11 +88,6 @@ export declare class ManagedClient {
|
|
|
139
88
|
listConversations(): Promise<Array<Record<string, unknown>>>;
|
|
140
89
|
deleteConversation(conversationId: string): Promise<void>;
|
|
141
90
|
uploadInput(file: Blob, filename?: string): Promise<ManagedInputAsset>;
|
|
142
|
-
/**
|
|
143
|
-
* Fetch a finished asset. Follows redirects on purpose: large images are served
|
|
144
|
-
* straight from storage rather than proxied, so a client that refuses redirects
|
|
145
|
-
* receives the redirect instead of the image.
|
|
146
|
-
*/
|
|
147
91
|
download(url: string): Promise<Uint8Array>;
|
|
148
92
|
private parse;
|
|
149
93
|
private fetchStream;
|
package/dist/client.js
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hosted client for the DreamLayer Agent API.
|
|
3
|
-
*
|
|
4
|
-
* Deliberately a COPY of the same file in dreamlayer-mcp rather than a shared package.
|
|
5
|
-
* The alternative makes every capability three releases in strict order (client, then
|
|
6
|
-
* CLI, then MCP) instead of one release per repo. For a client over eight endpoints
|
|
7
|
-
* that trade is not worth the friction.
|
|
8
|
-
*
|
|
9
|
-
* Lifted from the DreamLayer runtime's TypeScript client, which is retired. The
|
|
10
|
-
* validation, error sanitisation, and origin hardening are kept verbatim because they
|
|
11
|
-
* were already correct; what changed is that the SSE reader is now wired to the hosted
|
|
12
|
-
* client instead of the dead local-proxy class, and that timeouts and an explicit
|
|
13
|
-
* redirect policy were added, which the original lacked.
|
|
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
1
|
export const KNOWN_OPERATIONS = [
|
|
32
2
|
"text_to_image",
|
|
33
3
|
"image_to_image",
|
|
34
4
|
"background_remove",
|
|
35
5
|
"upscale",
|
|
6
|
+
"sprite_sheet",
|
|
36
7
|
];
|
|
8
|
+
export class InputValidationError extends Error {
|
|
9
|
+
}
|
|
10
|
+
export class RecoveryRequiredError extends Error {
|
|
11
|
+
}
|
|
12
|
+
export function spriteCreditPrice(frameCount) {
|
|
13
|
+
if (!Number.isInteger(frameCount) || frameCount < 7 || frameCount > 100)
|
|
14
|
+
throw new InputValidationError("frame_count must be an integer from 7 to 100");
|
|
15
|
+
const cents = 14 * Math.min(frameCount, 14) + 7 * Math.max(frameCount - 14, 0);
|
|
16
|
+
return Math.ceil(cents * 10 / 17) / 10;
|
|
17
|
+
}
|
|
18
|
+
export function validateSpriteInput(input) {
|
|
19
|
+
if (input.operation !== "sprite_sheet")
|
|
20
|
+
return;
|
|
21
|
+
const options = input.options;
|
|
22
|
+
if (!options || (options.action === undefined) === (options.animation_prompt === undefined))
|
|
23
|
+
throw new InputValidationError("Sprite requests require exactly one of options.action or options.animation_prompt");
|
|
24
|
+
if (options.action !== undefined && !["walk", "run", "idle"].includes(options.action))
|
|
25
|
+
throw new InputValidationError("Invalid sprite preset");
|
|
26
|
+
if (options.animation_prompt !== undefined && (typeof options.animation_prompt !== "string" || !options.animation_prompt.trim() || [...options.animation_prompt].length > 4000))
|
|
27
|
+
throw new InputValidationError("animation_prompt must contain 1–4000 characters");
|
|
28
|
+
if (options.animation_mode !== undefined && !["loop", "once"].includes(options.animation_mode))
|
|
29
|
+
throw new InputValidationError("animation_mode must be loop or once");
|
|
30
|
+
const price = spriteCreditPrice(options.frame_count ?? 12);
|
|
31
|
+
if (options.frame_size !== undefined && ![32, 64, 128, 256, 512, 720, 1080].includes(options.frame_size))
|
|
32
|
+
throw new InputValidationError("frame_size must be 32, 64, 128, 256, 512, 720 or 1080");
|
|
33
|
+
if (typeof input.max_credits !== "number" || !Number.isFinite(input.max_credits) || input.max_credits < price || input.max_credits > 100)
|
|
34
|
+
throw new InputValidationError(`This sprite request requires ${price} credits. Supply a sufficient max_credits limit.`);
|
|
35
|
+
}
|
|
37
36
|
const DIRECT_INPUT_BYTES = 20 * 1024 * 1024;
|
|
38
37
|
const RASTER_INPUT_SUFFIXES = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
39
38
|
const LEGACY_INPUT_SUFFIXES = new Set([
|
|
@@ -54,6 +53,7 @@ export const PUBLIC_ERROR_REASONS = [
|
|
|
54
53
|
"content_refused",
|
|
55
54
|
"temporarily_unavailable",
|
|
56
55
|
"generation_failed",
|
|
56
|
+
"insufficient_frames",
|
|
57
57
|
];
|
|
58
58
|
const PUBLIC_ERROR_SPECS = {
|
|
59
59
|
invalid_request: { message: "The request could not be validated.", retryable: false },
|
|
@@ -82,6 +82,7 @@ const PUBLIC_ERROR_SPECS = {
|
|
|
82
82
|
message: "The service is temporarily unavailable. Please try again.",
|
|
83
83
|
retryable: true,
|
|
84
84
|
},
|
|
85
|
+
insufficient_frames: { message: "Not enough distinct animation frames. Try a lower frame count.", retryable: false },
|
|
85
86
|
generation_failed: { message: "Image generation failed.", retryable: false },
|
|
86
87
|
};
|
|
87
88
|
const PUBLIC_ERROR_REASON_SET = new Set(PUBLIC_ERROR_REASONS);
|
|
@@ -123,6 +124,7 @@ function defaultCode(reason) {
|
|
|
123
124
|
content_refused: "CONTENT_REFUSED",
|
|
124
125
|
temporarily_unavailable: "SERVICE_UNAVAILABLE",
|
|
125
126
|
generation_failed: "INTERNAL_ERROR",
|
|
127
|
+
insufficient_frames: "INSUFFICIENT_FRAMES",
|
|
126
128
|
};
|
|
127
129
|
return codes[reason];
|
|
128
130
|
}
|
|
@@ -140,6 +142,7 @@ function statusForReason(reason) {
|
|
|
140
142
|
content_refused: 422,
|
|
141
143
|
temporarily_unavailable: 503,
|
|
142
144
|
generation_failed: 500,
|
|
145
|
+
insufficient_frames: 422,
|
|
143
146
|
};
|
|
144
147
|
return statuses[reason];
|
|
145
148
|
}
|
|
@@ -153,9 +156,7 @@ export class ApiError extends Error {
|
|
|
153
156
|
requestId;
|
|
154
157
|
code;
|
|
155
158
|
reason;
|
|
156
|
-
constructor(status, surface = "DreamLayer Agent API", detail = null,
|
|
157
|
-
/** Server-assigned id for this failure. The only handle support can search on. */
|
|
158
|
-
requestId = null, code = null, reason = reasonForStatus(status)) {
|
|
159
|
+
constructor(status, surface = "DreamLayer Agent API", detail = null, requestId = null, code = null, reason = reasonForStatus(status)) {
|
|
159
160
|
const safeDetail = detail ?? PUBLIC_ERROR_SPECS[reason].message;
|
|
160
161
|
super(`${safeDetail || `${surface} request failed (${status})`}${requestId ? ` (request ${requestId})` : ""}`);
|
|
161
162
|
this.status = status;
|
|
@@ -165,11 +166,9 @@ export class ApiError extends Error {
|
|
|
165
166
|
this.code = code ?? defaultCode(reason);
|
|
166
167
|
this.reason = reason;
|
|
167
168
|
}
|
|
168
|
-
/** Whether retrying with the same idempotency key is worth doing. */
|
|
169
169
|
get retryable() {
|
|
170
170
|
return PUBLIC_ERROR_SPECS[this.reason].retryable;
|
|
171
171
|
}
|
|
172
|
-
/** The same stable fields exposed by REST and MCP, with no private response text. */
|
|
173
172
|
toPublicEnvelope() {
|
|
174
173
|
return {
|
|
175
174
|
error: {
|
|
@@ -182,14 +181,6 @@ export class ApiError extends Error {
|
|
|
182
181
|
};
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
|
-
/**
|
|
186
|
-
* A stream that went silent, as distinct from a slow one.
|
|
187
|
-
*
|
|
188
|
-
* Thrown as a real error type because the CLI's exit codes and its retry advice are
|
|
189
|
-
* driven off the error, and a bare DOMException from AbortSignal fell through to the
|
|
190
|
-
* generic handler: exit 1 with no guidance, on the one failure most likely to have been
|
|
191
|
-
* charged for. See ManagedApiError.retryable.
|
|
192
|
-
*/
|
|
193
184
|
export class StreamIdleError extends Error {
|
|
194
185
|
idleMs = STREAM_IDLE_TIMEOUT_MS;
|
|
195
186
|
constructor() {
|
|
@@ -205,9 +196,6 @@ export class UploadTimeoutError extends Error {
|
|
|
205
196
|
}
|
|
206
197
|
}
|
|
207
198
|
const ERROR_BODY_LIMIT = 16 * 1024;
|
|
208
|
-
/**
|
|
209
|
-
* A plain request: send, get a body back. Bounded work, so a total cap is right.
|
|
210
|
-
*/
|
|
211
199
|
const REQUEST_TIMEOUT_MS = 130_000;
|
|
212
200
|
const UPLOAD_MIN_BYTES_PER_SECOND = 256 * 1024;
|
|
213
201
|
const UPLOAD_MAX_TIMEOUT_MS = 15 * 60_000;
|
|
@@ -218,29 +206,7 @@ export function uploadTimeoutMs(bytes) {
|
|
|
218
206
|
}
|
|
219
207
|
return Math.min(Math.max(REQUEST_TIMEOUT_MS, 60_000 + Math.ceil(bytes / UPLOAD_MIN_BYTES_PER_SECOND) * 1000), UPLOAD_MAX_TIMEOUT_MS);
|
|
220
208
|
}
|
|
221
|
-
/**
|
|
222
|
-
* A STREAM is different, and conflating the two shipped a broken `upscale`.
|
|
223
|
-
*
|
|
224
|
-
* AbortSignal.timeout() caps TOTAL duration. An upscale of a 2048px image takes about
|
|
225
|
-
* 150s server-side, so a 130s total cap aborted every single one: a command that failed
|
|
226
|
-
* 100% of the time on a normal input, while the server had done the work and charged
|
|
227
|
-
* for it.
|
|
228
|
-
*
|
|
229
|
-
* Raising the number would fix upscale and break again on the next slower operation.
|
|
230
|
-
* The right question is not "how long may a job take" (unknowable, and it is the
|
|
231
|
-
* server's business) but "how long may we hear NOTHING before the connection is dead".
|
|
232
|
-
* The server sends `: keepalive` comments precisely so a client can tell those apart;
|
|
233
|
-
* a total-duration timeout throws that information away.
|
|
234
|
-
*
|
|
235
|
-
* So: idle timeout, reset on every byte received.
|
|
236
|
-
*/
|
|
237
209
|
const STREAM_IDLE_DEFAULT_MS = 90_000;
|
|
238
|
-
/**
|
|
239
|
-
* Overridable, within bounds. Two honest reasons rather than one: a test cannot wait
|
|
240
|
-
* 90 seconds to prove a timeout fires, and a user on a genuinely bad link may need
|
|
241
|
-
* longer. Clamped so a typo cannot disable the guard entirely or set it to zero, and
|
|
242
|
-
* an unparseable value falls back rather than becoming NaN, which would abort instantly.
|
|
243
|
-
*/
|
|
244
210
|
const STREAM_IDLE_TIMEOUT_MS = (() => {
|
|
245
211
|
const raw = Number(process.env.DREAMLAYER_STREAM_IDLE_MS);
|
|
246
212
|
if (!Number.isFinite(raw) || raw <= 0)
|
|
@@ -284,16 +250,12 @@ async function apiError(response, surface = "DreamLayer Agent API") {
|
|
|
284
250
|
const raw = await boundedErrorBody(response);
|
|
285
251
|
const parsed = raw ? JSON.parse(raw) : null;
|
|
286
252
|
if (isRecord(parsed) && isRecord(parsed.error)) {
|
|
287
|
-
// Treat only the closed reason/code/id fields as data. The local message table
|
|
288
|
-
// deliberately ignores arbitrary remote detail so a private upstream response
|
|
289
|
-
// cannot leak through a client even if a server regression serializes it.
|
|
290
253
|
reason = publicReason(parsed.error.reason) ?? reason;
|
|
291
254
|
code = publicIdentifier(parsed.error.code);
|
|
292
255
|
requestId = publicRequestId(parsed.error.request_id) ?? requestId;
|
|
293
256
|
}
|
|
294
257
|
}
|
|
295
258
|
catch {
|
|
296
|
-
// A malformed or oversized response still becomes a closed, status-derived error.
|
|
297
259
|
}
|
|
298
260
|
return new ApiError(response.status, surface, null, requestId, code, reason);
|
|
299
261
|
}
|
|
@@ -318,14 +280,17 @@ export function managedBalance(value) {
|
|
|
318
280
|
throw new Error("Invalid DreamLayer balance response");
|
|
319
281
|
}
|
|
320
282
|
for (const field of ["promotional", "purchased", "available"]) {
|
|
321
|
-
if (!Number.
|
|
283
|
+
if (typeof value[field] !== "number" || !Number.isFinite(value[field]) || Number(value[field]) < 0 || Number(value[field]) > Number.MAX_SAFE_INTEGER / 10) {
|
|
322
284
|
throw new Error("Invalid DreamLayer balance response");
|
|
323
285
|
}
|
|
324
286
|
}
|
|
325
287
|
if (value.credit_usd !== "0.17" ||
|
|
326
|
-
Number(value.available)
|
|
288
|
+
Math.round(Number(value.available) * 10) < Math.round(Number(value.promotional) * 10) + Math.round(Number(value.purchased) * 10) ||
|
|
289
|
+
Math.round(Number(value.available) * 10) > Math.round(Number(value.promotional) * 10) + Math.round(Number(value.purchased) * 10) + 1) {
|
|
327
290
|
throw new Error("Invalid DreamLayer balance response");
|
|
328
291
|
}
|
|
292
|
+
if ([value.promotional, value.purchased, value.available].some(v => Math.abs(Number(v) * 10 - Math.round(Number(v) * 10)) > 1e-7))
|
|
293
|
+
throw new Error("Invalid DreamLayer balance response");
|
|
329
294
|
return {
|
|
330
295
|
promotional: Number(value.promotional),
|
|
331
296
|
purchased: Number(value.purchased),
|
|
@@ -333,7 +298,6 @@ export function managedBalance(value) {
|
|
|
333
298
|
credit_usd: "0.17",
|
|
334
299
|
};
|
|
335
300
|
}
|
|
336
|
-
/** Convert a terminal job failure into the same safe contract used by HTTP errors. */
|
|
337
301
|
export function terminalExecutionError(execution) {
|
|
338
302
|
if (!isRecord(execution.image_job) || !isRecord(execution.image_job.sanitized_error)) {
|
|
339
303
|
return execution.status === "failed" ? new ApiError(500) : null;
|
|
@@ -342,13 +306,6 @@ export function terminalExecutionError(execution) {
|
|
|
342
306
|
const reason = publicReason(error.reason) ?? "generation_failed";
|
|
343
307
|
return new ApiError(statusForReason(reason), "DreamLayer execution", null, publicRequestId(error.request_id), publicIdentifier(error.code), reason);
|
|
344
308
|
}
|
|
345
|
-
/**
|
|
346
|
-
* Validate one sanitized event against the published contract.
|
|
347
|
-
*
|
|
348
|
-
* Deliberately strict, including rejecting UNKNOWN fields: the point of the closed
|
|
349
|
-
* schema is that a field appearing where none is documented means something changed
|
|
350
|
-
* server-side that a client should not silently consume.
|
|
351
|
-
*/
|
|
352
309
|
export function managedEvent(event, id, value) {
|
|
353
310
|
if (!MANAGED_EVENT_NAMES.has(event) || !isRecord(value)) {
|
|
354
311
|
throw new Error("Invalid DreamLayer managed event");
|
|
@@ -417,15 +374,12 @@ export function managedEvent(event, id, value) {
|
|
|
417
374
|
}
|
|
418
375
|
return { id, event: event, data };
|
|
419
376
|
}
|
|
420
|
-
/** Parse a server-sent-event body into blocks. Handles multi-line data and comments. */
|
|
421
377
|
async function* readEventStream(body, onBytes) {
|
|
422
378
|
const reader = body.getReader();
|
|
423
379
|
const decoder = new TextDecoder();
|
|
424
380
|
let buffer = "";
|
|
425
381
|
for (;;) {
|
|
426
382
|
const { value, done } = await reader.read();
|
|
427
|
-
// Any byte at all, including a `: keepalive` comment that parses to no event,
|
|
428
|
-
// proves the connection is alive. That is the signal the idle timer needs.
|
|
429
383
|
if (!done)
|
|
430
384
|
onBytes?.();
|
|
431
385
|
buffer += decoder.decode(value, { stream: !done });
|
|
@@ -454,18 +408,6 @@ async function* readEventStream(body, onBytes) {
|
|
|
454
408
|
return;
|
|
455
409
|
}
|
|
456
410
|
}
|
|
457
|
-
/**
|
|
458
|
-
* Hosts this client will send a bearer key to.
|
|
459
|
-
*
|
|
460
|
-
* In August a build moved the endpoint default from api.dreamlayer.io to the bare
|
|
461
|
-
* marketing apex, and every request carried Authorization there for two days. The
|
|
462
|
-
* origin passed every cleanliness check below, because those check the SHAPE of a URL
|
|
463
|
-
* and never which host it names. An allowlist is the only thing that catches a host
|
|
464
|
-
* swap, which is why the gateway now has a pinned-origin test and why this mirrors it.
|
|
465
|
-
*
|
|
466
|
-
* DREAMLAYER_API_URL still works for a genuinely different deployment: set
|
|
467
|
-
* DREAMLAYER_ALLOW_ANY_HOST=1 alongside it and accept that you are vouching for the host.
|
|
468
|
-
*/
|
|
469
411
|
const ALLOWED_HOSTS = new Set(["api.dreamlayer.io"]);
|
|
470
412
|
function isLoopback(hostname) {
|
|
471
413
|
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]";
|
|
@@ -510,14 +452,8 @@ export class ManagedClient {
|
|
|
510
452
|
throw new Error("DREAMLAYER_API_KEY is required");
|
|
511
453
|
this.baseUrl = managedOrigin(baseUrl);
|
|
512
454
|
}
|
|
513
|
-
/**
|
|
514
|
-
* Run or continue an execution, yielding each validated event as it arrives.
|
|
515
|
-
*
|
|
516
|
-
* Streams rather than buffers. The Python server this replaces collected events into
|
|
517
|
-
* a list and threw the whole list away on overflow, taking the execution ID with it,
|
|
518
|
-
* so a caller could not even resume what it had already paid for.
|
|
519
|
-
*/
|
|
520
455
|
async *execute(input, options) {
|
|
456
|
+
validateSpriteInput(input);
|
|
521
457
|
const stream = await this.fetchStream("/v1/execute", {
|
|
522
458
|
method: "POST",
|
|
523
459
|
headers: {
|
|
@@ -529,7 +465,53 @@ export class ManagedClient {
|
|
|
529
465
|
});
|
|
530
466
|
yield* this.parse(stream);
|
|
531
467
|
}
|
|
532
|
-
|
|
468
|
+
async *follow(input, options) {
|
|
469
|
+
let executionId;
|
|
470
|
+
let cursor;
|
|
471
|
+
let stream = this.execute(input, options);
|
|
472
|
+
const deadline = Date.now() + 16 * 60_000;
|
|
473
|
+
let failures = 0;
|
|
474
|
+
while (Date.now() < deadline) {
|
|
475
|
+
try {
|
|
476
|
+
for await (const event of stream) {
|
|
477
|
+
if (event.event === "started")
|
|
478
|
+
executionId = String(event.data.execution_id);
|
|
479
|
+
if (event.id)
|
|
480
|
+
cursor = event.id;
|
|
481
|
+
yield event;
|
|
482
|
+
if (event.event === "done")
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
failures = 0;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
if (error instanceof StreamIdleError)
|
|
489
|
+
throw error;
|
|
490
|
+
if (error instanceof InputValidationError || (error instanceof ApiError && ![429, 500, 502, 503, 504].includes(error.status)))
|
|
491
|
+
throw error;
|
|
492
|
+
if (!executionId && error instanceof ApiError)
|
|
493
|
+
throw error;
|
|
494
|
+
if (!executionId || ++failures > 5)
|
|
495
|
+
throw new RecoveryRequiredError("Execution state is uncertain. Read saved state before retrying.");
|
|
496
|
+
}
|
|
497
|
+
if (!executionId)
|
|
498
|
+
throw new RecoveryRequiredError("Execution stream ended before an identifier was received; reuse your idempotency key.");
|
|
499
|
+
const state = await this.getExecution(executionId);
|
|
500
|
+
if (["completed", "failed", "cancelled"].includes(state.status)) {
|
|
501
|
+
if (state.status === "completed") {
|
|
502
|
+
const assets = state.image_job?.finished_assets;
|
|
503
|
+
if (!Array.isArray(assets) || assets.length !== 1 || typeof assets[0]?.download_url !== "string")
|
|
504
|
+
throw new RecoveryRequiredError(`Execution ${executionId} has no downloadable asset yet.`);
|
|
505
|
+
yield managedEvent("asset", null, { asset_id: assets[0].asset_id, download_url: assets[0].download_url });
|
|
506
|
+
}
|
|
507
|
+
yield managedEvent("done", null, { status: state.status });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(5000, 500 * 2 ** failures)));
|
|
511
|
+
stream = this.events(executionId, cursor);
|
|
512
|
+
}
|
|
513
|
+
throw new RecoveryRequiredError(`Execution ${executionId ?? "unknown"} is still active. Use status to resume; the job has not been cancelled.`);
|
|
514
|
+
}
|
|
533
515
|
async *events(executionId, lastEventId) {
|
|
534
516
|
const headers = { Accept: "text/event-stream" };
|
|
535
517
|
if (lastEventId)
|
|
@@ -539,8 +521,6 @@ export class ManagedClient {
|
|
|
539
521
|
}
|
|
540
522
|
async getCapabilities() {
|
|
541
523
|
this.capabilitiesPromise ??= this.request("/v1/capabilities").catch((error) => {
|
|
542
|
-
// Cache a successful contract for the process, but never pin a transient
|
|
543
|
-
// capabilities failure as a permanent result.
|
|
544
524
|
this.capabilitiesPromise = null;
|
|
545
525
|
throw error;
|
|
546
526
|
});
|
|
@@ -624,17 +604,7 @@ export class ManagedClient {
|
|
|
624
604
|
body.append("file", file, filename);
|
|
625
605
|
return this.request("/v1/input-assets", { method: "POST", body });
|
|
626
606
|
}
|
|
627
|
-
/**
|
|
628
|
-
* Fetch a finished asset. Follows redirects on purpose: large images are served
|
|
629
|
-
* straight from storage rather than proxied, so a client that refuses redirects
|
|
630
|
-
* receives the redirect instead of the image.
|
|
631
|
-
*/
|
|
632
607
|
async download(url) {
|
|
633
|
-
// Only attach the key when the URL is OUR origin. download_url arrives in the event
|
|
634
|
-
// stream and is validated as text, so a wrong or hostile value would otherwise walk
|
|
635
|
-
// off with a live credential on the very first request. Node strips Authorization
|
|
636
|
-
// across a cross-origin redirect, so the hop to signed storage stays safe either way,
|
|
637
|
-
// and storage URLs are pre-signed and need no header from us.
|
|
638
608
|
const sameOrigin = (() => {
|
|
639
609
|
try {
|
|
640
610
|
return new URL(url).origin === this.baseUrl;
|
|
@@ -664,9 +634,6 @@ export class ManagedClient {
|
|
|
664
634
|
}
|
|
665
635
|
}
|
|
666
636
|
finally {
|
|
667
|
-
// Also runs when the consumer breaks out of the loop early, which the CLI does
|
|
668
|
-
// as soon as it sees a terminal event. Without this the timer keeps the process
|
|
669
|
-
// alive for another idle period.
|
|
670
637
|
finish();
|
|
671
638
|
}
|
|
672
639
|
}
|
|
@@ -674,9 +641,6 @@ export class ManagedClient {
|
|
|
674
641
|
const headers = new Headers(init.headers);
|
|
675
642
|
headers.set("Authorization", `Bearer ${this.apiKey}`);
|
|
676
643
|
headers.set("DreamLayer-Version", "1");
|
|
677
|
-
// One controller for the whole stream, armed on an IDLE clock that every received
|
|
678
|
-
// byte pushes forward. The signal has to outlive the fetch() call: aborting only
|
|
679
|
-
// the handshake would leave a stalled body hanging forever.
|
|
680
644
|
const controller = new AbortController();
|
|
681
645
|
let timer;
|
|
682
646
|
const keepAlive = () => {
|
package/dist/render.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal rendering of an execution.
|
|
3
|
-
*
|
|
4
|
-
* Two modes, decided by whether stdout is a TTY and whether --json was passed. A CLI
|
|
5
|
-
* that only prints prose cannot be piped into anything, and one that only prints JSON
|
|
6
|
-
* is miserable to watch. Progress goes to stderr so `dreamlayer generate ... | jq`
|
|
7
|
-
* works without the spinner corrupting the pipe.
|
|
8
|
-
*/
|
|
9
1
|
import type { ManagedEvent } from "./client.js";
|
|
10
2
|
export type Outcome = {
|
|
11
3
|
execution_id: string | null;
|
package/dist/render.js
CHANGED
|
@@ -34,7 +34,6 @@ export class Progress {
|
|
|
34
34
|
process.stderr.write(`${final}\n`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
/** Human wording for each event. `thinking` carries no text by contract. */
|
|
38
37
|
function describe(event) {
|
|
39
38
|
switch (event.event) {
|
|
40
39
|
case "started":
|
|
@@ -97,13 +96,6 @@ export async function consume(stream, progress, onEvent) {
|
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
catch (error) {
|
|
100
|
-
// `started` arrives within seconds and carries the execution id. When the stream
|
|
101
|
-
// later dies, that id is the only way a user can find a job they may already have
|
|
102
|
-
// paid for, and it was being discarded along with the exception.
|
|
103
|
-
//
|
|
104
|
-
// Attached to the error rather than wrapped in a new one: the top-level handler
|
|
105
|
-
// branches on `instanceof ApiError`, and a wrapper would silently defeat that
|
|
106
|
-
// while looking tidier.
|
|
107
99
|
if (error !== null && typeof error === "object") {
|
|
108
100
|
error.partialOutcome = outcome;
|
|
109
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamlayer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-beta.2",
|
|
4
4
|
"description": "Generate and edit images from your terminal, over local files, with one API key.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,11 +31,16 @@
|
|
|
31
31
|
"cli",
|
|
32
32
|
"dreamlayer",
|
|
33
33
|
"ai",
|
|
34
|
-
"image-editing"
|
|
34
|
+
"image-editing",
|
|
35
|
+
"sprite-sheet",
|
|
36
|
+
"automation"
|
|
35
37
|
],
|
|
36
38
|
"repository": {
|
|
37
39
|
"type": "git",
|
|
38
40
|
"url": "git+https://github.com/TheDesignFounder/dreamlayer-cli.git"
|
|
39
41
|
},
|
|
40
|
-
"homepage": "https://docs.dreamlayer.io/cli"
|
|
42
|
+
"homepage": "https://docs.dreamlayer.io/cli",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"tag": "beta"
|
|
45
|
+
}
|
|
41
46
|
}
|