pixelmuse 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/LICENSE +88 -0
- package/README.md +274 -0
- package/dist/chunk-743CKPHW.js +60 -0
- package/dist/chunk-7ARYEFAH.js +52 -0
- package/dist/chunk-MZZY4JXW.js +63 -0
- package/dist/chunk-ZVJQFWUI.js +272 -0
- package/dist/cli.js +531 -0
- package/dist/config-RMVFR3GN.js +13 -0
- package/dist/image-2H3GUQI6.js +16 -0
- package/dist/mcp/server.js +133 -0
- package/dist/tui.js +1274 -0
- package/package.json +94 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PATHS,
|
|
4
|
+
ensureDirs
|
|
5
|
+
} from "./chunk-7ARYEFAH.js";
|
|
6
|
+
|
|
7
|
+
// src/core/client.ts
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
|
|
10
|
+
// src/core/types.ts
|
|
11
|
+
var ApiError = class extends Error {
|
|
12
|
+
constructor(message, status, code, rateLimitRemaining, retryAfter) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.rateLimitRemaining = rateLimitRemaining;
|
|
17
|
+
this.retryAfter = retryAfter;
|
|
18
|
+
this.name = "ApiError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/core/client.ts
|
|
23
|
+
var require2 = createRequire(import.meta.url);
|
|
24
|
+
var CLI_VERSION = require2("../../package.json").version;
|
|
25
|
+
var BASE_URL = "https://www.pixelmuse.studio/api/v1";
|
|
26
|
+
var CLIENT_HEADERS = {
|
|
27
|
+
"User-Agent": `pixelmuse-cli/${CLI_VERSION}`,
|
|
28
|
+
"X-Client-Version": CLI_VERSION,
|
|
29
|
+
"X-Client-Platform": process.platform
|
|
30
|
+
};
|
|
31
|
+
var PixelmuseClient = class {
|
|
32
|
+
apiKey;
|
|
33
|
+
constructor(apiKey) {
|
|
34
|
+
this.apiKey = apiKey;
|
|
35
|
+
}
|
|
36
|
+
async request(path, options = {}) {
|
|
37
|
+
const url = `${BASE_URL}${path}`;
|
|
38
|
+
const headers = {
|
|
39
|
+
...CLIENT_HEADERS,
|
|
40
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
...options.headers
|
|
43
|
+
};
|
|
44
|
+
const res = await fetch(url, { ...options, headers });
|
|
45
|
+
const rateLimitRemaining = res.headers.get("X-RateLimit-Remaining");
|
|
46
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
let message = `HTTP ${res.status}`;
|
|
49
|
+
let code;
|
|
50
|
+
try {
|
|
51
|
+
const body = await res.json();
|
|
52
|
+
message = body.error ?? message;
|
|
53
|
+
code = body.code;
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
throw new ApiError(
|
|
57
|
+
message,
|
|
58
|
+
res.status,
|
|
59
|
+
code,
|
|
60
|
+
rateLimitRemaining ? Number(rateLimitRemaining) : void 0,
|
|
61
|
+
retryAfter ? Number(retryAfter) : void 0
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return await res.json();
|
|
65
|
+
}
|
|
66
|
+
/** Generate an image. Uses Prefer: wait=55 for sync response. */
|
|
67
|
+
async generate(req) {
|
|
68
|
+
return this.request("/images", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { Prefer: "wait=55" },
|
|
71
|
+
body: JSON.stringify(req)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Get a single generation by ID */
|
|
75
|
+
async getGeneration(id) {
|
|
76
|
+
return this.request(`/images/${encodeURIComponent(id)}`);
|
|
77
|
+
}
|
|
78
|
+
/** List generations with cursor pagination */
|
|
79
|
+
async listGenerations(params) {
|
|
80
|
+
const query = new URLSearchParams();
|
|
81
|
+
if (params?.cursor) query.set("cursor", params.cursor);
|
|
82
|
+
if (params?.limit) query.set("limit", String(params.limit));
|
|
83
|
+
if (params?.status) query.set("status", params.status);
|
|
84
|
+
const qs = query.toString();
|
|
85
|
+
return this.request(`/images${qs ? `?${qs}` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
/** Delete a generation */
|
|
88
|
+
async deleteGeneration(id) {
|
|
89
|
+
await this.request(`/images/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
90
|
+
}
|
|
91
|
+
/** Get account info */
|
|
92
|
+
async getAccount() {
|
|
93
|
+
return this.request("/account");
|
|
94
|
+
}
|
|
95
|
+
/** Get usage stats for a date range */
|
|
96
|
+
async getUsage(params) {
|
|
97
|
+
const query = new URLSearchParams({ start: params.start, end: params.end });
|
|
98
|
+
return this.request(`/account/usage?${query}`);
|
|
99
|
+
}
|
|
100
|
+
/** Create a Stripe checkout session */
|
|
101
|
+
async createCheckout(req) {
|
|
102
|
+
return this.request("/billing/checkout", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
body: JSON.stringify(req)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// ── Static methods (no auth required) ──────────────────────────────
|
|
108
|
+
/** List all available models */
|
|
109
|
+
static async listModels() {
|
|
110
|
+
const res = await fetch(`${BASE_URL}/models`, { headers: CLIENT_HEADERS });
|
|
111
|
+
if (!res.ok) throw new ApiError(`HTTP ${res.status}`, res.status);
|
|
112
|
+
const body = await res.json();
|
|
113
|
+
return body.data;
|
|
114
|
+
}
|
|
115
|
+
/** Get a single model by ID */
|
|
116
|
+
static async getModel(id) {
|
|
117
|
+
const res = await fetch(`${BASE_URL}/models/${encodeURIComponent(id)}`, { headers: CLIENT_HEADERS });
|
|
118
|
+
if (!res.ok) throw new ApiError(`HTTP ${res.status}`, res.status);
|
|
119
|
+
return await res.json();
|
|
120
|
+
}
|
|
121
|
+
/** List available credit packages */
|
|
122
|
+
static async listPackages() {
|
|
123
|
+
const res = await fetch(`${BASE_URL}/billing/packages`, { headers: CLIENT_HEADERS });
|
|
124
|
+
if (!res.ok) throw new ApiError(`HTTP ${res.status}`, res.status);
|
|
125
|
+
const body = await res.json();
|
|
126
|
+
return body.data;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/core/prompts.ts
|
|
131
|
+
import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
132
|
+
import { join } from "path";
|
|
133
|
+
import YAML from "yaml";
|
|
134
|
+
function listTemplates() {
|
|
135
|
+
ensureDirs();
|
|
136
|
+
if (!existsSync(PATHS.prompts)) return [];
|
|
137
|
+
const files = readdirSync(PATHS.prompts).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
138
|
+
return files.map((f) => {
|
|
139
|
+
const raw = readFileSync(join(PATHS.prompts, f), "utf-8");
|
|
140
|
+
return YAML.parse(raw);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function getTemplate(name) {
|
|
144
|
+
const slug = slugify(name);
|
|
145
|
+
const path = join(PATHS.prompts, `${slug}.yaml`);
|
|
146
|
+
if (!existsSync(path)) return null;
|
|
147
|
+
return YAML.parse(readFileSync(path, "utf-8"));
|
|
148
|
+
}
|
|
149
|
+
function saveTemplate(template) {
|
|
150
|
+
ensureDirs();
|
|
151
|
+
const yaml = YAML.stringify(template);
|
|
152
|
+
YAML.parse(yaml);
|
|
153
|
+
const slug = slugify(template.name);
|
|
154
|
+
const path = join(PATHS.prompts, `${slug}.yaml`);
|
|
155
|
+
writeFileSync(path, yaml, "utf-8");
|
|
156
|
+
}
|
|
157
|
+
function deleteTemplate(name) {
|
|
158
|
+
const slug = slugify(name);
|
|
159
|
+
const path = join(PATHS.prompts, `${slug}.yaml`);
|
|
160
|
+
if (!existsSync(path)) return false;
|
|
161
|
+
unlinkSync(path);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
function extractVariables(prompt) {
|
|
165
|
+
const matches = prompt.match(/\{\{(\w+)\}\}/g);
|
|
166
|
+
if (!matches) return [];
|
|
167
|
+
return [...new Set(matches.map((m) => m.slice(2, -2)))];
|
|
168
|
+
}
|
|
169
|
+
function interpolate(prompt, vars) {
|
|
170
|
+
return prompt.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
171
|
+
}
|
|
172
|
+
function slugify(name) {
|
|
173
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/core/auth.ts
|
|
177
|
+
import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
178
|
+
var KEY_PATTERN = /^pm_(live|test)_[0-9a-f]{32}$/;
|
|
179
|
+
var SERVICE = "pixelmuse-cli";
|
|
180
|
+
var ACCOUNT = "api-key";
|
|
181
|
+
function isValidKeyFormat(key) {
|
|
182
|
+
return KEY_PATTERN.test(key);
|
|
183
|
+
}
|
|
184
|
+
async function getApiKey() {
|
|
185
|
+
const envKey = process.env["PIXELMUSE_API_KEY"];
|
|
186
|
+
if (envKey) return envKey;
|
|
187
|
+
try {
|
|
188
|
+
const { getPassword } = await import("keyring-node");
|
|
189
|
+
const keychainKey = getPassword(SERVICE, ACCOUNT);
|
|
190
|
+
if (keychainKey) return keychainKey;
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
if (existsSync2(PATHS.auth)) {
|
|
194
|
+
try {
|
|
195
|
+
const data = JSON.parse(readFileSync2(PATHS.auth, "utf-8"));
|
|
196
|
+
if (data.apiKey) return data.apiKey;
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
async function saveApiKey(key) {
|
|
203
|
+
ensureDirs();
|
|
204
|
+
try {
|
|
205
|
+
const { setPassword } = await import("keyring-node");
|
|
206
|
+
setPassword(SERVICE, ACCOUNT, key);
|
|
207
|
+
return;
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
writeFileSync2(PATHS.auth, JSON.stringify({ apiKey: key }), "utf-8");
|
|
211
|
+
chmodSync(PATHS.auth, 384);
|
|
212
|
+
}
|
|
213
|
+
async function deleteApiKey() {
|
|
214
|
+
try {
|
|
215
|
+
const { deletePassword } = await import("keyring-node");
|
|
216
|
+
deletePassword(SERVICE, ACCOUNT);
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
if (existsSync2(PATHS.auth)) {
|
|
220
|
+
unlinkSync2(PATHS.auth);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/core/polling.ts
|
|
225
|
+
var TYPICAL_DURATIONS = {
|
|
226
|
+
"flux-schnell": 12,
|
|
227
|
+
"recraft-v4": 20,
|
|
228
|
+
"recraft-v4-pro": 25
|
|
229
|
+
};
|
|
230
|
+
async function pollGeneration(client, id, options = {}) {
|
|
231
|
+
const { interval = 2e3, timeout = 12e4, onProgress } = options;
|
|
232
|
+
const start = Date.now();
|
|
233
|
+
let typicalDuration = 15;
|
|
234
|
+
let currentInterval = interval;
|
|
235
|
+
let model;
|
|
236
|
+
while (true) {
|
|
237
|
+
const elapsed = (Date.now() - start) / 1e3;
|
|
238
|
+
if (Date.now() - start > timeout) {
|
|
239
|
+
throw new Error(`Generation timed out after ${timeout / 1e3}s`);
|
|
240
|
+
}
|
|
241
|
+
const gen = await client.getGeneration(id);
|
|
242
|
+
if (!model && gen.model) {
|
|
243
|
+
model = gen.model;
|
|
244
|
+
typicalDuration = TYPICAL_DURATIONS[gen.model] ?? 15;
|
|
245
|
+
}
|
|
246
|
+
if (gen.status === "succeeded") return gen;
|
|
247
|
+
if (gen.status === "failed") {
|
|
248
|
+
throw new Error(gen.error ?? "Generation failed");
|
|
249
|
+
}
|
|
250
|
+
const estimatedProgress = Math.min(elapsed / typicalDuration, 0.95);
|
|
251
|
+
onProgress?.(elapsed, estimatedProgress);
|
|
252
|
+
if (elapsed > 30) currentInterval = 5e3;
|
|
253
|
+
await new Promise((r) => setTimeout(r, currentInterval));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export {
|
|
258
|
+
CLI_VERSION,
|
|
259
|
+
PixelmuseClient,
|
|
260
|
+
pollGeneration,
|
|
261
|
+
listTemplates,
|
|
262
|
+
getTemplate,
|
|
263
|
+
saveTemplate,
|
|
264
|
+
deleteTemplate,
|
|
265
|
+
extractVariables,
|
|
266
|
+
interpolate,
|
|
267
|
+
slugify,
|
|
268
|
+
isValidKeyFormat,
|
|
269
|
+
getApiKey,
|
|
270
|
+
saveApiKey,
|
|
271
|
+
deleteApiKey
|
|
272
|
+
};
|