@hypersocial/decks-cli 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersocial/decks-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Command-line client for Hypersocial Decks live analytics and share management.",
5
5
  "type": "module",
6
6
  "main": "./src/commands.mjs",
package/src/commands.mjs CHANGED
@@ -227,6 +227,8 @@ commands:
227
227
  skill install [--agent codex,claude-code] [--all-agents]
228
228
  skills install [--agent codex,claude-code] [--all-agents]
229
229
  activity [--deck <slug>] [--limit <n>] [--json]
230
+ gen-image "<prompt>" [--transparent] [--image <url>] [--size <s>] [--quality <q>] [--count <n>] [--slug <deck>] [--download <file>]
231
+ image-job <jobId> [--json]
230
232
 
231
233
  global:
232
234
  --json
@@ -244,7 +246,7 @@ async function genImage(pos, opts) {
244
246
  'usage: gen-image "<prompt>" [--image <url>] [--transparent] [--size <s>] [--quality <q>] [--count <n>] [--format png|jpeg|webp] [--slug <deck>] [--name <name>] [--download <file>]',
245
247
  );
246
248
  }
247
- const out = await ops.generateImage(opts, {
249
+ let out = await ops.generateImage(opts, {
248
250
  prompt,
249
251
  image: opts.image,
250
252
  transparent: opts.transparent,
@@ -255,6 +257,16 @@ async function genImage(pos, opts) {
255
257
  slug: opts.slug,
256
258
  name: opts.name,
257
259
  });
260
+ // Slow requests (high/auto quality, transparent, count>1) come back as a job id;
261
+ // poll it so gen-image stays one blocking command. Progress prints to stderr so
262
+ // it doesn't pollute the URL output (or --json).
263
+ if (out.jobId) {
264
+ process.stderr.write(`queued ${out.count} image(s) — job ${out.jobId}\n`);
265
+ out = await ops.pollImageJob(opts, out.jobId, {
266
+ onProgress: (job) => process.stderr.write(` ${job.progress} (${job.status})…\r`),
267
+ });
268
+ process.stderr.write("\n");
269
+ }
258
270
  const images = out.images || [];
259
271
  if (opts.download && images.length) {
260
272
  for (let i = 0; i < images.length; i++) {
@@ -267,9 +279,23 @@ async function genImage(pos, opts) {
267
279
  if (opts.json) {
268
280
  console.log(JSON.stringify(out, null, 2));
269
281
  } else {
270
- for (const im of images) console.log(im.url);
271
- if (opts.download) console.log(`saved ${images.length} file(s)`);
282
+ for (const im of images) if (im.url) console.log(im.url);
283
+ if (opts.download) console.log(`\nSaved ${images.length} file(s) to ${opts.download}`);
284
+ }
285
+ }
286
+
287
+ async function imageJob(pos, opts) {
288
+ const [jobId] = pos;
289
+ if (!jobId) throw new Error("usage: image-job <jobId>");
290
+ const job = await ops.checkImageJob(opts, jobId);
291
+ if (opts.json) {
292
+ console.log(JSON.stringify(job, null, 2));
293
+ return;
272
294
  }
295
+ console.log(`status: ${job.status}`);
296
+ console.log(`progress: ${job.progress}`);
297
+ if (job.error) console.log(`error: ${job.error}`);
298
+ for (const im of job.results || []) if (im.url) console.log(im.url);
273
299
  }
274
300
 
275
301
  export async function runCli(argv = process.argv.slice(2)) {
@@ -307,6 +333,7 @@ export async function runCli(argv = process.argv.slice(2)) {
307
333
  "skills install": () => installHyperdecksSkill(opts, rest),
308
334
  activity: () => activity(pos.slice(1), opts),
309
335
  "gen-image": () => genImage(pos.slice(1), opts),
336
+ "image-job": () => imageJob(pos.slice(1), opts),
310
337
  };
311
338
 
312
339
  const handler = handlers[`${group} ${cmd}`] || handlers[group];
package/src/ops.mjs CHANGED
@@ -51,9 +51,41 @@ export async function generateImage(opts, fields = {}) {
51
51
  if (fields.quality) args.quality = String(fields.quality);
52
52
  if (fields.count != null && fields.count !== "") args.count = Number(fields.count);
53
53
  if (fields.format) args.format = String(fields.format);
54
+ // Returns { images } for fast requests, or { jobId, status, count } for slow ones
55
+ // (high/auto quality, transparent, count>1). The MCP tool returns this as-is so it
56
+ // never blocks; the CLI polls with pollImageJob to stay one-command (see commands).
54
57
  return (await client(opts)).action(api.imagegen.generateImage, args);
55
58
  }
56
59
 
60
+ // Read one async image job's status/results. Shared by the MCP check_image_job tool
61
+ // and the CLI poller.
62
+ export async function checkImageJob(opts, jobId) {
63
+ if (!jobId) throw new Error("jobId is required");
64
+ return (await client(opts)).query(api.imagegen.checkImageJob, { jobId: String(jobId) });
65
+ }
66
+
67
+ // Poll an async image job until it finishes. Returns { images, slug } on success
68
+ // (results are the same shape as the inline { images }); throws on job error or
69
+ // timeout. `onProgress(job)` is called each tick. Used by the CLI so gen-image stays
70
+ // a single blocking command even when it routes to a background job.
71
+ export async function pollImageJob(opts, jobId, { intervalMs = 3000, timeoutMs = 900_000, onProgress } = {}) {
72
+ const c = await client(opts);
73
+ const start = Date.now();
74
+ for (;;) {
75
+ const job = await c.query(api.imagegen.checkImageJob, { jobId: String(jobId) });
76
+ if (job.status === "done") return { images: job.results || [], slug: job.slug };
77
+ if (job.status === "error") {
78
+ // A batch isn't atomic: earlier images may have been generated + saved before
79
+ // the failure. Surface that so the user knows assets/spend exist despite "error".
80
+ const ok = (job.results || []).filter((r) => r.url).length;
81
+ throw new Error(`${job.error || "image job failed"}${ok ? ` (${ok} image(s) completed before the failure)` : ""}`);
82
+ }
83
+ if (Date.now() - start > timeoutMs) throw new Error(`image job ${jobId} timed out after ${Math.round(timeoutMs / 1000)}s`);
84
+ if (onProgress) onProgress(job);
85
+ await new Promise((r) => setTimeout(r, intervalMs));
86
+ }
87
+ }
88
+
57
89
  export async function listShares(opts, slug) {
58
90
  if (!slug) throw new Error("slug is required");
59
91
  const rows = await (await client(opts)).query(api.shares.listForDeck, { deckSlug: slug });
package/src/runtime.mjs CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { ConvexHttpClient } from "convex/browser";
9
9
  import { makeFunctionReference } from "convex/server";
10
10
 
11
- export const VERSION = "0.1.7";
11
+ export const VERSION = "0.1.9";
12
12
  const UPLOAD_TIMEOUT_MS = 125_000;
13
13
 
14
14
  export const PROD = {
@@ -47,6 +47,7 @@ export const api = {
47
47
  },
48
48
  imagegen: {
49
49
  generateImage: makeFunctionReference("imagegen:generateImage"),
50
+ checkImageJob: makeFunctionReference("imagegen:checkImageJob"),
50
51
  },
51
52
  };
52
53