fluncle 0.127.0 → 0.129.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 +178 -2
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -561,7 +561,7 @@ function parseVersion(version) {
561
561
  var currentVersion;
562
562
  var init_version = __esm(() => {
563
563
  init_output();
564
- currentVersion = "0.127.0".trim() ? "0.127.0".trim() : "0.1.0";
564
+ currentVersion = "0.129.0".trim() ? "0.129.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -2189,7 +2189,7 @@ function parseVersion2(version) {
2189
2189
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2190
2190
  var init_version2 = __esm(() => {
2191
2191
  init_output();
2192
- currentVersion2 = "0.127.0".trim() ? "0.127.0".trim() : "0.1.0";
2192
+ currentVersion2 = "0.129.0".trim() ? "0.129.0".trim() : "0.1.0";
2193
2193
  });
2194
2194
 
2195
2195
  // ../../packages/registry/src/index.ts
@@ -2346,6 +2346,34 @@ var init_src = __esm(() => {
2346
2346
  url: `${SITE}/artist`,
2347
2347
  weights: { ssh: "secondary", web: "secondary" }
2348
2348
  },
2349
+ {
2350
+ discoveryUrl: `${SITE}/llms.txt`,
2351
+ exposedContent: [
2352
+ "/labels — every record label Fluncle has found a banger on",
2353
+ "/label/:slug — one label: its findings, the artists on it, and the rest of its catalogue"
2354
+ ],
2355
+ kind: "web_route",
2356
+ name: "web.labels",
2357
+ operatorNotes: "The label half of the graph (log ↔ artist ↔ label ↔ album). The INDEX is probeable (always 200); the slug page is not, so the probe targets /labels. A label below the renderable-track floor renders noindex + stays out of the sitemap (the ARTIST_INDEX_MIN_FINDINGS precedent). The page is BLIND to the label's crawl seed_state — that is crawl scope, never storage (docs/label-entity.md); the operator's ruling station is /admin/labels.",
2358
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2359
+ route: "/labels",
2360
+ url: `${SITE}/labels`,
2361
+ weights: { web: "secondary" }
2362
+ },
2363
+ {
2364
+ discoveryUrl: `${SITE}/llms.txt`,
2365
+ exposedContent: [
2366
+ "/albums — every record Fluncle has found a banger on",
2367
+ "/album/:slug — one record: its findings, its artists, its label, and the rest of its tracklist"
2368
+ ],
2369
+ kind: "web_route",
2370
+ name: "web.albums",
2371
+ operatorNotes: "The album half of the graph, and the node that closes it: the album page carries the album → label edge (a link, plus `albumRelease.recordLabel` in its MusicAlbum JSON-LD). The INDEX is probeable (always 200); the slug page is not. Bounded by the archive, not the catalogue — an album earns a page by carrying a finding (docs/album-entity.md).",
2372
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2373
+ route: "/albums",
2374
+ url: `${SITE}/albums`,
2375
+ weights: { web: "secondary" }
2376
+ },
2349
2377
  {
2350
2378
  exposedContent: [
2351
2379
  "/galaxies — the browse-by-feel lens: the archive grouped into operator-named sonic galaxies (k-means over the MuQ audio embedding space)",
@@ -2982,6 +3010,17 @@ var init_src = __esm(() => {
2982
3010
  probeConfig: { cadenceMs: 20 * MINUTE_MS, cronName: "fluncle-clip-drip", kind: "cron" },
2983
3011
  weights: { status: "hidden" }
2984
3012
  },
3013
+ {
3014
+ command: "fluncle admin publish pause",
3015
+ exposedContent: [
3016
+ "advance one freshly-rendered finding into the publish push — YouTube Short + TikTok inbox draft (--no-agent, Worker HTTP)"
3017
+ ],
3018
+ kind: "cron",
3019
+ name: "cron.publish-advance",
3020
+ operatorNotes: "every 30m. Pure HTTP trigger, zero LLM tokens. The last autonomy gap: render finishes → this pushes, with no operator beat between. Admin tier (needs the Worker's Postiz key, which the box never sees — the box only triggers; the `drip_clips` / `capture_post_urls` precedent). SHIPS DARK: the Worker's kill switch is DEFAULT-DENY (only an explicit `false` in the `publish_advance_paused` setting runs it), so the timer ticks and posts nothing until `fluncle admin publish resume`. The Worker reads the switch FIRST, then advances at most ONE ready finding — both masters finalized, 15m settled, the whole bundle served on R2, the (track, platform) row CLAIMED atomically before any Postiz call, a rolling-24h cap of 6 pushes. A failed push is left `failed` for the operator and never auto-retried. Source: docs/agents/hermes/scripts/publish-advance-sweep.sh. Probed on /status as cron.publish-advance.",
3021
+ probeConfig: { cadenceMs: 30 * MINUTE_MS, cronName: "fluncle-publish-advance", kind: "cron" },
3022
+ weights: { status: "hidden" }
3023
+ },
2985
3024
  {
2986
3025
  command: "fluncle admin tracks queue",
2987
3026
  exposedContent: [
@@ -4097,6 +4136,23 @@ var init_mixtape_youtube2 = __esm(() => {
4097
4136
  init_output();
4098
4137
  });
4099
4138
 
4139
+ // src/commands/publish.ts
4140
+ var exports_publish = {};
4141
+ __export(exports_publish, {
4142
+ publishAdvancePauseCommand: () => publishAdvancePauseCommand,
4143
+ publishAdvanceCommand: () => publishAdvanceCommand
4144
+ });
4145
+ async function publishAdvanceCommand() {
4146
+ return adminApiPost("/api/admin/social/publish/advance", {});
4147
+ }
4148
+ async function publishAdvancePauseCommand(paused) {
4149
+ const response = await adminApiPut("/api/admin/social/publish/advance/state", { paused });
4150
+ return response.paused;
4151
+ }
4152
+ var init_publish = __esm(() => {
4153
+ init_api();
4154
+ });
4155
+
4100
4156
  // ../../packages/contracts/src/util/multipart.ts
4101
4157
  function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
4102
4158
  if (!Number.isInteger(contentLength) || contentLength <= 0) {
@@ -5295,6 +5351,29 @@ var init_auth_lastfm = __esm(() => {
5295
5351
  init_api();
5296
5352
  });
5297
5353
 
5354
+ // src/commands/admin-catalogue.ts
5355
+ var exports_admin_catalogue = {};
5356
+ __export(exports_admin_catalogue, {
5357
+ catalogueRankCommand: () => catalogueRankCommand,
5358
+ catalogueListCommand: () => catalogueListCommand
5359
+ });
5360
+ async function catalogueRankCommand(options) {
5361
+ const limit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
5362
+ const response = await adminApiPost("/api/admin/catalogue/rank", limit ? { limit } : {});
5363
+ return response.summary;
5364
+ }
5365
+ async function catalogueListCommand(options) {
5366
+ const params = new URLSearchParams;
5367
+ params.set("lens", options.lens === "capture" ? "capture" : "ear");
5368
+ if (options.limit) {
5369
+ params.set("limit", options.limit);
5370
+ }
5371
+ return adminApiGet(`/api/admin/catalogue?${params.toString()}`);
5372
+ }
5373
+ var init_admin_catalogue = __esm(() => {
5374
+ init_api();
5375
+ });
5376
+
5298
5377
  // src/commands/admin-artists.ts
5299
5378
  var exports_admin_artists = {};
5300
5379
  __export(exports_admin_artists, {
@@ -8042,6 +8121,22 @@ function addAdminCommands(program2) {
8042
8121
  const { mixtapeResyncCommand: mixtapeResyncCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
8043
8122
  await runMixtapeResync(idOrLogId, options, mixtapeResyncCommand2);
8044
8123
  });
8124
+ const adminPublish = configureCommand(admin.command("publish").description("Render \u2192 publish auto-advance commands"));
8125
+ adminPublish.action(() => {
8126
+ adminPublish.outputHelp();
8127
+ });
8128
+ adminPublish.command("advance").description("Run one tick of the render \u2192 publish auto-advance (kill-switch aware)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
8129
+ const { publishAdvanceCommand: publishAdvanceCommand2 } = await Promise.resolve().then(() => (init_publish(), exports_publish));
8130
+ await runPublishAdvance(options, publishAdvanceCommand2);
8131
+ });
8132
+ adminPublish.command("pause").description("Pause the render \u2192 publish auto-advance, the kill switch (operator)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
8133
+ const { publishAdvancePauseCommand: publishAdvancePauseCommand2 } = await Promise.resolve().then(() => (init_publish(), exports_publish));
8134
+ await runPublishAdvancePause(true, options, publishAdvancePauseCommand2);
8135
+ });
8136
+ adminPublish.command("resume").description("Resume the render \u2192 publish auto-advance, clear the kill switch (operator)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
8137
+ const { publishAdvancePauseCommand: publishAdvancePauseCommand2 } = await Promise.resolve().then(() => (init_publish(), exports_publish));
8138
+ await runPublishAdvancePause(false, options, publishAdvancePauseCommand2);
8139
+ });
8045
8140
  const adminClips = configureCommand(admin.command("clips").description("Mixtape clip (Fluncle Studio) commands"));
8046
8141
  adminClips.action(() => {
8047
8142
  adminClips.outputHelp();
@@ -8212,6 +8307,18 @@ function addAdminCommands(program2) {
8212
8307
  const { galaxyMapWriteCommand: galaxyMapWriteCommand2 } = await Promise.resolve().then(() => (init_galaxies(), exports_galaxies));
8213
8308
  await runGalaxyMapWrite(options, galaxyMapWriteCommand2);
8214
8309
  });
8310
+ const catalogue = configureCommand(admin.command("catalogue").description("The catalogue (uncertified tracks) + its ranking"));
8311
+ catalogue.action(() => {
8312
+ catalogue.outputHelp();
8313
+ });
8314
+ catalogue.command("rank").description("Run one tick of the ranking sweep (nearest finding + capture priority)").option("--limit <limit>", "Candidates per tick (default 250)").option("--json", "Print JSON", false).action(async (options) => {
8315
+ const { catalogueRankCommand: catalogueRankCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
8316
+ await runCatalogueRank(options, catalogueRankCommand2);
8317
+ });
8318
+ catalogue.command("list").description("The ranked catalogue: closest to a finding (ear), or next to capture").option("--lens <lens>", "ear (default) or capture", "ear").option("--limit <limit>", "Rows (default 50, max 200)").option("--json", "Print JSON", false).action(async (options) => {
8319
+ const { catalogueListCommand: catalogueListCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
8320
+ await runCatalogueList(options, catalogueListCommand2);
8321
+ });
8215
8322
  const backfill = configureCommand(admin.command("backfills").description("Backfill operator-only archives"));
8216
8323
  backfill.action(() => {
8217
8324
  backfill.outputHelp();
@@ -9081,6 +9188,39 @@ async function runClipsDripPause(paused, options, clipDripPauseCommand2) {
9081
9188
  }
9082
9189
  console.log(result ? "The drip-feed is paused." : "The drip-feed is running.");
9083
9190
  }
9191
+ async function runPublishAdvance(options, publishAdvanceCommand2) {
9192
+ const result = await publishAdvanceCommand2();
9193
+ if (options.json) {
9194
+ printJson(result);
9195
+ return;
9196
+ }
9197
+ if (result.paused) {
9198
+ console.log("Auto-advance is paused \u2014 nothing pushed.");
9199
+ return;
9200
+ }
9201
+ if (result.pushed.length === 0 && result.held.length === 0 && result.failed.length === 0) {
9202
+ console.log("Nothing ready to advance.");
9203
+ return;
9204
+ }
9205
+ for (const push of result.pushed) {
9206
+ console.log(`Pushed ${push.logId} to ${push.platform} (${push.status}).`);
9207
+ }
9208
+ for (const held of result.held) {
9209
+ const detail = held.missing?.length ? ` (${held.missing.join(", ")})` : "";
9210
+ console.log(`Held ${held.platform} for ${held.trackId}: ${held.reason}${detail}.`);
9211
+ }
9212
+ for (const failure of result.failed) {
9213
+ console.log(`Failed ${failure.platform} for ${failure.trackId} \u2014 left for the operator.`);
9214
+ }
9215
+ }
9216
+ async function runPublishAdvancePause(paused, options, publishAdvancePauseCommand2) {
9217
+ const result = await publishAdvancePauseCommand2(paused);
9218
+ if (options.json) {
9219
+ printJson({ ok: true, paused: result });
9220
+ return;
9221
+ }
9222
+ console.log(result ? "The publish auto-advance is paused." : "The publish auto-advance is running.");
9223
+ }
9084
9224
  async function runMixtapeGet(idOrLogId, options, mixtapeGetCommand2) {
9085
9225
  if (!idOrLogId) {
9086
9226
  throw new Error("Missing mixtape id or log id. Usage: fluncle admin mixtapes get <id|logId>");
@@ -9314,6 +9454,41 @@ async function runGalaxies(slug, options, commands) {
9314
9454
  console.log(`${galaxy.name.padEnd(40)} ${String(galaxy.memberCount).padStart(3)} finding${galaxy.memberCount === 1 ? "" : "s"}`);
9315
9455
  }
9316
9456
  }
9457
+ async function runCatalogueRank(options, catalogueRankCommand2) {
9458
+ const summary = await catalogueRankCommand2({ limit: options.limit });
9459
+ if (options.json) {
9460
+ printJson({ ok: true, summary });
9461
+ return;
9462
+ }
9463
+ console.log(`Ranked ${summary.scored} against ${summary.embeddedFindings} embedded findings; prioritized ${summary.prioritized} for capture. ${summary.remaining} still stale.`);
9464
+ }
9465
+ async function runCatalogueList(options, catalogueListCommand2) {
9466
+ const { summary, tracks } = await catalogueListCommand2({
9467
+ lens: options.lens,
9468
+ limit: options.limit
9469
+ });
9470
+ if (options.json) {
9471
+ printJson({ ok: true, summary, tracks });
9472
+ return;
9473
+ }
9474
+ if (tracks.length === 0) {
9475
+ console.log("Nothing out there yet.");
9476
+ return;
9477
+ }
9478
+ for (const track of tracks) {
9479
+ const identity = `${track.artists.join(", ")} \u2014 ${track.title}`;
9480
+ if (options.lens === "capture") {
9481
+ const reason = track.captureReason;
9482
+ const why2 = reason?.kind === "artist" ? `${reason.name} is already in the archive` : reason?.kind === "label" ? `${reason.name} already carries a finding` : reason?.kind === "seed-label" ? `${reason.name} is a label the crawler digs from` : reason?.kind === "skipped-label" ? `${reason.name} is not your lane \u2014 ranked last, kept anyway` : "nothing ties it to the archive yet";
9483
+ console.log(`${String(track.capturePriority ?? 0)} ${identity.padEnd(52)} ${why2}`);
9484
+ continue;
9485
+ }
9486
+ const match = track.nearestFinding;
9487
+ const score = track.nearestFindingScore?.toFixed(2) ?? "\u2014";
9488
+ const why = match ? `closest to ${match.logId ? `${match.logId} ` : ""}${match.artists.join(", ")} \u2014 ${match.title}` : "nothing to compare it to yet";
9489
+ console.log(`${score} ${identity.padEnd(52)} ${why}`);
9490
+ }
9491
+ }
9317
9492
  async function runGalaxyMapRead(options, galaxyMapReadCommand2) {
9318
9493
  const galaxies = await galaxyMapReadCommand2();
9319
9494
  if (options.json) {
@@ -9878,6 +10053,7 @@ var stringOptions = new Set([
9878
10053
  "--key-confidence",
9879
10054
  "--key-source",
9880
10055
  "--kind",
10056
+ "--lens",
9881
10057
  "--limit",
9882
10058
  "--metrics",
9883
10059
  "--mime",
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.127.0"
34
+ "version": "0.129.0"
35
35
  }