storyboard-bridge 0.3.8 → 0.4.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.
Files changed (2) hide show
  1. package/index.mjs +30 -7
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -304,8 +304,9 @@ async function handleHiggsfieldCli(msg, ws) {
304
304
  if (!o?.id) throw new Error('higgsfield upload returned no id');
305
305
  medias.push({ role: 'image', data: { id: o.id, type: 'media_input' } }); // 'media_input' = uploaded file ('image' is rejected)
306
306
  }
307
+ const promptStr = String(p.prompt ?? '');
307
308
  const args = ['generate', 'create', String(p.slug || 'seedance_2_0'),
308
- '--prompt', String(p.prompt ?? ''),
309
+ '--prompt', promptStr.startsWith('@') ? ' ' + promptStr : promptStr, // leading '@' = "read from file" to the CLI; a space neutralizes it
309
310
  '--duration', String(Math.max(1, Math.round(p.durationSec || 5))),
310
311
  '--resolution', String(p.resolution || '720p'),
311
312
  '--aspect_ratio', String(p.aspectRatio || '16:9'),
@@ -325,6 +326,24 @@ async function handleHiggsfieldCli(msg, ws) {
325
326
  }
326
327
  }
327
328
 
329
+ // ---- disconnect-proof result delivery ----------------------------------------
330
+ // claude -p runs independently of the socket, so a WS drop mid-job must NOT lose the answer. Job results go
331
+ // through deliver(): sent on the live socket if open, else QUEUED in an outbox and flushed when we reconnect.
332
+ // The backend keeps the job pending across the drop and resolves it by id on whichever socket the result
333
+ // arrives on — so a deploy/network blip no longer turns a real answer into a mock fallback.
334
+ let activeWs = null;
335
+ const outbox = []; // JSON frame strings that couldn't be sent because the socket was down
336
+ function deliver(frame) {
337
+ const s = JSON.stringify(frame);
338
+ if (activeWs && activeWs.readyState === 1 /* WebSocket.OPEN */) { try { activeWs.send(s); return; } catch { /* fall through → queue */ } }
339
+ outbox.push(s);
340
+ }
341
+ function flushOutbox() {
342
+ if (!activeWs || activeWs.readyState !== 1) return;
343
+ while (outbox.length) { try { activeWs.send(outbox.shift()); } catch { break; } }
344
+ if (outbox.length === 0) return;
345
+ }
346
+
328
347
  // ---- connection (reconnects forever with backoff) ----
329
348
  let backoff = 1000;
330
349
  function connect() {
@@ -333,6 +352,8 @@ function connect() {
333
352
 
334
353
  ws.on('open', async () => {
335
354
  backoff = 1000;
355
+ activeWs = ws; // this socket is now the delivery channel
356
+ flushOutbox(); // re-send any results that completed while we were disconnected
336
357
  const providers = {
337
358
  claude: await cliAvailable(BIN_FOR.claude),
338
359
  gemini: await cliAvailable(BIN_FOR.gemini),
@@ -363,18 +384,18 @@ function connect() {
363
384
  const t0 = Date.now();
364
385
  try {
365
386
  const text = await runJob(String(msg.prompt ?? ''), Array.isArray(msg.images) ? msg.images : null);
366
- ws.send(JSON.stringify({ type: 'result', id: msg.id, text }));
367
- log(`job ${msg.id} ✓ (${Date.now() - t0}ms)`);
387
+ deliver({ type: 'result', id: msg.id, text }); // survives a mid-job reconnect (queued + flushed)
388
+ log(`job ${msg.id} ✓ (${Date.now() - t0}ms)${activeWs?.readyState === 1 ? '' : ' [queued — offline]'}`);
368
389
  } catch (e) {
369
- ws.send(JSON.stringify({ type: 'error', id: msg.id, message: String(e?.message ?? e) }));
390
+ deliver({ type: 'error', id: msg.id, message: String(e?.message ?? e) });
370
391
  log(`job ${msg.id} ✗ ${e?.message ?? e}`);
371
392
  }
372
393
  } else if (msg.type === 'file' && msg.id) {
373
394
  try {
374
395
  const r = await handleFile(msg);
375
- ws.send(JSON.stringify({ type: 'fileResult', id: msg.id, ...r }));
396
+ deliver({ type: 'fileResult', id: msg.id, ...r });
376
397
  } catch (e) {
377
- ws.send(JSON.stringify({ type: 'fileError', id: msg.id, message: String(e?.message ?? e) }));
398
+ deliver({ type: 'fileError', id: msg.id, message: String(e?.message ?? e) });
378
399
  log(`file ${msg.op} ${msg.id} ✗ ${e?.message ?? e}`);
379
400
  }
380
401
  } else if (msg.type === 'higgsfield' && msg.id) {
@@ -393,7 +414,9 @@ function connect() {
393
414
  });
394
415
 
395
416
  ws.on('close', () => {
396
- log(`disconnected — retrying in ${Math.round(backoff / 1000)}s`);
417
+ if (activeWs === ws) activeWs = null; // stop sending on the dead socket; results queue until we reconnect
418
+ const queued = outbox.length ? ` (${outbox.length} result(s) queued for re-delivery)` : '';
419
+ log(`disconnected — retrying in ${Math.round(backoff / 1000)}s${queued}`);
397
420
  setTimeout(connect, backoff);
398
421
  backoff = Math.min(backoff * 2, 30000);
399
422
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyboard-bridge",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "description": "Desktop bridge that powers a hosted Storyboard AI webapp with your own local Claude Code / Gemini CLI login (no API key).",
5
5
  "type": "module",
6
6
  "bin": {