fluncle 0.167.0 → 0.169.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.
- package/bin/fluncle.mjs +190 -9
- 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.
|
|
564
|
+
currentVersion = "0.169.0".trim() ? "0.169.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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
2262
|
+
currentVersion2 = "0.169.0".trim() ? "0.169.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
|
],
|
|
@@ -3119,6 +3186,19 @@ var init_src = __esm(() => {
|
|
|
3119
3186
|
title: "Recording MBIDs",
|
|
3120
3187
|
weights: { status: "hidden" }
|
|
3121
3188
|
},
|
|
3189
|
+
{
|
|
3190
|
+
command: "fluncle admin backfills label-lineage",
|
|
3191
|
+
exposedContent: [
|
|
3192
|
+
"resolve each label's founding date + place + parent imprint from MusicBrainz → the labels row"
|
|
3193
|
+
],
|
|
3194
|
+
kind: "cron",
|
|
3195
|
+
name: "cron.label-lineage",
|
|
3196
|
+
operatorNotes: "every 60m, run by a rave-02 HOST systemd timer (docs/agents/hermes/label-lineage-timer/). The label entity's LINEAGE half (RFC label-lineage-remixer, U1): gives each label its founding facts + its place in the imprint hierarchy from MusicBrainz — `life-span.begin` → `founding_date`, `area.name` → `founded_location`, and the `backward` `label ownership` / `imprint` label-rels → `parent_label_id` (matched to an EXISTING label by MBID; NEVER minted — an unmatched parent is only counted). A dedicated sweep, not a rider on the label-image sweep, because that one is terminal per label and a logo-resolved label would never get its lineage: this carries its OWN `lineage_state` machine so it reaches every label once. METADATA ONLY — it certifies nothing, mints nothing, publishes nothing (agent tier, the `backfill_label_images` precedent). Worker-paced (the box holds no MusicBrainz budget): one bounded batch per tick, 1 req/s, circuit-broken on a throttle, reusing the shared MB client + exact-fold identity search. The `labels` row carries the durable reliability state (lineage_state/lineage_attempted_at/lineage_failures), so a resolved/none label is terminal. Emitted as the `/label/<slug>` Organization's `foundingDate` / `location` / `parentOrganization` / `subOrganization`. Zero LLM tokens. Source: docs/agents/hermes/scripts/label-lineage-sweep.*. See docs/label-entity.md.",
|
|
3197
|
+
probeConfig: { cadenceMs: 60 * MINUTE_MS, cronName: "fluncle-label-lineage", kind: "cron" },
|
|
3198
|
+
statusDescription: "resolves each label's founding and imprint",
|
|
3199
|
+
title: "Label lineage",
|
|
3200
|
+
weights: { status: "hidden" }
|
|
3201
|
+
},
|
|
3122
3202
|
{
|
|
3123
3203
|
command: "fluncle admin backfills cover-masters",
|
|
3124
3204
|
exposedContent: [
|
|
@@ -3419,6 +3499,23 @@ var init_src = __esm(() => {
|
|
|
3419
3499
|
title: "Reach snapshot",
|
|
3420
3500
|
weights: { status: "secondary" }
|
|
3421
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
|
+
},
|
|
3422
3519
|
{
|
|
3423
3520
|
exposedContent: [
|
|
3424
3521
|
"nightly codebase audit — one domain/night on a 7-day rotation; opens a PR the reviewer merges (claude -p, subscription auth)"
|
|
@@ -6250,6 +6347,7 @@ __export(exports_admin_labels, {
|
|
|
6250
6347
|
labelsBioQueueCommand: () => labelsBioQueueCommand,
|
|
6251
6348
|
draftLabelBioCommand: () => draftLabelBioCommand,
|
|
6252
6349
|
describeLabelCommand: () => describeLabelCommand,
|
|
6350
|
+
backfillLabelLineageCommand: () => backfillLabelLineageCommand,
|
|
6253
6351
|
backfillLabelImagesCommand: () => backfillLabelImagesCommand
|
|
6254
6352
|
});
|
|
6255
6353
|
async function describeLabelCommand(slug, options) {
|
|
@@ -6269,6 +6367,13 @@ async function backfillLabelImagesCommand(limit, dryRun, cursor) {
|
|
|
6269
6367
|
}
|
|
6270
6368
|
return adminApiPost(`/api/admin/backfill/label-images?${params.toString()}`);
|
|
6271
6369
|
}
|
|
6370
|
+
async function backfillLabelLineageCommand(limit, dryRun, cursor) {
|
|
6371
|
+
const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
|
|
6372
|
+
if (cursor) {
|
|
6373
|
+
params.set("cursor", cursor);
|
|
6374
|
+
}
|
|
6375
|
+
return adminApiPost(`/api/admin/backfill/label-lineage?${params.toString()}`);
|
|
6376
|
+
}
|
|
6272
6377
|
var init_admin_labels = __esm(() => {
|
|
6273
6378
|
init_api();
|
|
6274
6379
|
init_admin_artists2();
|
|
@@ -8794,9 +8899,13 @@ function addListenCommands(program2) {
|
|
|
8794
8899
|
const { recentCommand: recentCommand3 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
|
|
8795
8900
|
await runRecent(options, recentCommand3);
|
|
8796
8901
|
});
|
|
8797
|
-
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) => {
|
|
8902
|
+
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) => {
|
|
8798
8903
|
const { freshCommand: freshCommand2 } = await Promise.resolve().then(() => (init_fresh(), exports_fresh));
|
|
8799
|
-
await freshCommand2({
|
|
8904
|
+
await freshCommand2({
|
|
8905
|
+
json: options.json,
|
|
8906
|
+
limit: parseListLimit(options.limit),
|
|
8907
|
+
view: parseFreshView(options.view)
|
|
8908
|
+
});
|
|
8800
8909
|
});
|
|
8801
8910
|
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) => {
|
|
8802
8911
|
const { artistsListCommand: artistsListCommand2, artistsGetCommand: artistsGetCommand2 } = await Promise.resolve().then(() => (init_artists(), exports_artists));
|
|
@@ -9510,6 +9619,10 @@ function addAdminCommands(program2) {
|
|
|
9510
9619
|
const { backfillLabelImagesCommand: backfillLabelImagesCommand2 } = await Promise.resolve().then(() => (init_admin_labels(), exports_admin_labels));
|
|
9511
9620
|
await runBackfillLabelImages(options, backfillLabelImagesCommand2);
|
|
9512
9621
|
});
|
|
9622
|
+
backfill.command("label-lineage").description("Resolve label lineage (founding date, place, parent imprint) from MusicBrainz").option("--dry-run", "Report the eligible worklist without any vendor call or write", false).option("--limit <limit>", "Max labels to process", "50").option("--json", "Print JSON", false).action(async (options) => {
|
|
9623
|
+
const { backfillLabelLineageCommand: backfillLabelLineageCommand2 } = await Promise.resolve().then(() => (init_admin_labels(), exports_admin_labels));
|
|
9624
|
+
await runBackfillLabelLineage(options, backfillLabelLineageCommand2);
|
|
9625
|
+
});
|
|
9513
9626
|
backfill.command("recording-mbids").description("Fill MusicBrainz recording MBIDs (crawler PK strip + ISRC resolve) over tracks").option("--dry-run", "Report the eligible worklist without any vendor call or write", false).option("--limit <limit>", "Max ISRC lookups to process", "50").option("--json", "Print JSON", false).action(async (options) => {
|
|
9514
9627
|
const { backfillRecordingMbidsCommand: backfillRecordingMbidsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
9515
9628
|
await runBackfillRecordingMbids(options, backfillRecordingMbidsCommand2);
|
|
@@ -10308,6 +10421,66 @@ async function runBackfillCoverMasters(options, backfillCoverMastersCommand2) {
|
|
|
10308
10421
|
process.exitCode = 1;
|
|
10309
10422
|
}
|
|
10310
10423
|
}
|
|
10424
|
+
async function runBackfillLabelLineage(options, backfillLabelLineageCommand2) {
|
|
10425
|
+
const limit = parseListLimit(options.limit);
|
|
10426
|
+
const resolved = [];
|
|
10427
|
+
const none = [];
|
|
10428
|
+
const failed = [];
|
|
10429
|
+
let cursor;
|
|
10430
|
+
let dryRun = options.dryRun;
|
|
10431
|
+
let throttled = false;
|
|
10432
|
+
let unmatchedParents = 0;
|
|
10433
|
+
while (resolved.length + none.length + failed.length < limit) {
|
|
10434
|
+
const remaining = limit - (resolved.length + none.length + failed.length);
|
|
10435
|
+
const result = await backfillLabelLineageCommand2(remaining, options.dryRun, cursor);
|
|
10436
|
+
dryRun = result.dryRun;
|
|
10437
|
+
resolved.push(...result.resolved);
|
|
10438
|
+
none.push(...result.none);
|
|
10439
|
+
failed.push(...result.failed);
|
|
10440
|
+
unmatchedParents += result.unmatchedParents;
|
|
10441
|
+
if (!options.json) {
|
|
10442
|
+
const verb2 = result.dryRun ? "would walk" : "walked";
|
|
10443
|
+
console.log(` \u2026${verb2} ${result.resolvedCount}; ${result.noneCount} with no MusicBrainz identity; ${result.failedCount} failed; ${result.unmatchedParents} unmatched parent(s)`);
|
|
10444
|
+
}
|
|
10445
|
+
if (result.rateLimited) {
|
|
10446
|
+
throttled = true;
|
|
10447
|
+
break;
|
|
10448
|
+
}
|
|
10449
|
+
if (result.nextCursor === null) {
|
|
10450
|
+
break;
|
|
10451
|
+
}
|
|
10452
|
+
cursor = result.nextCursor;
|
|
10453
|
+
}
|
|
10454
|
+
if (options.json) {
|
|
10455
|
+
printJson({
|
|
10456
|
+
dryRun,
|
|
10457
|
+
failed,
|
|
10458
|
+
failedCount: failed.length,
|
|
10459
|
+
none,
|
|
10460
|
+
noneCount: none.length,
|
|
10461
|
+
ok: true,
|
|
10462
|
+
rateLimited: throttled,
|
|
10463
|
+
resolved,
|
|
10464
|
+
resolvedCount: resolved.length,
|
|
10465
|
+
unmatchedParents
|
|
10466
|
+
});
|
|
10467
|
+
if (failed.length > 0) {
|
|
10468
|
+
process.exitCode = 1;
|
|
10469
|
+
}
|
|
10470
|
+
return;
|
|
10471
|
+
}
|
|
10472
|
+
const verb = dryRun ? "Would walk" : "Walked";
|
|
10473
|
+
console.log(`${verb} ${resolved.length} label lineage(s); ${none.length} with no MusicBrainz identity; ${failed.length} failed; ${unmatchedParents} unmatched parent(s).`);
|
|
10474
|
+
for (const slug of resolved) {
|
|
10475
|
+
console.log(` ${slug}`);
|
|
10476
|
+
}
|
|
10477
|
+
for (const item of failed) {
|
|
10478
|
+
console.log(` ${item.slug}: ${item.error}`);
|
|
10479
|
+
}
|
|
10480
|
+
if (failed.length > 0) {
|
|
10481
|
+
process.exitCode = 1;
|
|
10482
|
+
}
|
|
10483
|
+
}
|
|
10311
10484
|
async function runBackfillLabelImages(options, backfillLabelImagesCommand2) {
|
|
10312
10485
|
const limit = parseListLimit(options.limit);
|
|
10313
10486
|
const resolved = [];
|
|
@@ -11459,6 +11632,13 @@ function parseListLimit(value) {
|
|
|
11459
11632
|
}
|
|
11460
11633
|
return limit;
|
|
11461
11634
|
}
|
|
11635
|
+
function parseFreshView(value) {
|
|
11636
|
+
const view = value ?? "all";
|
|
11637
|
+
if (view !== "all" && view !== "tracks" && view !== "albums") {
|
|
11638
|
+
throw new Error("View must be one of: all, tracks, albums");
|
|
11639
|
+
}
|
|
11640
|
+
return view;
|
|
11641
|
+
}
|
|
11462
11642
|
function resolveHasKey(options) {
|
|
11463
11643
|
if (options.key === false) {
|
|
11464
11644
|
return false;
|
|
@@ -12048,6 +12228,7 @@ var stringOptions = new Set([
|
|
|
12048
12228
|
"--verdict-file",
|
|
12049
12229
|
"--video",
|
|
12050
12230
|
"--video-url",
|
|
12231
|
+
"--view",
|
|
12051
12232
|
"--voice-id",
|
|
12052
12233
|
"--window-since",
|
|
12053
12234
|
"--window-until"
|
package/package.json
CHANGED