fluncle 0.51.0 → 0.53.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 +153 -27
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -423,7 +423,7 @@ function parseVersion(version) {
423
423
  var currentVersion;
424
424
  var init_version = __esm(() => {
425
425
  init_output();
426
- currentVersion = "0.51.0".trim() ? "0.51.0".trim() : "0.1.0";
426
+ currentVersion = "0.53.0".trim() ? "0.53.0".trim() : "0.1.0";
427
427
  });
428
428
 
429
429
  // src/update-notifier.ts
@@ -1912,7 +1912,7 @@ function parseVersion2(version) {
1912
1912
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
1913
1913
  var init_version2 = __esm(() => {
1914
1914
  init_output();
1915
- currentVersion2 = "0.51.0".trim() ? "0.51.0".trim() : "0.1.0";
1915
+ currentVersion2 = "0.53.0".trim() ? "0.53.0".trim() : "0.1.0";
1916
1916
  });
1917
1917
 
1918
1918
  // src/commands/track.ts
@@ -2200,7 +2200,7 @@ async function previewArchiveUploadCommand(idOrLogId, options) {
2200
2200
  form.append("preview", Bun.file(options.file), basename(options.file));
2201
2201
  form.append("source", options.source);
2202
2202
  form.append("mime", options.mime);
2203
- return adminApiPostForm(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/preview-archive`, form);
2203
+ return adminApiPostForm(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/preview`, form);
2204
2204
  }
2205
2205
  async function previewArchiveBackfillCommand(options) {
2206
2206
  const result = {
@@ -2220,7 +2220,7 @@ async function previewArchiveBackfillCommand(options) {
2220
2220
  result.skipped.push({ reason: "no_log_id", trackId: track.trackId });
2221
2221
  continue;
2222
2222
  }
2223
- const status = await adminApiGet(`/api/admin/tracks/${encodeURIComponent(track.trackId)}/preview-archive`);
2223
+ const status = await adminApiGet(`/api/admin/tracks/${encodeURIComponent(track.trackId)}/preview`);
2224
2224
  if (status.archived) {
2225
2225
  result.skipped.push({ reason: "already_archived", trackId: track.trackId });
2226
2226
  continue;
@@ -2259,7 +2259,7 @@ async function uploadResolvedPreview(trackId, preview) {
2259
2259
  form.append("preview", new Blob([preview.bytes], { type: preview.mime }), `preview.${extension}`);
2260
2260
  form.append("source", preview.source);
2261
2261
  form.append("mime", preview.mime);
2262
- await adminApiPostForm(`/api/admin/tracks/${encodeURIComponent(trackId)}/preview-archive`, form);
2262
+ await adminApiPostForm(`/api/admin/tracks/${encodeURIComponent(trackId)}/preview`, form);
2263
2263
  }
2264
2264
  async function resolvePreview(track) {
2265
2265
  const stored = await downloadPreview(track.previewUrl, "deezer:stored");
@@ -2540,6 +2540,62 @@ var init_mixtape_youtube2 = __esm(() => {
2540
2540
  init_output();
2541
2541
  });
2542
2542
 
2543
+ // src/commands/newsletter.ts
2544
+ var exports_newsletter = {};
2545
+ __export(exports_newsletter, {
2546
+ newsletterUpdateCommand: () => newsletterUpdateCommand,
2547
+ newsletterSendCommand: () => newsletterSendCommand,
2548
+ newsletterListCommand: () => newsletterListCommand,
2549
+ newsletterDraftCommand: () => newsletterDraftCommand
2550
+ });
2551
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
2552
+ function buildBody2(options, { requireContent }) {
2553
+ const body = {};
2554
+ if (options.contentFile !== undefined) {
2555
+ body.contentJson = readContentFile(options.contentFile);
2556
+ } else if (requireContent) {
2557
+ throw new CliError2("missing_content", "A draft needs the structured content payload via --content-file <edition.json>");
2558
+ }
2559
+ if (options.subject !== undefined) {
2560
+ body.subject = options.subject;
2561
+ }
2562
+ if (options.windowSince !== undefined) {
2563
+ body.windowSince = options.windowSince;
2564
+ }
2565
+ if (options.windowUntil !== undefined) {
2566
+ body.windowUntil = options.windowUntil;
2567
+ }
2568
+ return body;
2569
+ }
2570
+ function readContentFile(filePath) {
2571
+ if (!existsSync2(filePath)) {
2572
+ throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
2573
+ }
2574
+ const text = readFileSync2(filePath, "utf-8");
2575
+ try {
2576
+ return JSON.parse(text);
2577
+ } catch (error) {
2578
+ throw new CliError2("invalid_content_json", `Content JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
2579
+ }
2580
+ }
2581
+ async function newsletterDraftCommand(options) {
2582
+ return adminApiPost("/api/admin/newsletter/editions", buildBody2(options, { requireContent: true }));
2583
+ }
2584
+ async function newsletterUpdateCommand(id, options) {
2585
+ return adminApiPatch(`/api/admin/newsletter/editions/${encodeURIComponent(id)}`, buildBody2(options, { requireContent: false }));
2586
+ }
2587
+ async function newsletterSendCommand(id) {
2588
+ return adminApiPost(`/api/admin/newsletter/editions/${encodeURIComponent(id)}/send`);
2589
+ }
2590
+ async function newsletterListCommand() {
2591
+ const response = await adminApiGet("/api/admin/newsletter/editions");
2592
+ return response.editions;
2593
+ }
2594
+ var init_newsletter = __esm(() => {
2595
+ init_api();
2596
+ init_output();
2597
+ });
2598
+
2543
2599
  // src/commands/add.ts
2544
2600
  async function addCommand2(spotifyUrl, options) {
2545
2601
  const result = await adminApiPost("/api/admin/tracks", {
@@ -3149,7 +3205,7 @@ var COORD_FALLBACK2 = "—";
3149
3205
  var init_format2 = () => {};
3150
3206
 
3151
3207
  // src/cli.ts
3152
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
3208
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
3153
3209
  import path2 from "path";
3154
3210
 
3155
3211
  // ../../node_modules/commander/lib/error.js
@@ -5346,8 +5402,8 @@ function addMetaCommands(program2) {
5346
5402
  });
5347
5403
  }
5348
5404
  function addTrackCommands(program2) {
5349
- const track = configureCommand(program2.command("track", { hidden: true }).description("Public track lookups"));
5350
- track.command("get").description("Look up one finding by id or Log ID").argument("[idOrLogId]").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5405
+ const tracks = configureCommand(program2.command("tracks", { hidden: true }).alias("track").description("Public track lookups"));
5406
+ tracks.command("get").description("Look up one finding by id or Log ID").argument("[idOrLogId]").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5351
5407
  const { trackGetCommand: trackGetCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
5352
5408
  await runTrackGet(idOrLogId, options, trackGetCommand2);
5353
5409
  });
@@ -5380,19 +5436,12 @@ function addAdminCommands(program2) {
5380
5436
  const { queueCommand: queueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
5381
5437
  await runAdminQueue(options, queueCommand2);
5382
5438
  });
5383
- adminTracks.command("context-queue").description("Findings missing their field notes, oldest first (the context cron's worklist)").option("--limit <limit>", "Number of findings to show", "10").option("--json", "Print JSON", false).action(async (options) => {
5384
- const { contextQueueCommand: contextQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
5385
- await runAdminContextQueue(options, contextQueueCommand2);
5386
- });
5387
- adminTracks.command("observe-queue").description("Findings with notes but no observation yet, oldest first (the observe cron's worklist)").option("--limit <limit>", "Number of findings to show", "10").option("--json", "Print JSON", false).action(async (options) => {
5388
- const { observeQueueCommand: observeQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
5389
- await runAdminObserveQueue(options, observeQueueCommand2);
5390
- });
5391
- adminTracks.command("enrich-queue").description("Findings needing (re-)enrichment: pending, failed, or stuck processing").option("--limit <limit>", "Number of findings to show", "10").option("--json", "Print JSON", false).action(async (options) => {
5392
- const { enrichQueueCommand: enrichQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
5393
- await runAdminEnrichQueue(options, enrichQueueCommand2);
5394
- });
5395
- admin.command("enrich-queue", { hidden: true }).description("Enrich queue (alias of `admin tracks enrich-queue`)").option("--limit <limit>", "Number of findings to show", "10").option("--json", "Print JSON", false).action(async (options) => {
5439
+ adminTracks.command("enrich").description("Enrichment worklist (pending, failed, or stuck processing) \u2014 use --queue").option("--queue", "Show the enrichment worklist, oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--json", "Print JSON", false).action(async (options) => {
5440
+ if (!options.queue) {
5441
+ console.error("`tracks enrich` is a worklist view \u2014 enrichment runs on the on-box `fluncle-enrich` cron.\nUse `tracks enrich --queue` to see findings needing (re-)enrichment.");
5442
+ process.exitCode = 1;
5443
+ return;
5444
+ }
5396
5445
  const { enrichQueueCommand: enrichQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
5397
5446
  await runAdminEnrichQueue(options, enrichQueueCommand2);
5398
5447
  });
@@ -5421,15 +5470,25 @@ function addAdminCommands(program2) {
5421
5470
  const { trackSocialShowCommand: trackSocialShowCommand2, trackSocialUpdateCommand: trackSocialUpdateCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
5422
5471
  await runTrackSocial(idOrLogId, options, trackSocialShowCommand2, trackSocialUpdateCommand2);
5423
5472
  });
5424
- adminTrack.command("preview").alias("preview-archive").description("Store one official preview at the operator-only archive path for analysis").argument("[idOrLogId]").option("--file <file>", "Preview audio file to archive").option("--mime <mime>", "MIME type of the preview audio").option("--source <source>", "Provenance label for the archived preview").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5473
+ adminTrack.command("preview").description("Store one official preview at the operator-only archive path for analysis").argument("[idOrLogId]").option("--file <file>", "Preview audio file to archive").option("--mime <mime>", "MIME type of the preview audio").option("--source <source>", "Provenance label for the archived preview").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5425
5474
  const { previewArchiveUploadCommand: previewArchiveUploadCommand2 } = await Promise.resolve().then(() => (init_preview_archive(), exports_preview_archive));
5426
5475
  await runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCommand2);
5427
5476
  });
5428
- adminTrack.command("observe").description("Render Fluncle's spoken field observation for a track (ElevenLabs, Worker-side)").argument("[idOrLogId]").option("--script <text>", "The voice-gated observation script (the spoken text)").option("--script-file <file>", "Read the observation script from a file (e.g. observation.txt)").option("--voice-id <id>", "Override the configured ElevenLabs voice id").option("--model <model>", "TTS model (eleven_multilingual_v2 | eleven_v3)").option("--duration-ms <ms>", "Probed audio duration in ms (the agent runs ffprobe)").option("--duration-target-sec <sec>", "Target observation length in seconds (20\u201345)").option("--context-note <text>", "Pre-fetched factual context (else the Worker firecrawls)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5477
+ adminTrack.command("observe").description("Render Fluncle's spoken field observation for a track (ElevenLabs, Worker-side)").argument("[idOrLogId]").option("--queue", "Show the observe worklist (notes but no observation yet), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--script <text>", "The voice-gated observation script (the spoken text)").option("--script-file <file>", "Read the observation script from a file (e.g. observation.txt)").option("--voice-id <id>", "Override the configured ElevenLabs voice id").option("--model <model>", "TTS model (eleven_multilingual_v2 | eleven_v3)").option("--duration-ms <ms>", "Probed audio duration in ms (the agent runs ffprobe)").option("--duration-target-sec <sec>", "Target observation length in seconds (20\u201345)").option("--context-note <text>", "Pre-fetched factual context (else the Worker firecrawls)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5478
+ if (options.queue) {
5479
+ const { observeQueueCommand: observeQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
5480
+ await runAdminObserveQueue(options, observeQueueCommand2);
5481
+ return;
5482
+ }
5429
5483
  const { trackObserveCommand: trackObserveCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
5430
5484
  await runTrackObserve(idOrLogId, options, trackObserveCommand2);
5431
5485
  });
5432
- adminTrack.command("context").description("Gather the field notes for a finding (facts only; observe speaks from them)").argument("[idOrLogId]").option("--query <text>", "Override the fact-search query (else the Worker builds one)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5486
+ adminTrack.command("context").description("Gather the field notes for a finding (facts only; observe speaks from them)").argument("[idOrLogId]").option("--queue", "Show the context worklist (findings missing field notes), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--query <text>", "Override the fact-search query (else the Worker builds one)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
5487
+ if (options.queue) {
5488
+ const { contextQueueCommand: contextQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
5489
+ await runAdminContextQueue(options, contextQueueCommand2);
5490
+ return;
5491
+ }
5433
5492
  const { trackContextCommand: trackContextCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
5434
5493
  await runTrackContext(idOrLogId, options, trackContextCommand2);
5435
5494
  });
@@ -5473,6 +5532,26 @@ function addAdminCommands(program2) {
5473
5532
  const { publishYoutubeCommand: publishYoutubeCommand3 } = await Promise.resolve().then(() => (init_mixtape_youtube2(), exports_mixtape_youtube2));
5474
5533
  await runMixtapePublishYoutube(idOrLogId, options, publishYoutubeCommand3);
5475
5534
  });
5535
+ const adminNewsletter = configureCommand(admin.command("newsletter").description("Newsletter edition commands"));
5536
+ adminNewsletter.action(() => {
5537
+ adminNewsletter.outputHelp();
5538
+ });
5539
+ adminNewsletter.command("draft").description("Persist a newsletter edition draft (the agent authors it, you send it)").option("--content-file <file>", "Structured edition content payload (JSON)").option("--subject <text>", "Email subject line").option("--window-since <date>", "Discovery-window start (ISO)").option("--window-until <date>", "Discovery-window end (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
5540
+ const { newsletterDraftCommand: newsletterDraftCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
5541
+ await runNewsletterDraft(options, newsletterDraftCommand2);
5542
+ });
5543
+ adminNewsletter.command("update").description("Update a draft edition's payload, subject, or window").argument("[id]").option("--content-file <file>", "Structured edition content payload (JSON)").option("--subject <text>", "Email subject line").option("--window-since <date>", "Discovery-window start (ISO)").option("--window-until <date>", "Discovery-window end (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
5544
+ const { newsletterUpdateCommand: newsletterUpdateCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
5545
+ await runNewsletterUpdate(id, options, newsletterUpdateCommand2);
5546
+ });
5547
+ adminNewsletter.command("send").description("Send an edition \u2014 OPERATOR only (Resend broadcast + mint the number)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
5548
+ const { newsletterSendCommand: newsletterSendCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
5549
+ await runNewsletterSend(id, options, newsletterSendCommand2);
5550
+ });
5551
+ adminNewsletter.command("list").description("List every edition including drafts (the cron's miss-recovery read)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
5552
+ const { newsletterListCommand: newsletterListCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
5553
+ await runNewsletterList(options, newsletterListCommand2);
5554
+ });
5476
5555
  const submissions = configureCommand(admin.command("submissions").description("Review listener submissions"));
5477
5556
  submissions.option("--json", "Print JSON", false).action(async (options) => {
5478
5557
  const { listSubmissionsCommand: listSubmissionsCommand2 } = await Promise.resolve().then(() => (init_submissions(), exports_submissions));
@@ -5552,7 +5631,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
5552
5631
  console.log(` mime: ${result.mime}`);
5553
5632
  }
5554
5633
  async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
5555
- const script = options.scriptFile ? readFileSync2(options.scriptFile, "utf8") : options.script;
5634
+ const script = options.scriptFile ? readFileSync3(options.scriptFile, "utf8") : options.script;
5556
5635
  if (!idOrLogId || !script || !script.trim()) {
5557
5636
  throw new Error("Usage: fluncle admin tracks observe <track_id|log_id> (--script <text> | --script-file <file>) [--voice-id <id>] [--model <model>] [--duration-ms <ms>] [--context-note <text>] [--json]");
5558
5637
  }
@@ -5714,7 +5793,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
5714
5793
  return;
5715
5794
  }
5716
5795
  const candidate = path2.join(dir, name);
5717
- return existsSync2(candidate) ? candidate : undefined;
5796
+ return existsSync3(candidate) ? candidate : undefined;
5718
5797
  };
5719
5798
  const resolveFile = (explicit, name) => {
5720
5799
  if (explicit) {
@@ -5800,7 +5879,7 @@ async function runTrackSocial(idOrLogId, options, trackSocialShowCommand2, track
5800
5879
  }
5801
5880
  async function runTrackGet(idOrLogId, options, trackGetCommand2) {
5802
5881
  if (!idOrLogId) {
5803
- throw new Error("Missing id. Usage: fluncle track get <track_id|log_id> [--json]");
5882
+ throw new Error("Missing id. Usage: fluncle tracks get <track_id|log_id> [--json]");
5804
5883
  }
5805
5884
  const result = await trackGetCommand2(idOrLogId);
5806
5885
  if (options.json) {
@@ -5980,6 +6059,53 @@ function formatCue(ms) {
5980
6059
  }
5981
6060
  return `${minutes}:${seconds.toString().padStart(2, "0")}`;
5982
6061
  }
6062
+ async function runNewsletterDraft(options, newsletterDraftCommand2) {
6063
+ const result = await newsletterDraftCommand2(options);
6064
+ if (options.json) {
6065
+ printJson(result);
6066
+ return;
6067
+ }
6068
+ console.log(`Drafted edition ${result.edition.id}. It stays a draft until the operator sends it.`);
6069
+ }
6070
+ async function runNewsletterUpdate(id, options, newsletterUpdateCommand2) {
6071
+ if (!id) {
6072
+ throw new Error("Missing edition id. Usage: fluncle admin newsletter update <id>");
6073
+ }
6074
+ const result = await newsletterUpdateCommand2(id, options);
6075
+ if (options.json) {
6076
+ printJson(result);
6077
+ return;
6078
+ }
6079
+ console.log(`Saved draft ${result.edition.id}.`);
6080
+ }
6081
+ async function runNewsletterSend(id, options, newsletterSendCommand2) {
6082
+ if (!id) {
6083
+ throw new Error("Missing edition id. Usage: fluncle admin newsletter send <id>");
6084
+ }
6085
+ const result = await newsletterSendCommand2(id);
6086
+ if (options.json) {
6087
+ printJson(result);
6088
+ return;
6089
+ }
6090
+ const number = result.edition.number;
6091
+ console.log(number === undefined ? `Sent edition ${result.edition.id}.` : `Sent edition #${number} \u2014 it's out to the list and in the archive.`);
6092
+ }
6093
+ async function runNewsletterList(options, newsletterListCommand2) {
6094
+ const editions = await newsletterListCommand2();
6095
+ if (options.json) {
6096
+ printJson({ editions, ok: true });
6097
+ return;
6098
+ }
6099
+ if (editions.length === 0) {
6100
+ console.log("No editions yet.");
6101
+ return;
6102
+ }
6103
+ for (const edition of editions) {
6104
+ const label = edition.number === undefined ? "draft" : `#${edition.number}`;
6105
+ const subject = edition.subject ?? "(no subject)";
6106
+ console.log(`${label} ${edition.status} ${edition.id} ${subject}`);
6107
+ }
6108
+ }
5983
6109
  async function runAdd(spotifyUrl, options, addCommand3) {
5984
6110
  if (!spotifyUrl) {
5985
6111
  throw new Error("Missing Spotify track URL");
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.51.0"
34
+ "version": "0.53.0"
35
35
  }