pixelmuse 0.2.0 → 0.3.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 CHANGED
@@ -29,19 +29,14 @@
29
29
  - **Built for developer workflows.** Pipe from stdin, JSON output for scripting, watch mode for prompt iteration, MCP server for AI agents.
30
30
  - **Predictable credit pricing.** 1-3 credits per generation, no surprises. Free credits on signup.
31
31
 
32
- ## Install
32
+ ## Get Started
33
33
 
34
34
  ```bash
35
35
  npm install -g pixelmuse
36
+ pixelmuse setup
36
37
  ```
37
38
 
38
- Then authenticate:
39
-
40
- ```bash
41
- pixelmuse login
42
- ```
43
-
44
- Sign up at [pixelmuse.studio/sign-up](https://www.pixelmuse.studio/sign-up) — new accounts include free credits.
39
+ The setup wizard creates your account (opens browser), configures your API key, and optionally sets up the MCP server for Claude Code, Cursor, or Windsurf. New accounts include **15 free credits**.
45
40
 
46
41
  > Requires Node.js 20+. For terminal image previews, install [chafa](https://hpjansson.org/chafa/) (`brew install chafa` / `sudo apt install chafa`).
47
42
 
@@ -212,6 +207,7 @@ Pixelmuse ships four interfaces. Pick the one that fits your workflow — they a
212
207
 
213
208
  | Command | Description |
214
209
  |---------|-------------|
210
+ | `pixelmuse setup` | First-time setup wizard (account, MCP, defaults) |
215
211
  | `pixelmuse "prompt"` | Generate an image (default command) |
216
212
  | `pixelmuse models` | List available models with costs |
217
213
  | `pixelmuse account` | Account balance and usage stats |
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  pollGeneration,
4
4
  slugify
5
- } from "./chunk-ZVJQFWUI.js";
5
+ } from "./chunk-H6VI5DLX.js";
6
6
  import {
7
7
  autoSave,
8
8
  imageToBuffer,
@@ -4,9 +4,6 @@ import {
4
4
  ensureDirs
5
5
  } from "./chunk-7ARYEFAH.js";
6
6
 
7
- // src/core/client.ts
8
- import { createRequire } from "module";
9
-
10
7
  // src/core/types.ts
11
8
  var ApiError = class extends Error {
12
9
  constructor(message, status, code, rateLimitRemaining, retryAfter) {
@@ -20,8 +17,7 @@ var ApiError = class extends Error {
20
17
  };
21
18
 
22
19
  // src/core/client.ts
23
- var require2 = createRequire(import.meta.url);
24
- var CLI_VERSION = require2("../../package.json").version;
20
+ var CLI_VERSION = "0.3.0";
25
21
  var BASE_URL = "https://www.pixelmuse.studio/api/v1";
26
22
  var CLIENT_HEADERS = {
27
23
  "User-Agent": `pixelmuse-cli/${CLI_VERSION}`,
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  generateImage
4
- } from "./chunk-743CKPHW.js";
4
+ } from "./chunk-4DZUZTTL.js";
5
5
  import {
6
+ CLI_VERSION,
6
7
  PixelmuseClient,
7
8
  deleteApiKey,
8
9
  extractVariables,
@@ -14,19 +15,20 @@ import {
14
15
  saveApiKey,
15
16
  saveTemplate,
16
17
  slugify
17
- } from "./chunk-ZVJQFWUI.js";
18
+ } from "./chunk-H6VI5DLX.js";
18
19
  import {
19
20
  renderImageDirect
20
21
  } from "./chunk-MZZY4JXW.js";
21
22
  import {
22
- readSettings
23
+ readSettings,
24
+ writeSettings
23
25
  } from "./chunk-7ARYEFAH.js";
24
26
 
25
27
  // src/cli.ts
26
28
  import meow from "meow";
27
29
  import chalk from "chalk";
28
30
  import ora from "ora";
29
- import { readFileSync, watchFile, unwatchFile } from "fs";
31
+ import { readFileSync as readFileSync2, watchFile, unwatchFile } from "fs";
30
32
  import { resolve } from "path";
31
33
 
32
34
  // src/core/utils.ts
@@ -39,6 +41,128 @@ function timeAgo(date) {
39
41
  return date.toLocaleDateString();
40
42
  }
41
43
 
44
+ // src/core/device-auth.ts
45
+ var BASE_URL = "https://www.pixelmuse.studio/api/v1";
46
+ async function initiateDeviceAuth() {
47
+ const res = await fetch(`${BASE_URL}/auth/device`, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "User-Agent": `pixelmuse-cli/${CLI_VERSION}`
52
+ },
53
+ body: JSON.stringify({
54
+ client_info: {
55
+ platform: process.platform,
56
+ cli_version: CLI_VERSION
57
+ }
58
+ })
59
+ });
60
+ if (!res.ok) {
61
+ const body = await res.text().catch(() => "");
62
+ throw new Error(`Failed to initiate device auth (HTTP ${res.status}): ${body}`);
63
+ }
64
+ const data = await res.json();
65
+ return {
66
+ deviceCode: data.device_code,
67
+ userCode: data.user_code,
68
+ verificationUri: data.verification_uri,
69
+ verificationUriComplete: data.verification_uri_complete,
70
+ expiresIn: data.expires_in,
71
+ interval: data.interval
72
+ };
73
+ }
74
+ async function pollForToken(deviceCode, options) {
75
+ const deadline = Date.now() + options.expiresIn * 1e3;
76
+ let intervalMs = options.interval * 1e3;
77
+ while (Date.now() < deadline) {
78
+ await new Promise((r) => setTimeout(r, intervalMs));
79
+ options.onPoll?.();
80
+ try {
81
+ const res = await fetch(`${BASE_URL}/auth/device/token`, {
82
+ method: "POST",
83
+ headers: {
84
+ "Content-Type": "application/json",
85
+ "User-Agent": `pixelmuse-cli/${CLI_VERSION}`
86
+ },
87
+ body: JSON.stringify({
88
+ device_code: deviceCode,
89
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
90
+ })
91
+ });
92
+ const data = await res.json();
93
+ if (data.api_key) {
94
+ return {
95
+ apiKey: data.api_key,
96
+ keyPrefix: data.key_prefix
97
+ };
98
+ }
99
+ if (data.error === "slow_down") {
100
+ intervalMs = (data.interval ?? 10) * 1e3;
101
+ continue;
102
+ }
103
+ if (data.error === "expired_token") {
104
+ throw new Error("Authorization timed out. Run `pixelmuse setup` to try again.");
105
+ }
106
+ if (data.error === "access_denied") {
107
+ throw new Error("Authorization was denied.");
108
+ }
109
+ } catch (err) {
110
+ if (err instanceof Error && (err.message.includes("timed out") || err.message.includes("denied"))) {
111
+ throw err;
112
+ }
113
+ }
114
+ }
115
+ throw new Error("Authorization timed out. Run `pixelmuse setup` to try again.");
116
+ }
117
+
118
+ // src/core/mcp-config.ts
119
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
120
+ import { dirname, join } from "path";
121
+ import { homedir } from "os";
122
+ var HOME = homedir();
123
+ var EDITORS = [
124
+ {
125
+ name: "Claude Code",
126
+ id: "claude-code",
127
+ configPath: join(HOME, ".claude", "mcp.json")
128
+ },
129
+ {
130
+ name: "Cursor",
131
+ id: "cursor",
132
+ configPath: join(HOME, ".cursor", "mcp.json")
133
+ },
134
+ {
135
+ name: "Windsurf",
136
+ id: "windsurf",
137
+ configPath: join(HOME, ".codeium", "windsurf", "mcp_config.json")
138
+ }
139
+ ];
140
+ function detectEditors() {
141
+ return EDITORS.map((editor) => ({
142
+ ...editor,
143
+ installed: existsSync(dirname(editor.configPath))
144
+ }));
145
+ }
146
+ function configureMcp(editor, apiKey) {
147
+ const entry = {
148
+ command: "npx",
149
+ args: ["-y", "pixelmuse-mcp"],
150
+ env: { PIXELMUSE_API_KEY: apiKey }
151
+ };
152
+ let config = {};
153
+ if (existsSync(editor.configPath)) {
154
+ try {
155
+ config = JSON.parse(readFileSync(editor.configPath, "utf-8"));
156
+ } catch {
157
+ }
158
+ } else {
159
+ mkdirSync(dirname(editor.configPath), { recursive: true });
160
+ }
161
+ config.mcpServers = config.mcpServers ?? {};
162
+ config.mcpServers.pixelmuse = entry;
163
+ writeFileSync(editor.configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
164
+ }
165
+
42
166
  // src/cli.ts
43
167
  var cli = meow(
44
168
  `
@@ -50,6 +174,7 @@ var cli = meow(
50
174
  $ echo "prompt" | pixelmuse Pipe from stdin
51
175
 
52
176
  ${chalk.bold("Commands")}
177
+ pixelmuse setup First-time setup wizard
53
178
  pixelmuse models List available models
54
179
  pixelmuse account View balance & usage
55
180
  pixelmuse history Recent generations
@@ -220,7 +345,7 @@ async function handleWatch(filePath) {
220
345
  const generate = async () => {
221
346
  if (generating) return;
222
347
  generating = true;
223
- const prompt = readFileSync(absPath, "utf-8").trim();
348
+ const prompt = readFileSync2(absPath, "utf-8").trim();
224
349
  if (!prompt) {
225
350
  generating = false;
226
351
  return;
@@ -333,9 +458,9 @@ async function handleOpen(id) {
333
458
  if (gen.output?.[0]) {
334
459
  const { imageToBuffer: imageToBuffer2, saveImage: saveImage2 } = await import("./image-2H3GUQI6.js");
335
460
  const { PATHS: PATHS2 } = await import("./config-RMVFR3GN.js");
336
- const { join } = await import("path");
461
+ const { join: join2 } = await import("path");
337
462
  const buf = await imageToBuffer2(gen.output[0]);
338
- const path = join(PATHS2.generations, `${gen.id}.png`);
463
+ const path = join2(PATHS2.generations, `${gen.id}.png`);
339
464
  saveImage2(buf, path);
340
465
  const { default: open } = await import("open");
341
466
  await open(path);
@@ -349,10 +474,53 @@ async function handleOpen(id) {
349
474
  }
350
475
  }
351
476
  async function handleLogin() {
477
+ console.log(chalk.bold("Pixelmuse Login"));
478
+ console.log();
479
+ const spinner = ora("Preparing login...").start();
480
+ try {
481
+ const init = await initiateDeviceAuth();
482
+ spinner.stop();
483
+ console.log(` Your code: ${chalk.bold.cyan(init.userCode)}`);
484
+ console.log();
485
+ console.log(` Opening ${chalk.underline(init.verificationUriComplete)}`);
486
+ console.log(` ${chalk.gray("Or visit")} ${chalk.underline(init.verificationUri)} ${chalk.gray("and enter the code manually")}`);
487
+ console.log();
488
+ const { default: open } = await import("open");
489
+ await open(init.verificationUriComplete);
490
+ const pollSpinner = ora("Waiting for authorization...").start();
491
+ let dots = 0;
492
+ const result = await pollForToken(init.deviceCode, {
493
+ interval: init.interval,
494
+ expiresIn: init.expiresIn,
495
+ onPoll: () => {
496
+ dots = (dots + 1) % 4;
497
+ pollSpinner.text = `Waiting for authorization${".".repeat(dots)}`;
498
+ }
499
+ });
500
+ await saveApiKey(result.apiKey);
501
+ pollSpinner.stop();
502
+ const client = new PixelmuseClient(result.apiKey);
503
+ const account = await client.getAccount();
504
+ console.log(chalk.green("\u2713") + ` Authenticated as ${chalk.bold(account.email)} (${chalk.green(`${account.credits.total} credits`)})`);
505
+ return result.apiKey;
506
+ } catch (err) {
507
+ spinner.stop();
508
+ if (err instanceof Error && !err.message.includes("timed out") && !err.message.includes("denied")) {
509
+ console.log(chalk.gray("Browser login unavailable. Enter your API key manually."));
510
+ } else if (err instanceof Error) {
511
+ console.log(chalk.yellow(err.message));
512
+ }
513
+ return handleManualLogin();
514
+ }
515
+ }
516
+ async function handleManualLogin() {
517
+ console.log();
518
+ console.log(` Get your key at: ${chalk.underline("https://www.pixelmuse.studio/settings/api-keys")}`);
519
+ console.log();
352
520
  const { createInterface } = await import("readline");
353
521
  const rl = createInterface({ input: process.stdin, output: process.stdout });
354
522
  const key = await new Promise((resolve2) => {
355
- rl.question("API key (from pixelmuse.studio/settings/api-keys): ", (answer) => {
523
+ rl.question("API key: ", (answer) => {
356
524
  rl.close();
357
525
  resolve2(answer.trim());
358
526
  });
@@ -367,11 +535,103 @@ async function handleLogin() {
367
535
  const account = await client.getAccount();
368
536
  await saveApiKey(key);
369
537
  spinner.succeed(`Authenticated as ${chalk.bold(account.email)} (${chalk.green(`${account.credits.total} credits`)})`);
538
+ return key;
370
539
  } catch (err) {
371
540
  spinner.fail(err instanceof Error ? err.message : "Authentication failed");
372
541
  process.exit(1);
373
542
  }
374
543
  }
544
+ async function handleSetup() {
545
+ console.log();
546
+ console.log(chalk.bold(" Pixelmuse Setup"));
547
+ console.log(chalk.gray(" AI image generation from the terminal"));
548
+ console.log();
549
+ const existingKey = await getApiKey();
550
+ let apiKey;
551
+ if (existingKey) {
552
+ const spinner = ora("Checking existing credentials...").start();
553
+ try {
554
+ const client = new PixelmuseClient(existingKey);
555
+ const account = await client.getAccount();
556
+ spinner.succeed(`Already authenticated as ${chalk.bold(account.email)} (${chalk.green(`${account.credits.total} credits`)})`);
557
+ apiKey = existingKey;
558
+ } catch {
559
+ spinner.warn("Existing credentials invalid. Re-authenticating...");
560
+ apiKey = await handleLogin();
561
+ }
562
+ } else {
563
+ console.log(chalk.bold("Step 1:") + " Authenticate");
564
+ console.log();
565
+ apiKey = await handleLogin();
566
+ }
567
+ console.log();
568
+ const editors = detectEditors();
569
+ const available = editors.filter((e) => e.installed);
570
+ if (available.length > 0) {
571
+ console.log(chalk.bold("Step 2:") + " MCP Server (AI agent integration)");
572
+ console.log();
573
+ const { createInterface } = await import("readline");
574
+ for (const editor of available) {
575
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
576
+ const answer = await new Promise((resolve2) => {
577
+ rl2.question(` Configure ${chalk.bold(editor.name)}? ${chalk.gray("[Y/n]")} `, (a) => {
578
+ rl2.close();
579
+ resolve2(a.trim().toLowerCase());
580
+ });
581
+ });
582
+ if (answer === "" || answer === "y" || answer === "yes") {
583
+ configureMcp(editor, apiKey);
584
+ console.log(chalk.green(" \u2713") + ` ${editor.name} MCP configured`);
585
+ } else {
586
+ console.log(chalk.gray(` Skipped ${editor.name}`));
587
+ }
588
+ }
589
+ console.log();
590
+ }
591
+ console.log(chalk.bold(available.length > 0 ? "Step 3:" : "Step 2:") + " Defaults");
592
+ console.log();
593
+ const { createInterface: createRl } = await import("readline");
594
+ const rl = createRl({ input: process.stdin, output: process.stdout });
595
+ const customize = await new Promise((resolve2) => {
596
+ rl.question(` Customize default model and settings? ${chalk.gray("[y/N]")} `, (a) => {
597
+ rl.close();
598
+ resolve2(a.trim().toLowerCase());
599
+ });
600
+ });
601
+ if (customize === "y" || customize === "yes") {
602
+ const settings = readSettings();
603
+ const updated = {};
604
+ const models = ["nano-banana-2", "nano-banana-pro", "flux-schnell", "imagen-3", "recraft-v4", "recraft-v4-pro"];
605
+ const aspects = ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"];
606
+ const styles = ["none", "realistic", "anime", "artistic"];
607
+ const pick = async (label, options, current) => {
608
+ const { createInterface } = await import("readline");
609
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
610
+ console.log(` ${label}: ${options.map((o) => o === current ? chalk.bold.cyan(o) : chalk.gray(o)).join(" | ")}`);
611
+ const answer = await new Promise((resolve2) => {
612
+ rl2.question(` Choice ${chalk.gray(`[${current}]`)}: `, (a) => {
613
+ rl2.close();
614
+ resolve2(a.trim() || current);
615
+ });
616
+ });
617
+ return options.includes(answer) ? answer : current;
618
+ };
619
+ updated.defaultModel = await pick("Model", models, settings.defaultModel);
620
+ updated.defaultAspectRatio = await pick("Aspect ratio", aspects, settings.defaultAspectRatio);
621
+ updated.defaultStyle = await pick("Style", styles, settings.defaultStyle);
622
+ writeSettings({ ...settings, ...updated });
623
+ console.log(chalk.green(" \u2713") + " Defaults saved");
624
+ } else {
625
+ console.log(chalk.gray(" Using defaults (nano-banana-2, 1:1, no style)"));
626
+ }
627
+ console.log();
628
+ console.log(chalk.green.bold(" Setup complete!"));
629
+ console.log();
630
+ console.log(` Try it: ${chalk.cyan('pixelmuse "a cat floating through space"')}`);
631
+ console.log(` TUI: ${chalk.cyan("pixelmuse ui")}`);
632
+ console.log(` Help: ${chalk.cyan("pixelmuse --help")}`);
633
+ console.log();
634
+ }
375
635
  async function handleTemplate() {
376
636
  const [, templateCmd, ...templateArgs] = cli.input;
377
637
  switch (templateCmd) {
@@ -490,8 +750,11 @@ async function main() {
490
750
  process.exit(1);
491
751
  }
492
752
  return handleOpen(rest[0]);
753
+ case "setup":
754
+ return handleSetup();
493
755
  case "login":
494
- return handleLogin();
756
+ await handleLogin();
757
+ return;
495
758
  case "logout":
496
759
  await deleteApiKey();
497
760
  console.log("Logged out.");
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  generateImage
4
- } from "../chunk-743CKPHW.js";
4
+ } from "../chunk-4DZUZTTL.js";
5
5
  import {
6
6
  CLI_VERSION,
7
7
  PixelmuseClient,
8
8
  getApiKey
9
- } from "../chunk-ZVJQFWUI.js";
9
+ } from "../chunk-H6VI5DLX.js";
10
10
  import "../chunk-MZZY4JXW.js";
11
11
  import {
12
12
  readSettings
package/dist/tui.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  pollGeneration,
12
12
  saveApiKey,
13
13
  saveTemplate
14
- } from "./chunk-ZVJQFWUI.js";
14
+ } from "./chunk-H6VI5DLX.js";
15
15
  import {
16
16
  autoSave,
17
17
  hasChafa,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixelmuse",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI image generation from the terminal — CLI, TUI, and MCP server for text-to-image with Flux, Imagen, Recraft, and more",
5
5
  "homepage": "https://pixelmuse.studio",
6
6
  "repository": {