fluncle 0.84.0 → 0.86.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/bin/fluncle.mjs +305 -253
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -538,7 +538,7 @@ function parseVersion(version) {
538
538
  var currentVersion;
539
539
  var init_version = __esm(() => {
540
540
  init_output();
541
- currentVersion = "0.84.0".trim() ? "0.84.0".trim() : "0.1.0";
541
+ currentVersion = "0.86.0".trim() ? "0.86.0".trim() : "0.1.0";
542
542
  });
543
543
 
544
544
  // src/update-notifier.ts
@@ -1329,186 +1329,6 @@ var init_mixtape_mixcloud = __esm(() => {
1329
1329
  PICTURE_MAX_BYTES = 10 * 1024 * 1024;
1330
1330
  });
1331
1331
 
1332
- // src/commands/mixtape-set-video.ts
1333
- var exports_mixtape_set_video = {};
1334
- __export(exports_mixtape_set_video, {
1335
- uploadRenditionMultipart: () => uploadRenditionMultipart,
1336
- stageSetVideo: () => stageSetVideo,
1337
- renditionFfmpegArgs: () => renditionFfmpegArgs,
1338
- planMultipart: () => planMultipart,
1339
- buildCompleteXml: () => buildCompleteXml,
1340
- SET_VIDEO_RENDITION: () => SET_VIDEO_RENDITION,
1341
- MIN_PART_SIZE: () => MIN_PART_SIZE,
1342
- MAX_PARTS: () => MAX_PARTS,
1343
- DEFAULT_PART_SIZE: () => DEFAULT_PART_SIZE
1344
- });
1345
- import { randomUUID } from "node:crypto";
1346
- import { existsSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
1347
- import { tmpdir } from "node:os";
1348
- import { join as join4 } from "node:path";
1349
- function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
1350
- if (!Number.isInteger(contentLength) || contentLength <= 0) {
1351
- throw new CliError2("invalid_size", `The rendition size must be a positive integer (got ${contentLength})`);
1352
- }
1353
- let effective = Math.max(partSize, MIN_PART_SIZE);
1354
- if (Math.ceil(contentLength / effective) > MAX_PARTS) {
1355
- effective = Math.ceil(contentLength / MAX_PARTS);
1356
- }
1357
- const parts = [];
1358
- let start = 0;
1359
- let partNumber = 1;
1360
- while (start < contentLength) {
1361
- const end = Math.min(start + effective, contentLength);
1362
- parts.push({ end, partNumber, size: end - start, start });
1363
- start = end;
1364
- partNumber += 1;
1365
- }
1366
- return { partCount: parts.length, partSize: effective, parts };
1367
- }
1368
- function escapeXml(value) {
1369
- return value.replace(/[<>&'"]/g, (char) => ({ '"': "&quot;", "&": "&amp;", "'": "&apos;", "<": "&lt;", ">": "&gt;" })[char] ?? char);
1370
- }
1371
- function buildCompleteXml(parts) {
1372
- const ordered = [...parts].sort((a, b) => a.partNumber - b.partNumber);
1373
- const body = ordered.map((part) => `<Part><PartNumber>${part.partNumber}</PartNumber><ETag>${escapeXml(part.etag)}</ETag></Part>`).join("");
1374
- return `<CompleteMultipartUpload>${body}</CompleteMultipartUpload>`;
1375
- }
1376
- function renditionFfmpegArgs(inputPath, outputPath) {
1377
- return [
1378
- "-y",
1379
- "-i",
1380
- inputPath,
1381
- "-vf",
1382
- `scale=-2:${SET_VIDEO_RENDITION.height}`,
1383
- "-c:v",
1384
- "libx264",
1385
- "-preset",
1386
- "medium",
1387
- "-crf",
1388
- String(SET_VIDEO_RENDITION.crf),
1389
- "-force_key_frames",
1390
- `expr:gte(t,n_forced*${SET_VIDEO_RENDITION.gopSeconds})`,
1391
- "-pix_fmt",
1392
- "yuv420p",
1393
- "-c:a",
1394
- "aac",
1395
- "-b:a",
1396
- SET_VIDEO_RENDITION.audioBitrate,
1397
- "-movflags",
1398
- "+faststart",
1399
- outputPath
1400
- ];
1401
- }
1402
- async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
1403
- if (!existsSync(masterPath)) {
1404
- throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
1405
- }
1406
- await assertFfmpeg();
1407
- const renditionPath = join4(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
1408
- onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
1409
- await deriveRendition(masterPath, renditionPath);
1410
- try {
1411
- const size = statSync2(renditionPath).size;
1412
- const plan = planMultipart(size);
1413
- onProgress(`Set video: uploading ${(size / 1e9).toFixed(2)} GB in ${plan.partCount} part(s)…`);
1414
- const presign = await adminApiPost(presignPath, {
1415
- partCount: plan.partCount
1416
- });
1417
- const urlByPart = new Map(presign.parts.map((part) => [part.partNumber, part.url]));
1418
- const completed = [];
1419
- try {
1420
- for (const part of plan.parts) {
1421
- const url = urlByPart.get(part.partNumber);
1422
- if (!url) {
1423
- throw new CliError2("presign_missing", `Worker did not sign part ${part.partNumber} of ${plan.partCount}`);
1424
- }
1425
- onProgress(`Set video: part ${part.partNumber}/${plan.partCount}`);
1426
- const etag = await putPart(url, renditionPath, part);
1427
- completed.push({ etag, partNumber: part.partNumber });
1428
- }
1429
- await completeUpload(presign.completeUrl, completed);
1430
- } catch (error) {
1431
- await abortUpload(presign.abortUrl).catch(() => {});
1432
- throw error;
1433
- }
1434
- return { key: presign.key, url: `${FOUND_BASE}/${presign.key}` };
1435
- } finally {
1436
- rmSync2(renditionPath, { force: true });
1437
- }
1438
- }
1439
- async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
1440
- const result = await uploadRenditionMultipart(masterPath, `/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/set-video/presign`, onProgress);
1441
- await adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}`, {
1442
- setVideoAt: new Date().toISOString()
1443
- });
1444
- onProgress("Set video: flipped setVideoAt — the /log player + video SEO are live.");
1445
- return result;
1446
- }
1447
- async function putPart(url, path2, part) {
1448
- const response = await fetch(url, {
1449
- body: Bun.file(path2).slice(part.start, part.end),
1450
- method: "PUT"
1451
- });
1452
- if (!response.ok) {
1453
- const detail = (await response.text().catch(() => "")).slice(0, 300);
1454
- throw new CliError2("r2_part_failed", `R2 rejected part ${part.partNumber} (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}`);
1455
- }
1456
- const etag = response.headers.get("etag");
1457
- if (!etag) {
1458
- throw new CliError2("r2_no_etag", `R2 returned no ETag for part ${part.partNumber}`);
1459
- }
1460
- return etag;
1461
- }
1462
- async function completeUpload(url, parts) {
1463
- const response = await fetch(url, {
1464
- body: buildCompleteXml(parts),
1465
- headers: { "content-type": "application/xml" },
1466
- method: "POST"
1467
- });
1468
- const text = await response.text().catch(() => "");
1469
- if (!response.ok || text.includes("<Error>")) {
1470
- throw new CliError2("r2_complete_failed", `R2 CompleteMultipartUpload failed (${response.status} ${response.statusText})${text ? `: ${text.slice(0, 300)}` : ""}`);
1471
- }
1472
- }
1473
- async function abortUpload(url) {
1474
- await fetch(url, { method: "DELETE" });
1475
- }
1476
- async function assertFfmpeg() {
1477
- try {
1478
- const proc = Bun.spawn(["ffmpeg", "-version"], { stderr: "ignore", stdout: "ignore" });
1479
- await proc.exited;
1480
- if (proc.exitCode !== 0) {
1481
- throw new Error("ffmpeg -version exited non-zero");
1482
- }
1483
- } catch {
1484
- throw new CliError2("ffmpeg_missing", "--set-video needs ffmpeg to derive the 1080p rendition. Install it (brew install ffmpeg).");
1485
- }
1486
- }
1487
- async function deriveRendition(inputPath, outputPath) {
1488
- const proc = Bun.spawn(["ffmpeg", ...renditionFfmpegArgs(inputPath, outputPath)], {
1489
- stderr: "pipe",
1490
- stdout: "ignore"
1491
- });
1492
- await proc.exited;
1493
- if (proc.exitCode !== 0) {
1494
- const detail = (await new Response(proc.stderr).text().catch(() => "")).slice(-400);
1495
- throw new CliError2("ffmpeg_failed", `ffmpeg failed to derive the set-video rendition${detail ? `: ${detail}` : ""}`);
1496
- }
1497
- }
1498
- var FOUND_BASE = "https://found.fluncle.com", SET_VIDEO_RENDITION, DEFAULT_PART_SIZE, MIN_PART_SIZE, MAX_PARTS = 1e4;
1499
- var init_mixtape_set_video = __esm(() => {
1500
- init_api();
1501
- init_output();
1502
- SET_VIDEO_RENDITION = {
1503
- audioBitrate: "192k",
1504
- crf: 20,
1505
- gopSeconds: 2,
1506
- height: 1080
1507
- };
1508
- DEFAULT_PART_SIZE = 100 * 1024 * 1024;
1509
- MIN_PART_SIZE = 5 * 1024 * 1024;
1510
- });
1511
-
1512
1332
  // src/commands/mixtapes.ts
1513
1333
  var exports_mixtapes = {};
1514
1334
  __export(exports_mixtapes, {
@@ -1523,7 +1343,7 @@ __export(exports_mixtapes, {
1523
1343
  mixtapeDeleteCommand: () => mixtapeDeleteCommand,
1524
1344
  mixtapeCreateCommand: () => mixtapeCreateCommand
1525
1345
  });
1526
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
1346
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1527
1347
  async function mixtapesCommand() {
1528
1348
  const response = await publicApiGet("/api/mixtapes");
1529
1349
  return response.mixtapes;
@@ -1552,21 +1372,24 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
1552
1372
  if (!mixtape.id) {
1553
1373
  throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
1554
1374
  }
1555
- const both = !options.youtube && !options.mixcloud && !options.setVideo;
1375
+ if (mixtape.status === "draft") {
1376
+ throw new CliError2("mixtape_not_promoted", `${mixtape.id} is still a draft — promote its recording first:
1377
+ ` + " fluncle admin recordings promote <recordingId>");
1378
+ }
1379
+ const both = !options.youtube && !options.mixcloud;
1556
1380
  const doYoutube = both || Boolean(options.youtube);
1557
1381
  const doMixcloud = both || Boolean(options.mixcloud);
1558
- const doSetVideo = Boolean(options.setVideo);
1559
1382
  if (doYoutube && !options.video) {
1560
1383
  throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
1561
1384
  }
1562
1385
  if (doMixcloud && !options.audio) {
1563
1386
  throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
1564
1387
  }
1565
- if (doSetVideo && !options.video) {
1566
- throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
1567
- }
1568
1388
  const mixtapeId = mixtape.id;
1569
- let logId = mixtape.logId;
1389
+ const logId = mixtape.logId;
1390
+ if (!logId) {
1391
+ throw new CliError2("missing_log_id", "The mixtape has no Log ID — was it promoted successfully?");
1392
+ }
1570
1393
  if (!mixtape.durationMs) {
1571
1394
  const source = options.audio ?? options.video;
1572
1395
  const durationMs = source ? await probeDurationMs(source) : undefined;
@@ -1575,18 +1398,10 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
1575
1398
  onProgress(`Duration: ${Math.round(durationMs / 60000)} min (from the upload).`);
1576
1399
  }
1577
1400
  }
1578
- if (mixtape.status === "draft") {
1579
- onProgress("Minting the coordinate…");
1580
- const published = await mixtapePublishCommand(mixtapeId);
1581
- logId = published.mixtape.logId;
1582
- onProgress(`Minted ${logId}.`);
1583
- } else if (mixtape.status === "published") {
1401
+ if (mixtape.status === "published") {
1584
1402
  onProgress(`Already published (${logId}); re-distributing.`);
1585
1403
  } else {
1586
- onProgress(`Resuming distribution for ${logId}.`);
1587
- }
1588
- if (!logId) {
1589
- throw new CliError2("mint_failed", "The mixtape has no Log ID after minting");
1404
+ onProgress(`Distributing ${logId} (promoted — coordinate already minted).`);
1590
1405
  }
1591
1406
  const results = [];
1592
1407
  if (doYoutube) {
@@ -1609,16 +1424,6 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
1609
1424
  results.push({ platform: "mixcloud", url: result.url });
1610
1425
  onProgress(`Mixcloud: ${result.url}`);
1611
1426
  }
1612
- if (doSetVideo) {
1613
- if (!options.video) {
1614
- throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
1615
- }
1616
- onProgress("Set video: staging the 1080p rendition…");
1617
- const { stageSetVideo: stageSetVideo2 } = await Promise.resolve().then(() => (init_mixtape_set_video(), exports_mixtape_set_video));
1618
- const result = await stageSetVideo2(mixtapeId, options.video, onProgress);
1619
- results.push({ platform: "set-video", url: result.url });
1620
- onProgress(`Set video: ${result.url}`);
1621
- }
1622
1427
  return { logId, mixtapeId, results };
1623
1428
  }
1624
1429
  async function mixtapeResyncCommand(idOrLogId, options, onProgress = () => {}) {
@@ -1691,7 +1496,7 @@ function buildBody(options) {
1691
1496
  return body;
1692
1497
  }
1693
1498
  function parseCueFile(filePath) {
1694
- if (!existsSync2(filePath)) {
1499
+ if (!existsSync(filePath)) {
1695
1500
  throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
1696
1501
  }
1697
1502
  const text = readFileSync2(filePath, "utf-8");
@@ -2446,7 +2251,7 @@ function parseVersion2(version) {
2446
2251
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2447
2252
  var init_version2 = __esm(() => {
2448
2253
  init_output();
2449
- currentVersion2 = "0.84.0".trim() ? "0.84.0".trim() : "0.1.0";
2254
+ currentVersion2 = "0.86.0".trim() ? "0.86.0".trim() : "0.1.0";
2450
2255
  });
2451
2256
 
2452
2257
  // ../../packages/registry/src/index.ts
@@ -3200,7 +3005,7 @@ async function trackVideoCommand(idOrLogId, files, onProgress) {
3200
3005
  const detail = (await response.text().catch(() => "")).slice(0, 300);
3201
3006
  throw new CliError2("r2_put_failed", `R2 rejected ${spec.field} with ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`);
3202
3007
  }
3203
- urls[spec.field] = `${FOUND_BASE2}/${upload.key}`;
3008
+ urls[spec.field] = `${FOUND_BASE}/${upload.key}`;
3204
3009
  }
3205
3010
  const manifest = files.render ? await readManifestFields(files.render) : {};
3206
3011
  const videoModel = files.model?.trim().slice(0, 120) || manifest.model || DEFAULT_VIDEO_MODEL;
@@ -3311,7 +3116,7 @@ async function trackNoteCommand(idOrLogId, options) {
3311
3116
  const body = { note: options.note };
3312
3117
  return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
3313
3118
  }
3314
- var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE2 = "https://found.fluncle.com", VIDEO_FIELDS;
3119
+ var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS;
3315
3120
  var init_track = __esm(() => {
3316
3121
  init_api();
3317
3122
  init_output();
@@ -3705,7 +3510,7 @@ __export(exports_mixtape_youtube2, {
3705
3510
  distributeYoutube: () => distributeYoutube2,
3706
3511
  authYoutubeCommand: () => authYoutubeCommand2
3707
3512
  });
3708
- import { statSync as statSync3 } from "node:fs";
3513
+ import { statSync as statSync2 } from "node:fs";
3709
3514
  async function distributeYoutube2(mixtapeId, videoPath, onProgress) {
3710
3515
  const contentLength = fileSize2(videoPath);
3711
3516
  const contentType = "video/mp4";
@@ -3841,7 +3646,7 @@ async function resolveMixtapeId2(idOrLogId) {
3841
3646
  }
3842
3647
  function fileSize2(path2) {
3843
3648
  try {
3844
- return statSync3(path2).size;
3649
+ return statSync2(path2).size;
3845
3650
  } catch {
3846
3651
  throw new CliError2("video_not_found", `Cannot read video file at ${path2}`);
3847
3652
  }
@@ -3861,11 +3666,172 @@ var init_mixtape_youtube2 = __esm(() => {
3861
3666
  init_output();
3862
3667
  });
3863
3668
 
3669
+ // src/commands/mixtape-set-video.ts
3670
+ import { randomUUID } from "node:crypto";
3671
+ import { existsSync as existsSync2, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
3672
+ import { tmpdir } from "node:os";
3673
+ import { join as join4 } from "node:path";
3674
+ function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
3675
+ if (!Number.isInteger(contentLength) || contentLength <= 0) {
3676
+ throw new CliError2("invalid_size", `The rendition size must be a positive integer (got ${contentLength})`);
3677
+ }
3678
+ let effective = Math.max(partSize, MIN_PART_SIZE);
3679
+ if (Math.ceil(contentLength / effective) > MAX_PARTS) {
3680
+ effective = Math.ceil(contentLength / MAX_PARTS);
3681
+ }
3682
+ const parts = [];
3683
+ let start = 0;
3684
+ let partNumber = 1;
3685
+ while (start < contentLength) {
3686
+ const end = Math.min(start + effective, contentLength);
3687
+ parts.push({ end, partNumber, size: end - start, start });
3688
+ start = end;
3689
+ partNumber += 1;
3690
+ }
3691
+ return { partCount: parts.length, partSize: effective, parts };
3692
+ }
3693
+ function escapeXml(value) {
3694
+ return value.replace(/[<>&'"]/g, (char) => ({ '"': "&quot;", "&": "&amp;", "'": "&apos;", "<": "&lt;", ">": "&gt;" })[char] ?? char);
3695
+ }
3696
+ function buildCompleteXml(parts) {
3697
+ const ordered = [...parts].sort((a, b) => a.partNumber - b.partNumber);
3698
+ const body = ordered.map((part) => `<Part><PartNumber>${part.partNumber}</PartNumber><ETag>${escapeXml(part.etag)}</ETag></Part>`).join("");
3699
+ return `<CompleteMultipartUpload>${body}</CompleteMultipartUpload>`;
3700
+ }
3701
+ function renditionFfmpegArgs(inputPath, outputPath) {
3702
+ return [
3703
+ "-y",
3704
+ "-i",
3705
+ inputPath,
3706
+ "-vf",
3707
+ `scale=-2:${SET_VIDEO_RENDITION.height}`,
3708
+ "-c:v",
3709
+ "libx264",
3710
+ "-preset",
3711
+ "medium",
3712
+ "-crf",
3713
+ String(SET_VIDEO_RENDITION.crf),
3714
+ "-force_key_frames",
3715
+ `expr:gte(t,n_forced*${SET_VIDEO_RENDITION.gopSeconds})`,
3716
+ "-pix_fmt",
3717
+ "yuv420p",
3718
+ "-c:a",
3719
+ "aac",
3720
+ "-b:a",
3721
+ SET_VIDEO_RENDITION.audioBitrate,
3722
+ "-movflags",
3723
+ "+faststart",
3724
+ outputPath
3725
+ ];
3726
+ }
3727
+ async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
3728
+ if (!existsSync2(masterPath)) {
3729
+ throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
3730
+ }
3731
+ await assertFfmpeg();
3732
+ const renditionPath = join4(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
3733
+ onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
3734
+ await deriveRendition(masterPath, renditionPath);
3735
+ try {
3736
+ const size = statSync3(renditionPath).size;
3737
+ const plan = planMultipart(size);
3738
+ onProgress(`Set video: uploading ${(size / 1e9).toFixed(2)} GB in ${plan.partCount} part(s)…`);
3739
+ const presign = await adminApiPost(presignPath, {
3740
+ partCount: plan.partCount
3741
+ });
3742
+ const urlByPart = new Map(presign.parts.map((part) => [part.partNumber, part.url]));
3743
+ const completed = [];
3744
+ try {
3745
+ for (const part of plan.parts) {
3746
+ const url = urlByPart.get(part.partNumber);
3747
+ if (!url) {
3748
+ throw new CliError2("presign_missing", `Worker did not sign part ${part.partNumber} of ${plan.partCount}`);
3749
+ }
3750
+ onProgress(`Set video: part ${part.partNumber}/${plan.partCount}`);
3751
+ const etag = await putPart(url, renditionPath, part);
3752
+ completed.push({ etag, partNumber: part.partNumber });
3753
+ }
3754
+ await completeUpload(presign.completeUrl, completed);
3755
+ } catch (error) {
3756
+ await abortUpload(presign.abortUrl).catch(() => {});
3757
+ throw error;
3758
+ }
3759
+ return { key: presign.key, url: `${FOUND_BASE2}/${presign.key}` };
3760
+ } finally {
3761
+ rmSync2(renditionPath, { force: true });
3762
+ }
3763
+ }
3764
+ async function putPart(url, path2, part) {
3765
+ const response = await fetch(url, {
3766
+ body: Bun.file(path2).slice(part.start, part.end),
3767
+ method: "PUT"
3768
+ });
3769
+ if (!response.ok) {
3770
+ const detail = (await response.text().catch(() => "")).slice(0, 300);
3771
+ throw new CliError2("r2_part_failed", `R2 rejected part ${part.partNumber} (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}`);
3772
+ }
3773
+ const etag = response.headers.get("etag");
3774
+ if (!etag) {
3775
+ throw new CliError2("r2_no_etag", `R2 returned no ETag for part ${part.partNumber}`);
3776
+ }
3777
+ return etag;
3778
+ }
3779
+ async function completeUpload(url, parts) {
3780
+ const response = await fetch(url, {
3781
+ body: buildCompleteXml(parts),
3782
+ headers: { "content-type": "application/xml" },
3783
+ method: "POST"
3784
+ });
3785
+ const text = await response.text().catch(() => "");
3786
+ if (!response.ok || text.includes("<Error>")) {
3787
+ throw new CliError2("r2_complete_failed", `R2 CompleteMultipartUpload failed (${response.status} ${response.statusText})${text ? `: ${text.slice(0, 300)}` : ""}`);
3788
+ }
3789
+ }
3790
+ async function abortUpload(url) {
3791
+ await fetch(url, { method: "DELETE" });
3792
+ }
3793
+ async function assertFfmpeg() {
3794
+ try {
3795
+ const proc = Bun.spawn(["ffmpeg", "-version"], { stderr: "ignore", stdout: "ignore" });
3796
+ await proc.exited;
3797
+ if (proc.exitCode !== 0) {
3798
+ throw new Error("ffmpeg -version exited non-zero");
3799
+ }
3800
+ } catch {
3801
+ throw new CliError2("ffmpeg_missing", "--set-video needs ffmpeg to derive the 1080p rendition. Install it (brew install ffmpeg).");
3802
+ }
3803
+ }
3804
+ async function deriveRendition(inputPath, outputPath) {
3805
+ const proc = Bun.spawn(["ffmpeg", ...renditionFfmpegArgs(inputPath, outputPath)], {
3806
+ stderr: "pipe",
3807
+ stdout: "ignore"
3808
+ });
3809
+ await proc.exited;
3810
+ if (proc.exitCode !== 0) {
3811
+ const detail = (await new Response(proc.stderr).text().catch(() => "")).slice(-400);
3812
+ throw new CliError2("ffmpeg_failed", `ffmpeg failed to derive the set-video rendition${detail ? `: ${detail}` : ""}`);
3813
+ }
3814
+ }
3815
+ var FOUND_BASE2 = "https://found.fluncle.com", SET_VIDEO_RENDITION, DEFAULT_PART_SIZE, MIN_PART_SIZE, MAX_PARTS = 1e4;
3816
+ var init_mixtape_set_video = __esm(() => {
3817
+ init_api();
3818
+ init_output();
3819
+ SET_VIDEO_RENDITION = {
3820
+ audioBitrate: "192k",
3821
+ crf: 20,
3822
+ gopSeconds: 2,
3823
+ height: 1080
3824
+ };
3825
+ DEFAULT_PART_SIZE = 100 * 1024 * 1024;
3826
+ MIN_PART_SIZE = 5 * 1024 * 1024;
3827
+ });
3828
+
3864
3829
  // src/commands/recordings.ts
3865
3830
  var exports_recordings = {};
3866
3831
  __export(exports_recordings, {
3867
3832
  recordingsListCommand: () => recordingsListCommand,
3868
3833
  recordingUpdateCommand: () => recordingUpdateCommand,
3834
+ recordingReplaceCuesCommand: () => recordingReplaceCuesCommand,
3869
3835
  recordingPromoteCommand: () => recordingPromoteCommand,
3870
3836
  recordingGetCommand: () => recordingGetCommand,
3871
3837
  recordingGet: () => recordingGet,
@@ -3878,12 +3844,21 @@ async function recordingGet(id) {
3878
3844
  return response.recording;
3879
3845
  }
3880
3846
  function formatRecordingSummary(recording) {
3847
+ const kind = recording.hasVideo ? `take v${recording.version}` : "plan";
3881
3848
  const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
3882
3849
  const cues = recording.tracklist.length;
3883
- return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
3850
+ return `${recording.id} [${kind}] ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
3884
3851
  }
3885
3852
  async function recordingsListCommand(options = {}) {
3886
- const response = await adminApiGet("/api/admin/recordings");
3853
+ const query = new URLSearchParams;
3854
+ if (options.kind) {
3855
+ query.set("kind", options.kind);
3856
+ }
3857
+ if (options.parentId) {
3858
+ query.set("parentId", options.parentId);
3859
+ }
3860
+ const suffix = query.toString() ? `?${query.toString()}` : "";
3861
+ const response = await adminApiGet(`/api/admin/recordings${suffix}`);
3887
3862
  if (options.json) {
3888
3863
  printJson2({ ok: true, recordings: response.recordings });
3889
3864
  return;
@@ -3912,9 +3887,22 @@ async function recordingGetCommand(id, options = {}) {
3912
3887
  }
3913
3888
  }
3914
3889
  async function recordingCreateCommand(options = {}) {
3890
+ if (options.plan) {
3891
+ const created2 = await adminApiPost("/api/admin/recordings", {
3892
+ kind: "plan",
3893
+ recordedAt: options.recordedAt
3894
+ });
3895
+ const recording2 = created2.recording;
3896
+ if (options.json) {
3897
+ printJson2({ ok: true, recording: recording2 });
3898
+ return;
3899
+ }
3900
+ console.log(`Plan ${recording2.id} created — handle "${recording2.title}"`);
3901
+ return;
3902
+ }
3915
3903
  const title = options.title?.trim();
3916
3904
  if (!title) {
3917
- throw new CliError2("missing_title", "A recording needs a --title");
3905
+ throw new CliError2("missing_title", "A recording needs a --title (or --plan for a videoless plan)");
3918
3906
  }
3919
3907
  if (!options.video) {
3920
3908
  throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
@@ -3951,6 +3939,9 @@ async function recordingUpdateCommand(id, options = {}) {
3951
3939
  if (options.recordedAt !== undefined) {
3952
3940
  body.recordedAt = options.recordedAt;
3953
3941
  }
3942
+ if (options.parentId !== undefined) {
3943
+ body.parentId = options.parentId;
3944
+ }
3954
3945
  if (options.tracklistFile !== undefined) {
3955
3946
  if (!existsSync3(options.tracklistFile)) {
3956
3947
  throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
@@ -3962,7 +3953,7 @@ async function recordingUpdateCommand(id, options = {}) {
3962
3953
  }
3963
3954
  }
3964
3955
  if (Object.keys(body).length === 0) {
3965
- throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
3956
+ throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, --parent-id, or --tracklist-file");
3966
3957
  }
3967
3958
  const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
3968
3959
  if (options.json) {
@@ -3994,6 +3985,29 @@ async function recordingPromoteCommand(id, options = {}) {
3994
3985
  }
3995
3986
  console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
3996
3987
  }
3988
+ async function recordingReplaceCuesCommand(id, options = {}) {
3989
+ if (!id) {
3990
+ throw new CliError2("missing_id", "Missing recording id for: replace-cues");
3991
+ }
3992
+ if (!options.cuesFile) {
3993
+ throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
3994
+ }
3995
+ if (!existsSync3(options.cuesFile)) {
3996
+ throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
3997
+ }
3998
+ let cues;
3999
+ try {
4000
+ cues = JSON.parse(readFileSync3(options.cuesFile, "utf8"));
4001
+ } catch {
4002
+ throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
4003
+ }
4004
+ const response = await adminApiPut(`/api/admin/recordings/${encodeURIComponent(id)}/cues`, { cues });
4005
+ if (options.json) {
4006
+ printJson2({ ok: true, recording: response.recording });
4007
+ return;
4008
+ }
4009
+ console.log(`Replaced cues for ${formatRecordingSummary(response.recording)}`);
4010
+ }
3997
4011
  var init_recordings = __esm(() => {
3998
4012
  init_api();
3999
4013
  init_output();
@@ -4043,21 +4057,24 @@ async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () =>
4043
4057
  if (!mixtape.id) {
4044
4058
  throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
4045
4059
  }
4046
- const both = !options.youtube && !options.mixcloud && !options.setVideo;
4060
+ if (mixtape.status === "draft") {
4061
+ throw new CliError2("mixtape_not_promoted", `${mixtape.id} is still a draft — promote its recording first:
4062
+ ` + " fluncle admin recordings promote <recordingId>");
4063
+ }
4064
+ const both = !options.youtube && !options.mixcloud;
4047
4065
  const doYoutube = both || Boolean(options.youtube);
4048
4066
  const doMixcloud = both || Boolean(options.mixcloud);
4049
- const doSetVideo = Boolean(options.setVideo);
4050
4067
  if (doYoutube && !options.video) {
4051
4068
  throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
4052
4069
  }
4053
4070
  if (doMixcloud && !options.audio) {
4054
4071
  throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
4055
4072
  }
4056
- if (doSetVideo && !options.video) {
4057
- throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
4058
- }
4059
4073
  const mixtapeId = mixtape.id;
4060
- let logId = mixtape.logId;
4074
+ const logId = mixtape.logId;
4075
+ if (!logId) {
4076
+ throw new CliError2("missing_log_id", "The mixtape has no Log ID — was it promoted successfully?");
4077
+ }
4061
4078
  if (!mixtape.durationMs) {
4062
4079
  const source = options.audio ?? options.video;
4063
4080
  const durationMs = source ? await probeDurationMs2(source) : undefined;
@@ -4066,18 +4083,10 @@ async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () =>
4066
4083
  onProgress(`Duration: ${Math.round(durationMs / 60000)} min (from the upload).`);
4067
4084
  }
4068
4085
  }
4069
- if (mixtape.status === "draft") {
4070
- onProgress("Minting the coordinate…");
4071
- const published = await mixtapePublishCommand2(mixtapeId);
4072
- logId = published.mixtape.logId;
4073
- onProgress(`Minted ${logId}.`);
4074
- } else if (mixtape.status === "published") {
4086
+ if (mixtape.status === "published") {
4075
4087
  onProgress(`Already published (${logId}); re-distributing.`);
4076
4088
  } else {
4077
- onProgress(`Resuming distribution for ${logId}.`);
4078
- }
4079
- if (!logId) {
4080
- throw new CliError2("mint_failed", "The mixtape has no Log ID after minting");
4089
+ onProgress(`Distributing ${logId} (promoted — coordinate already minted).`);
4081
4090
  }
4082
4091
  const results = [];
4083
4092
  if (doYoutube) {
@@ -4100,16 +4109,6 @@ async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () =>
4100
4109
  results.push({ platform: "mixcloud", url: result.url });
4101
4110
  onProgress(`Mixcloud: ${result.url}`);
4102
4111
  }
4103
- if (doSetVideo) {
4104
- if (!options.video) {
4105
- throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
4106
- }
4107
- onProgress("Set video: staging the 1080p rendition…");
4108
- const { stageSetVideo: stageSetVideo2 } = await Promise.resolve().then(() => (init_mixtape_set_video(), exports_mixtape_set_video));
4109
- const result = await stageSetVideo2(mixtapeId, options.video, onProgress);
4110
- results.push({ platform: "set-video", url: result.url });
4111
- onProgress(`Set video: ${result.url}`);
4112
- }
4113
4112
  return { logId, mixtapeId, results };
4114
4113
  }
4115
4114
  async function mixtapeResyncCommand2(idOrLogId, options, onProgress = () => {}) {
@@ -4352,7 +4351,7 @@ async function resolveClipSource(clip) {
4352
4351
  throw new CliError2("mixtape_no_log_id", `Mixtape ${clip.mixtapeId} has no committed Log ID`);
4353
4352
  }
4354
4353
  if (!mixtape.setVideoAt) {
4355
- throw new CliError2("set_not_staged", `Mixtape ${mixtape.logId} has no staged set video — run \`distribute --set-video\` first`);
4354
+ throw new CliError2("set_not_staged", `Mixtape ${mixtape.logId} has no staged set video — run \`recordings promote <recordingId>\` first`);
4356
4355
  }
4357
4356
  return { setUrl: setVideoUrl(mixtape.logId) };
4358
4357
  }
@@ -4441,6 +4440,7 @@ var exports_recordings2 = {};
4441
4440
  __export(exports_recordings2, {
4442
4441
  recordingsListCommand: () => recordingsListCommand2,
4443
4442
  recordingUpdateCommand: () => recordingUpdateCommand2,
4443
+ recordingReplaceCuesCommand: () => recordingReplaceCuesCommand2,
4444
4444
  recordingPromoteCommand: () => recordingPromoteCommand2,
4445
4445
  recordingGetCommand: () => recordingGetCommand2,
4446
4446
  recordingGet: () => recordingGet2,
@@ -4453,12 +4453,21 @@ async function recordingGet2(id) {
4453
4453
  return response.recording;
4454
4454
  }
4455
4455
  function formatRecordingSummary2(recording) {
4456
+ const kind = recording.hasVideo ? `take v${recording.version}` : "plan";
4456
4457
  const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
4457
4458
  const cues = recording.tracklist.length;
4458
- return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
4459
+ return `${recording.id} [${kind}] ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
4459
4460
  }
4460
4461
  async function recordingsListCommand2(options = {}) {
4461
- const response = await adminApiGet("/api/admin/recordings");
4462
+ const query = new URLSearchParams;
4463
+ if (options.kind) {
4464
+ query.set("kind", options.kind);
4465
+ }
4466
+ if (options.parentId) {
4467
+ query.set("parentId", options.parentId);
4468
+ }
4469
+ const suffix = query.toString() ? `?${query.toString()}` : "";
4470
+ const response = await adminApiGet(`/api/admin/recordings${suffix}`);
4462
4471
  if (options.json) {
4463
4472
  printJson2({ ok: true, recordings: response.recordings });
4464
4473
  return;
@@ -4487,9 +4496,22 @@ async function recordingGetCommand2(id, options = {}) {
4487
4496
  }
4488
4497
  }
4489
4498
  async function recordingCreateCommand2(options = {}) {
4499
+ if (options.plan) {
4500
+ const created2 = await adminApiPost("/api/admin/recordings", {
4501
+ kind: "plan",
4502
+ recordedAt: options.recordedAt
4503
+ });
4504
+ const recording2 = created2.recording;
4505
+ if (options.json) {
4506
+ printJson2({ ok: true, recording: recording2 });
4507
+ return;
4508
+ }
4509
+ console.log(`Plan ${recording2.id} created — handle "${recording2.title}"`);
4510
+ return;
4511
+ }
4490
4512
  const title = options.title?.trim();
4491
4513
  if (!title) {
4492
- throw new CliError2("missing_title", "A recording needs a --title");
4514
+ throw new CliError2("missing_title", "A recording needs a --title (or --plan for a videoless plan)");
4493
4515
  }
4494
4516
  if (!options.video) {
4495
4517
  throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
@@ -4526,6 +4548,9 @@ async function recordingUpdateCommand2(id, options = {}) {
4526
4548
  if (options.recordedAt !== undefined) {
4527
4549
  body.recordedAt = options.recordedAt;
4528
4550
  }
4551
+ if (options.parentId !== undefined) {
4552
+ body.parentId = options.parentId;
4553
+ }
4529
4554
  if (options.tracklistFile !== undefined) {
4530
4555
  if (!existsSync5(options.tracklistFile)) {
4531
4556
  throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
@@ -4537,7 +4562,7 @@ async function recordingUpdateCommand2(id, options = {}) {
4537
4562
  }
4538
4563
  }
4539
4564
  if (Object.keys(body).length === 0) {
4540
- throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
4565
+ throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, --parent-id, or --tracklist-file");
4541
4566
  }
4542
4567
  const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
4543
4568
  if (options.json) {
@@ -4569,6 +4594,29 @@ async function recordingPromoteCommand2(id, options = {}) {
4569
4594
  }
4570
4595
  console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
4571
4596
  }
4597
+ async function recordingReplaceCuesCommand2(id, options = {}) {
4598
+ if (!id) {
4599
+ throw new CliError2("missing_id", "Missing recording id for: replace-cues");
4600
+ }
4601
+ if (!options.cuesFile) {
4602
+ throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
4603
+ }
4604
+ if (!existsSync5(options.cuesFile)) {
4605
+ throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
4606
+ }
4607
+ let cues;
4608
+ try {
4609
+ cues = JSON.parse(readFileSync5(options.cuesFile, "utf8"));
4610
+ } catch {
4611
+ throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
4612
+ }
4613
+ const response = await adminApiPut(`/api/admin/recordings/${encodeURIComponent(id)}/cues`, { cues });
4614
+ if (options.json) {
4615
+ printJson2({ ok: true, recording: response.recording });
4616
+ return;
4617
+ }
4618
+ console.log(`Replaced cues for ${formatRecordingSummary2(response.recording)}`);
4619
+ }
4572
4620
  var init_recordings2 = __esm(() => {
4573
4621
  init_api();
4574
4622
  init_output();
@@ -7598,7 +7646,7 @@ function addAdminCommands(program2) {
7598
7646
  const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7599
7647
  await runMixtapeGet(idOrLogId, options, mixtapeGetCommand2);
7600
7648
  });
7601
- adminMixtapes.command("distribute").description("Mint + push a mixtape to YouTube (video) and Mixcloud (audio)").argument("[idOrLogId]").option("--video <file>", "Video file for YouTube").option("--audio <file>", "Audio file for Mixcloud").option("--youtube", "Only distribute to YouTube").option("--mixcloud", "Only distribute to Mixcloud").option("--set-video", "Also stage a 1080p set-video rendition (from --video) to R2 + flip setVideoAt for the /log player").option("--unlisted", "Keep Mixcloud private too (YouTube is always unlisted until publish-youtube)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
7649
+ adminMixtapes.command("distribute").description("Push a promoted mixtape to YouTube (video) and Mixcloud (audio). The mixtape must already be promoted (`recordings promote`) \u2014 distribute is push-only.").argument("[idOrLogId]").option("--video <file>", "Video file for YouTube").option("--audio <file>", "Audio file for Mixcloud").option("--youtube", "Only distribute to YouTube").option("--mixcloud", "Only distribute to Mixcloud").option("--unlisted", "Keep Mixcloud private too (YouTube is always unlisted until publish-youtube)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
7602
7650
  const { mixtapeDistributeCommand: mixtapeDistributeCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
7603
7651
  await runMixtapeDistribute(idOrLogId, options, mixtapeDistributeCommand3);
7604
7652
  });
@@ -7626,11 +7674,11 @@ function addAdminCommands(program2) {
7626
7674
  adminRecordings.action(() => {
7627
7675
  adminRecordings.outputHelp();
7628
7676
  });
7629
- adminRecordings.command("create").description("Create a recording + stage its set video (from --video)").option("--title <text>", "Recording title").option("--video <file>", "Set-video master to stage (a 1080p rendition is derived)").option("--recorded-at <date>", "Recorded date (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
7677
+ adminRecordings.command("create").description("Create a recording (--video to stage a take, or --plan for a videoless plan)").option("--plan", "Create a videoless PLAN (server mints a Galaxy-vocab handle)", false).option("--title <text>", "Recording title (a take)").option("--video <file>", "Set-video master to stage (a 1080p rendition is derived)").option("--recorded-at <date>", "Recorded date (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
7630
7678
  const { recordingCreateCommand: recordingCreateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
7631
7679
  await recordingCreateCommand3(options);
7632
7680
  });
7633
- adminRecordings.command("list").description("List every recording").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
7681
+ adminRecordings.command("list").description("List recordings (--kind plan|take, --parent-id <plan> for a plan's takes)").option("--kind <kind>", "Filter by kind: plan | take").option("--parent-id <id>", "List the takes attached to this plan").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
7634
7682
  const { recordingsListCommand: recordingsListCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
7635
7683
  await recordingsListCommand3(options);
7636
7684
  });
@@ -7638,10 +7686,14 @@ function addAdminCommands(program2) {
7638
7686
  const { recordingGetCommand: recordingGetCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
7639
7687
  await recordingGetCommand3(id, options);
7640
7688
  });
7641
- adminRecordings.command("update").description("Update a recording's title, recorded date, or tracklist (--tracklist-file)").argument("[id]").option("--title <text>", "Recording title").option("--recorded-at <date>", "Recorded date (ISO)").option("--tracklist-file <file>", "JSON file with the whole cue tracklist array").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
7689
+ adminRecordings.command("update").description("Update a recording's title/recorded date/tracklist, or attach it to a plan").argument("[id]").option("--title <text>", "Recording title").option("--recorded-at <date>", "Recorded date (ISO)").option("--parent-id <id>", "Attach this take to its plan (assigns the take's version)").option("--tracklist-file <file>", "JSON file with the whole cue tracklist array").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
7642
7690
  const { recordingUpdateCommand: recordingUpdateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
7643
7691
  await recordingUpdateCommand3(id, options);
7644
7692
  });
7693
+ adminRecordings.command("replace-cues").description("Replace a recording's whole cue tracklist from a JSON file (--cues-file)").argument("[id]").option("--cues-file <file>", "JSON file with the ordered cue array").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
7694
+ const { recordingReplaceCuesCommand: recordingReplaceCuesCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
7695
+ await recordingReplaceCuesCommand3(id, options);
7696
+ });
7645
7697
  adminRecordings.command("delete").description("Delete a recording (cascade its clips)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
7646
7698
  const { recordingDeleteCommand: recordingDeleteCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
7647
7699
  await recordingDeleteCommand3(id, options);
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.84.0"
34
+ "version": "0.86.0"
35
35
  }