makefx 2.0.0 → 2.0.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/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
3
3
  The 1.x packages were built from the first version of makefx.app and do not
4
4
  work with the current service.
5
5
 
6
+ ## 2.0.2 — 2026-09-26
7
+
8
+ - `create --wait --json` now prints each asset's final ready or failed state instead of the queued state captured before waiting, and exits 1 with the failed asset's error when waiting ends in failure.
9
+ - Usage and unexpected errors are now also written as JSON to stdout under `--json`, and the `upload` error for an unrecognized file extension now names the supported upload types.
10
+
11
+ ## 2.0.1 — 2026-09-24
12
+
13
+ - Always print the login authorization URL, callback port, and an SSH tunnel command with the detected hostname so remote login can be completed from a local browser.
14
+
6
15
  ## 2.0.0 — 2026-09-24
7
16
 
8
17
  ### One canvas for you and your agent
package/README.md CHANGED
@@ -16,6 +16,19 @@ npm install -g makefx
16
16
  makefx login
17
17
  ```
18
18
 
19
+ Login always prints the authorization URL and callback address
20
+ (`http://127.0.0.1:8765/callback`), then tries to open a browser. If the CLI is
21
+ running on a remote host, keep login running and open a separate terminal on
22
+ the computer with your browser. Start the printed SSH tunnel before opening
23
+ the authorization URL:
24
+
25
+ ```bash
26
+ ssh -N -L 8765:127.0.0.1:8765 user@remote-host
27
+ ```
28
+
29
+ The CLI suggests the detected hostname; replace it with your usual SSH
30
+ destination or alias if needed. Keep the tunnel open until login finishes.
31
+
19
32
  Use `--env production|stage|local` to keep separate credentials and select the
20
33
  endpoint. Production is the default. `makefx logout` revokes the grant when the
21
34
  service is reachable and always removes the stored credentials; a grant can
package/dist/makefx.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import process4 from "node:process";
4
+ import process5 from "node:process";
5
5
 
6
6
  // src/lib/project.ts
7
7
  var SERVICE_NAME = "makefx.app";
@@ -13,7 +13,7 @@ var ENVIRONMENT_ORIGINS = {
13
13
  };
14
14
 
15
15
  // src/lib/version.ts
16
- var CLI_VERSION = true ? "2.0.0" : "0.0.0-dev";
16
+ var CLI_VERSION = true ? "2.0.2" : "0.0.0-dev";
17
17
 
18
18
  // src/commands/convenience.ts
19
19
  import { randomUUID } from "node:crypto";
@@ -870,6 +870,7 @@ async function handleOpen(parsed, dependencies = defaults) {
870
870
 
871
871
  // src/commands/create.ts
872
872
  import { randomUUID as randomUUID2 } from "node:crypto";
873
+ import process2 from "node:process";
873
874
 
874
875
  // src/lib/json-schema.ts
875
876
  import { isDeepStrictEqual } from "node:util";
@@ -1093,7 +1094,7 @@ function validateJsonSchema(schema, input) {
1093
1094
  // src/commands/create.ts
1094
1095
  var defaults2 = {
1095
1096
  client: authenticatedToolClient,
1096
- write: (text2) => process.stdout.write(`${text2}
1097
+ write: (text2) => process2.stdout.write(`${text2}
1097
1098
  `),
1098
1099
  id: randomUUID2
1099
1100
  };
@@ -1360,13 +1361,13 @@ async function handleCreate(parsed, dependencies = defaults2) {
1360
1361
  }
1361
1362
  const args = createToolArguments(prepared, entry, dependencies.id());
1362
1363
  const created = await client.call("create_asset", args);
1363
- const assets = Array.isArray(created.assets) ? created.assets : [];
1364
+ let assets = Array.isArray(created.assets) ? created.assets : [];
1364
1365
  if (parsed.options.wait === "true" && !assets.some(({ renders_in }) => renders_in === "browser")) {
1365
1366
  const spaceId2 = prepared.spaceId;
1366
- const failures = await Promise.all(
1367
+ assets = await Promise.all(
1367
1368
  assets.map(async (asset) => {
1368
1369
  const assetId = asset.asset_id;
1369
- if (typeof assetId !== "string") return null;
1370
+ if (typeof assetId !== "string") return asset;
1370
1371
  while (true) {
1371
1372
  const result = await client.call("get_asset", {
1372
1373
  space_id: spaceId2,
@@ -1374,16 +1375,26 @@ async function handleCreate(parsed, dependencies = defaults2) {
1374
1375
  wait_seconds: 60
1375
1376
  });
1376
1377
  const current = result.asset;
1377
- if (current?.status === "ready") return null;
1378
- if (current?.status === "failed") {
1379
- const error = current.error;
1380
- return error?.message ?? `Asset ${assetId} failed.`;
1378
+ if (current?.status === "ready" || current?.status === "failed") {
1379
+ return current ? { ...asset, ...current } : asset;
1381
1380
  }
1382
1381
  }
1383
1382
  })
1384
1383
  );
1385
- const messages = failures.filter((message) => message !== null);
1386
- if (messages.length > 0) throw new Error(messages.join("\n"));
1384
+ created.assets = assets;
1385
+ const failed = assets.filter((asset) => asset.status === "failed");
1386
+ if (failed.length > 0) {
1387
+ if (parsed.options.json === "true") {
1388
+ dependencies.write(JSON.stringify(created, null, 2));
1389
+ process2.exitCode = 1;
1390
+ return;
1391
+ }
1392
+ const messages = failed.map((asset) => {
1393
+ const error = asset.error;
1394
+ return error?.message ?? `Asset ${String(asset.asset_id)} failed.`;
1395
+ });
1396
+ throw new Error(messages.join("\n"));
1397
+ }
1387
1398
  }
1388
1399
  if (parsed.options.json === "true") {
1389
1400
  dependencies.write(JSON.stringify(created, null, 2));
@@ -1397,7 +1408,8 @@ async function handleCreate(parsed, dependencies = defaults2) {
1397
1408
  }
1398
1409
 
1399
1410
  // src/commands/login.ts
1400
- import process2 from "node:process";
1411
+ import process3 from "node:process";
1412
+ import { hostname } from "node:os";
1401
1413
  async function handleLogin(parsed) {
1402
1414
  const isLocal = parsed.options.local === "true";
1403
1415
  const env = isLocal ? "local" : parsed.options.env ?? DEFAULT_ENVIRONMENT;
@@ -1407,7 +1419,7 @@ async function handleLogin(parsed) {
1407
1419
  const insecure = isLocal;
1408
1420
  if (insecure) {
1409
1421
  console.log("\u26A0\uFE0F SSL certificate verification disabled (local dev mode)");
1410
- process2.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
1422
+ process3.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
1411
1423
  }
1412
1424
  console.log(`Starting login for environment "${env}" using ${baseUrl}`);
1413
1425
  const server = await discoverAuthorizationServer(baseUrl);
@@ -1424,12 +1436,22 @@ async function handleLogin(parsed) {
1424
1436
  authUrl.searchParams.set("code_challenge_method", "S256");
1425
1437
  authUrl.searchParams.set("state", state);
1426
1438
  authUrl.searchParams.set("resource", mcpResourceFor(baseUrl));
1439
+ console.log(`
1440
+ Open this URL in your browser to authenticate:
1441
+
1442
+ ${authUrl.toString()}
1443
+ `);
1444
+ console.log(`The OAuth callback uses ${redirectUri} (port ${redirectPort}).`);
1445
+ const detectedHost = hostname();
1446
+ const sshHost = /^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(detectedHost) ? detectedHost : "YOUR_SSH_HOST";
1447
+ console.log("If this CLI is on a remote host, run this in a separate terminal on the computer with your browser before opening the URL:");
1448
+ console.log(` ssh -N -L ${redirectPort}:127.0.0.1:${redirectPort} ${sshHost}`);
1449
+ console.log("Use your usual SSH destination (user@host or SSH alias) if the detected hostname is not reachable. Keep the tunnel open until login finishes.\n");
1427
1450
  console.log("Opening browser for Google authentication...");
1428
1451
  try {
1429
1452
  await openBrowser(authUrl.toString());
1430
1453
  } catch {
1431
- console.warn("Unable to open browser automatically. Please copy the URL below into your browser:");
1432
- console.log(authUrl.toString());
1454
+ console.warn("Unable to open browser automatically. Please open the URL above in your browser.");
1433
1455
  }
1434
1456
  const { code } = await waitForAuthorizationCode(redirectPort, state);
1435
1457
  console.log("Received authorization code. Exchanging for access token...");
@@ -1485,20 +1507,20 @@ async function handleLogout(parsed) {
1485
1507
  }
1486
1508
 
1487
1509
  // src/commands/mcp.ts
1488
- import process3 from "node:process";
1510
+ import process4 from "node:process";
1489
1511
  async function handleMcp(parsed) {
1490
1512
  const env = parsed.options.local === "true" ? "local" : parsed.options.env ?? DEFAULT_ENVIRONMENT;
1491
1513
  const loginHint = `Run: ${cliCommand()} login --env ${env}`;
1492
1514
  const stored = await loadStoredConfig(env);
1493
1515
  if (!stored) {
1494
1516
  console.error(`Not logged in for "${env}". ${loginHint}`);
1495
- process3.exitCode = 1;
1517
+ process4.exitCode = 1;
1496
1518
  return;
1497
1519
  }
1498
1520
  const config = await ensureFreshConfig(stored);
1499
1521
  if (!config) {
1500
1522
  console.error(`Credentials for "${env}" have expired. ${loginHint}`);
1501
- process3.exitCode = 1;
1523
+ process4.exitCode = 1;
1502
1524
  return;
1503
1525
  }
1504
1526
  const bridge = createMcpBridge({ config });
@@ -1508,15 +1530,15 @@ async function handleMcp(parsed) {
1508
1530
  case "accepted":
1509
1531
  return;
1510
1532
  case "response":
1511
- if (outcome.text) process3.stdout.write(`${outcome.text}
1533
+ if (outcome.text) process4.stdout.write(`${outcome.text}
1512
1534
  `);
1513
1535
  return;
1514
1536
  case "unauthorized":
1515
- if (outcome.text) process3.stdout.write(`${outcome.text}
1537
+ if (outcome.text) process4.stdout.write(`${outcome.text}
1516
1538
  `);
1517
1539
  console.error(`Access to "${env}" was revoked or has expired. ${loginHint}`);
1518
- process3.exitCode = 1;
1519
- process3.stdin.destroy();
1540
+ process4.exitCode = 1;
1541
+ process4.stdin.destroy();
1520
1542
  return;
1521
1543
  case "unreachable":
1522
1544
  console.error(`MCP request failed: ${outcome.message}`);
@@ -1525,8 +1547,8 @@ async function handleMcp(parsed) {
1525
1547
  }
1526
1548
  let buffer = "";
1527
1549
  const pending = [];
1528
- process3.stdin.setEncoding("utf8");
1529
- for await (const chunk of process3.stdin) {
1550
+ process4.stdin.setEncoding("utf8");
1551
+ for await (const chunk of process4.stdin) {
1530
1552
  buffer += chunk;
1531
1553
  let newline = buffer.indexOf("\n");
1532
1554
  while (newline !== -1) {
@@ -2328,7 +2350,12 @@ function uploadMime(parsed, path2) {
2328
2350
  const explicit = optional5(parsed, "mime");
2329
2351
  if (explicit) return explicit;
2330
2352
  const inferred = MIME_BY_EXTENSION[extname(path2).toLowerCase()];
2331
- if (!inferred) throw new CliUsageError("Cannot infer media type from --file; pass --mime <type>.");
2353
+ if (!inferred) {
2354
+ const supported = Object.keys(MIME_BY_EXTENSION).sort().join(", ");
2355
+ throw new CliUsageError(
2356
+ `Cannot infer media type from --file; supported uploads are ${supported} \u2014 pass --mime <type> to override.`
2357
+ );
2358
+ }
2332
2359
  return inferred;
2333
2360
  }
2334
2361
  function declaredRecipe(parsed) {
@@ -2552,11 +2579,16 @@ function reportCommandError(error, json2, output) {
2552
2579
  return 1;
2553
2580
  }
2554
2581
  if (error instanceof CliUsageError) {
2582
+ if (json2) output.stdout(`${JSON.stringify({ code: "usage", message: error.message })}
2583
+ `);
2555
2584
  output.stderr(`Error: ${error.message}
2556
2585
  `);
2557
2586
  return 2;
2558
2587
  }
2559
- output.stderr(`Error: ${error instanceof Error ? error.message : "Unexpected error occurred"}
2588
+ const message = error instanceof Error ? error.message : "Unexpected error occurred";
2589
+ if (json2) output.stdout(`${JSON.stringify({ code: "error", message })}
2590
+ `);
2591
+ output.stderr(`Error: ${message}
2560
2592
  `);
2561
2593
  return 1;
2562
2594
  }
@@ -2851,7 +2883,7 @@ async function handleSpaceVisibilityCommand(command, parsed, dependencies = defa
2851
2883
 
2852
2884
  // src/index.ts
2853
2885
  async function main() {
2854
- const [, , command, ...args] = process4.argv;
2886
+ const [, , command, ...args] = process5.argv;
2855
2887
  try {
2856
2888
  if (!command || command === "--help") {
2857
2889
  printHelp();
@@ -2871,9 +2903,9 @@ async function main() {
2871
2903
  await dispatchCommand(command, parsed);
2872
2904
  } catch (error) {
2873
2905
  const parsed = parseArgs(args);
2874
- process4.exitCode = reportCommandError(error, parsed.options.json === "true", {
2875
- stdout: (text2) => process4.stdout.write(text2),
2876
- stderr: (text2) => process4.stderr.write(text2)
2906
+ process5.exitCode = reportCommandError(error, parsed.options.json === "true", {
2907
+ stdout: (text2) => process5.stdout.write(text2),
2908
+ stderr: (text2) => process5.stderr.write(text2)
2877
2909
  });
2878
2910
  }
2879
2911
  }
@@ -2958,7 +2990,7 @@ Run makefx <command> --help, makefx space help, makefx asset help, makefx audio
2958
2990
  }
2959
2991
  function helpForCommand(command) {
2960
2992
  const staticUsage = {
2961
- login: "Usage: makefx login [--env production|stage|local] [--local]",
2993
+ login: "Usage: makefx login [--env production|stage|local] [--local]\n\nPrints the browser authorization URL and loopback callback port (8765).\nFor remote hosts, also prints an SSH tunnel command to run on your browser computer.",
2962
2994
  logout: "Usage: makefx logout [--env production|stage|local] [--local]",
2963
2995
  mcp: "Usage: makefx mcp [--env production|stage|local] [--local]",
2964
2996
  create: "Usage: makefx create --space ACCOUNT/SPACE --kind image|video|audio --model MODEL [--prompt TEXT] [--ref ASSET:SLOT]... [--param NAME=VALUE]... [--count 1..8] [--name TEXT] [--seed INTEGER] [--position JSON] [--tags JSON] [--note TEXT] [--from-asset ASSET] [--recipe-mode current|exact] [--request-id ID] [--wait] [--json]\nRepeated --ref values preserve left-to-right order as reference order 0, 1, and so on. Reads the live catalog for the paying Space before calling create_asset.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makefx",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Make images, video, and audio on makefx.app from your terminal or coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",