fluncle 0.168.0 → 0.170.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 +132 -10
  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.168.0".trim() ? "0.168.0".trim() : "0.1.0";
564
+ currentVersion = "0.170.0".trim() ? "0.170.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -1013,20 +1013,50 @@ function freshRows(tracks) {
1013
1013
  return `${coordinate(track).padEnd(coordWidth)} ${released} ${label}`;
1014
1014
  });
1015
1015
  }
1016
+ function albumRows(albums) {
1017
+ return albums.map((album) => {
1018
+ const released = album.releaseDate.slice(0, 10);
1019
+ return `${released} ${album.artists.join(", ")} — ${album.name}`;
1020
+ });
1021
+ }
1016
1022
  async function freshCommand({
1017
1023
  json,
1018
- limit
1024
+ limit,
1025
+ view
1019
1026
  }) {
1020
1027
  const response = await publicApiGet(`/api/v1/tracks/fresh?limit=${limit}`);
1028
+ const showTracks = view !== "albums";
1029
+ const showAlbums = view !== "tracks";
1021
1030
  if (json) {
1022
- printJson2({ ok: true, ...response });
1031
+ const payload = { ok: true, windowDays: response.windowDays };
1032
+ if (showAlbums) {
1033
+ payload.albums = response.albums;
1034
+ }
1035
+ if (showTracks) {
1036
+ payload.tracks = response.tracks;
1037
+ }
1038
+ printJson2(payload);
1023
1039
  return;
1024
1040
  }
1025
- if (response.tracks.length === 0) {
1041
+ const blocks = [];
1042
+ if (showTracks && response.tracks.length > 0) {
1043
+ const rows = freshRows(response.tracks).join(`
1044
+ `);
1045
+ blocks.push(view === "all" ? `Tracks
1046
+ ${rows}` : rows);
1047
+ }
1048
+ if (showAlbums && response.albums.length > 0) {
1049
+ const rows = albumRows(response.albums).join(`
1050
+ `);
1051
+ blocks.push(view === "all" ? `Albums & EPs
1052
+ ${rows}` : rows);
1053
+ }
1054
+ if (blocks.length === 0) {
1026
1055
  console.log(`Nothing new out in the last ${response.windowDays} days.`);
1027
1056
  return;
1028
1057
  }
1029
- console.log(freshRows(response.tracks).join(`
1058
+ console.log(blocks.join(`
1059
+
1030
1060
  `));
1031
1061
  }
1032
1062
  var COORD_FALLBACK = "—";
@@ -2229,7 +2259,7 @@ function parseVersion2(version) {
2229
2259
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2230
2260
  var init_version2 = __esm(() => {
2231
2261
  init_output();
2232
- currentVersion2 = "0.168.0".trim() ? "0.168.0".trim() : "0.1.0";
2262
+ currentVersion2 = "0.170.0".trim() ? "0.170.0".trim() : "0.1.0";
2233
2263
  });
2234
2264
 
2235
2265
  // ../../packages/registry/src/index.ts
@@ -2439,6 +2469,19 @@ var init_src = __esm(() => {
2439
2469
  url: `${SITE}/fresh`,
2440
2470
  weights: { web: "secondary" }
2441
2471
  },
2472
+ {
2473
+ discoveryUrl: `${SITE}/llms.txt`,
2474
+ exposedContent: [
2475
+ "/tracks — the whole list: every track Fluncle holds (certified findings + the wider catalogue), newest release first, filterable by release year, tempo, key, label, and galaxy"
2476
+ ],
2477
+ kind: "web_route",
2478
+ name: "web.tracks",
2479
+ operatorNotes: "The top-level track index — the whole archive as one browse list, findings in full voice and the catalogue rows in the unlit register (DESIGN.md). Ordered by tracks.release_date (what came out), never findings.added_at (the Found Rule), keyset-paginated over the tracks_release_date_idx btree so it stays a bounded reverse scan as the catalogue grows (lib/server/tracks-hub.ts). The filter params MIRROR the search vocabulary verbatim (yearMin/yearMax, bpmMin/bpmMax, key, label; galaxy is the one extension). The bare HUB is always indexable + in the sitemap; ANY filter param present flips it to noindex, and the canonical is always the bare /tracks. The INDEX is always-200, so it is HTTP-probeable.",
2480
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2481
+ route: "/tracks",
2482
+ url: `${SITE}/tracks`,
2483
+ weights: { web: "secondary" }
2484
+ },
2442
2485
  {
2443
2486
  exposedContent: [
2444
2487
  "/galaxies — the browse-by-feel lens: the archive grouped into operator-named sonic galaxies (k-means over the MuQ audio embedding space)",
@@ -2731,6 +2774,30 @@ var init_src = __esm(() => {
2731
2774
  url: `${SITE}/fresh.json`,
2732
2775
  weights: { web: "secondary" }
2733
2776
  },
2777
+ {
2778
+ apiFormat: "application/rss+xml",
2779
+ exposedContent: [
2780
+ "one artist's newest releases over a 30-day window, as RSS (release-dated, that artist only)"
2781
+ ],
2782
+ kind: "feed",
2783
+ name: "feed.fresh.artist.rss",
2784
+ operatorNotes: "Slug-parameterized (/artist/:slug/fresh.xml), so there is no fixed URL to health-probe — no probeConfig, like web.artist. An unknown slug 404s; a known artist with nothing in the window serves a valid empty feed. Source: apps/web/src/routes/artist.$slug.fresh[.]xml.ts + src/lib/server/fresh-entity.ts.",
2785
+ route: "/artist/:slug/fresh.xml",
2786
+ url: `${SITE}/artist/:slug/fresh.xml`,
2787
+ weights: { web: "tertiary" }
2788
+ },
2789
+ {
2790
+ apiFormat: "application/rss+xml",
2791
+ exposedContent: [
2792
+ "one label's newest releases over a 30-day window, as RSS (release-dated, that label only)"
2793
+ ],
2794
+ kind: "feed",
2795
+ name: "feed.fresh.label.rss",
2796
+ operatorNotes: "Slug-parameterized (/label/:slug/fresh.xml), so there is no fixed URL to health-probe — no probeConfig, like web.artist. An unknown slug 404s; a known label with nothing in the window serves a valid empty feed. Source: apps/web/src/routes/label.$slug.fresh[.]xml.ts + src/lib/server/fresh-entity.ts.",
2797
+ route: "/label/:slug/fresh.xml",
2798
+ url: `${SITE}/label/:slug/fresh.xml`,
2799
+ weights: { web: "tertiary" }
2800
+ },
2734
2801
  {
2735
2802
  apiFormat: "application/rss+xml",
2736
2803
  exposedContent: ["the mixtapes as a podcast feed (episode audio on found.fluncle.com)"],
@@ -2856,7 +2923,7 @@ var init_src = __esm(() => {
2856
2923
  apiFormat: "application/json",
2857
2924
  discoveryUrl: `${SITE}/.well-known/mcp/server-card.json`,
2858
2925
  exposedContent: [
2859
- "the archive as MCP tools (Streamable HTTP, no auth): list_tracks, list_fresh, get_track, get_random_track, get_status, search_tracks, submit_track, subscribe_newsletter",
2926
+ "the archive as MCP tools (Streamable HTTP, no auth): list_tracks, list_fresh, get_track, get_random_track, get_status, search_archive, get_artist, get_label, build_set, get_similar_artists, list_album_catalogue, list_artist_catalogue, list_label_catalogue, search_tracks, submit_track, subscribe_newsletter",
2860
2927
  "the archive as MCP resources: each finding/mixtape at fluncle://finding/<logId> or fluncle://mixtape/<logId> (its public record)",
2861
2928
  "Fluncle-voiced MCP prompts: recommend_finding, walk_recent_night, decode_coordinate"
2862
2929
  ],
@@ -3432,6 +3499,23 @@ var init_src = __esm(() => {
3432
3499
  title: "Reach snapshot",
3433
3500
  weights: { status: "secondary" }
3434
3501
  },
3502
+ {
3503
+ exposedContent: [
3504
+ "daily catalogue-funnel snapshot — one row per UTC day of stage totals + queue depths + frontier counts behind /admin/funnel (--no-agent)"
3505
+ ],
3506
+ kind: "cron",
3507
+ name: "cron.funnel-snapshot",
3508
+ operatorNotes: "23:45 UTC daily (end of the UTC day the snapshot is keyed on). A bare trigger (the reach/anchor shape): fires the AGENT-tier record_catalogue_snapshot op once — the Worker computes every stage total + queue depth + frontier count through the SAME predicates the sweeps run (lib/server/funnel.ts) and UPSERTS one idempotent row per UTC day (a same-day re-run overwrites, never doubles a bar). Zero LLM tokens; the box's agent token drives it and calls the oRPC HTTP endpoint directly (no new CLI command the pinned box CLI would lack), so no new secret. Source: docs/agents/hermes/scripts/funnel-snapshot-sweep.*. See docs/rfcs/catalogue-funnel-rfc.md.",
3509
+ probeConfig: {
3510
+ cadenceMs: 24 * 60 * MINUTE_MS,
3511
+ cronName: "fluncle-funnel-snapshot",
3512
+ kind: "cron",
3513
+ schedule: { time: "23:45", tz: "UTC" }
3514
+ },
3515
+ statusDescription: "records the catalogue pipeline's daily numbers",
3516
+ title: "Funnel snapshot",
3517
+ weights: { status: "hidden" }
3518
+ },
3435
3519
  {
3436
3520
  exposedContent: [
3437
3521
  "nightly codebase audit — one domain/night on a 7-day rotation; opens a PR the reviewer merges (claude -p, subscription auth)"
@@ -6260,12 +6344,17 @@ var init_admin_artists2 = __esm(() => {
6260
6344
  // src/commands/admin-labels.ts
6261
6345
  var exports_admin_labels = {};
6262
6346
  __export(exports_admin_labels, {
6347
+ mergeLabelCommand: () => mergeLabelCommand,
6263
6348
  labelsBioQueueCommand: () => labelsBioQueueCommand,
6264
6349
  draftLabelBioCommand: () => draftLabelBioCommand,
6265
6350
  describeLabelCommand: () => describeLabelCommand,
6266
6351
  backfillLabelLineageCommand: () => backfillLabelLineageCommand,
6267
6352
  backfillLabelImagesCommand: () => backfillLabelImagesCommand
6268
6353
  });
6354
+ async function mergeLabelCommand(losingSlug, canonicalSlug) {
6355
+ const response = await adminApiPost(`/api/admin/labels/${encodeURIComponent(losingSlug)}/merge`, { canonicalSlug });
6356
+ return response.result;
6357
+ }
6269
6358
  async function describeLabelCommand(slug, options) {
6270
6359
  return adminApiPost(`/api/admin/labels/${encodeURIComponent(slug)}/bio`, buildBioBody2(options));
6271
6360
  }
@@ -8815,9 +8904,13 @@ function addListenCommands(program2) {
8815
8904
  const { recentCommand: recentCommand3 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
8816
8905
  await runRecent(options, recentCommand3);
8817
8906
  });
8818
- program2.command("fresh").description("What just came out, newest release first").option("--limit <limit>", "Number of releases to fetch").option("--json", "Print JSON", false).action(async (options) => {
8907
+ program2.command("fresh").description("What just came out, newest release first").option("--limit <limit>", "Number of releases to fetch").option("--view <view>", "Which cut to show: all, tracks, or albums", "all").option("--json", "Print JSON", false).action(async (options) => {
8819
8908
  const { freshCommand: freshCommand2 } = await Promise.resolve().then(() => (init_fresh(), exports_fresh));
8820
- await freshCommand2({ json: options.json, limit: parseListLimit(options.limit) });
8909
+ await freshCommand2({
8910
+ json: options.json,
8911
+ limit: parseListLimit(options.limit),
8912
+ view: parseFreshView(options.view)
8913
+ });
8821
8914
  });
8822
8915
  program2.command("artists").description("Browse artists in Fluncle's archive").argument("[slug]", "Artist slug (omit for the full list)").option("--json", "Print JSON", false).action(async (slug, options) => {
8823
8916
  const { artistsListCommand: artistsListCommand2, artistsGetCommand: artistsGetCommand2 } = await Promise.resolve().then(() => (init_artists(), exports_artists));
@@ -9565,7 +9658,7 @@ function addAdminCommands(program2) {
9565
9658
  const { draftArtistBioCommand: draftArtistBioCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
9566
9659
  await runEntityBioDraft("artist", slug, options, draftArtistBioCommand2);
9567
9660
  });
9568
- const labels = configureCommand(admin.command("labels").description("Label entity commands (the voiced bio)"));
9661
+ const labels = configureCommand(admin.command("labels").description("Label entity commands (voiced bio + slug-split merge)"));
9569
9662
  labels.action(() => {
9570
9663
  labels.outputHelp();
9571
9664
  });
@@ -9582,6 +9675,27 @@ function addAdminCommands(program2) {
9582
9675
  const { draftLabelBioCommand: draftLabelBioCommand2 } = await Promise.resolve().then(() => (init_admin_labels(), exports_admin_labels));
9583
9676
  await runEntityBioDraft("label", slug, options, draftLabelBioCommand2);
9584
9677
  });
9678
+ labels.command("merge").description("Merge a slug-split label into its canonical row (operator; re-points + redirects)").argument("<losingSlug>", "The duplicate label to fold away").argument("<canonicalSlug>", "The label to keep \u2014 everything re-points onto it").option("--json", "Print JSON", false).action(async (losingSlug, canonicalSlug, options) => {
9679
+ const { mergeLabelCommand: mergeLabelCommand2 } = await Promise.resolve().then(() => (init_admin_labels(), exports_admin_labels));
9680
+ const result = await mergeLabelCommand2(losingSlug, canonicalSlug);
9681
+ if (options.json) {
9682
+ printJson({ ok: true, result });
9683
+ return;
9684
+ }
9685
+ const {
9686
+ aliasWritten,
9687
+ canonicalSlug: canon,
9688
+ losingSlug: loser,
9689
+ reconciled,
9690
+ repointed
9691
+ } = result;
9692
+ console.log(`Merged ${loser} \u2192 ${canon}.`);
9693
+ console.log(` Re-pointed: ${repointed.tracks} track(s), ${repointed.childLabels} sublabel(s), ${repointed.aliases} alias(es).`);
9694
+ console.log(reconciled.length > 0 ? ` Filled onto ${canon} (was empty): ${reconciled.join(", ")}.` : ` Nothing to fill \u2014 ${canon} already carried every fact.`);
9695
+ console.log(` Seed state resolved to: ${result.seedState}.`);
9696
+ console.log(` Alias written: "${aliasWritten.alias}" (${aliasWritten.aliasSlug}) \u2192 confirmed, so it can never re-mint.`);
9697
+ console.log(` /label/${loser} now 301s to /label/${canon}.`);
9698
+ });
9585
9699
  const albums = configureCommand(admin.command("albums").description("Album entity commands (the voiced bio)"));
9586
9700
  albums.action(() => {
9587
9701
  albums.outputHelp();
@@ -11544,6 +11658,13 @@ function parseListLimit(value) {
11544
11658
  }
11545
11659
  return limit;
11546
11660
  }
11661
+ function parseFreshView(value) {
11662
+ const view = value ?? "all";
11663
+ if (view !== "all" && view !== "tracks" && view !== "albums") {
11664
+ throw new Error("View must be one of: all, tracks, albums");
11665
+ }
11666
+ return view;
11667
+ }
11547
11668
  function resolveHasKey(options) {
11548
11669
  if (options.key === false) {
11549
11670
  return false;
@@ -12133,6 +12254,7 @@ var stringOptions = new Set([
12133
12254
  "--verdict-file",
12134
12255
  "--video",
12135
12256
  "--video-url",
12257
+ "--view",
12136
12258
  "--voice-id",
12137
12259
  "--window-since",
12138
12260
  "--window-until"
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.168.0"
34
+ "version": "0.170.0"
35
35
  }