fluncle 0.185.0 → 0.187.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 +260 -169
  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.185.0".trim() ? "0.185.0".trim() : "0.1.0";
564
+ currentVersion = "0.187.0".trim() ? "0.187.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -965,7 +965,7 @@ async function fetchRecentPage(cursor, limit = 10) {
965
965
  if (cursor) {
966
966
  params.set("cursor", cursor);
967
967
  }
968
- const response = await publicApiGet(`/api/tracks?${params.toString()}`);
968
+ const response = await publicApiGet(`/api/v1/findings?${params.toString()}`);
969
969
  return {
970
970
  nextCursor: response.nextCursor,
971
971
  totalCount: response.totalCount,
@@ -980,7 +980,7 @@ async function recentCommand(limit) {
980
980
  if (cursor) {
981
981
  params.set("cursor", cursor);
982
982
  }
983
- const response = await publicApiGet(`/api/tracks?${params.toString()}`);
983
+ const response = await publicApiGet(`/api/v1/findings?${params.toString()}`);
984
984
  for (const apiTrack of response.tracks) {
985
985
  results.push(mapTrack(apiTrack));
986
986
  if (results.length >= limit) {
@@ -1287,7 +1287,7 @@ async function galaxyGetCommand(slug) {
1287
1287
  return { findings: response.findings, galaxy: response.galaxy };
1288
1288
  }
1289
1289
  async function galaxyMapReadCommand() {
1290
- const response = await adminApiGet("/api/admin/galaxies");
1290
+ const response = await adminApiGet("/api/v1/admin/galaxies");
1291
1291
  return response.galaxies;
1292
1292
  }
1293
1293
  async function galaxyEmbeddingsCommand(options) {
@@ -1299,10 +1299,10 @@ async function galaxyEmbeddingsCommand(options) {
1299
1299
  params.set("limit", options.limit);
1300
1300
  }
1301
1301
  const query = params.toString();
1302
- return adminApiGet(`/api/admin/tracks/embeddings${query ? `?${query}` : ""}`);
1302
+ return adminApiGet(`/api/v1/admin/tracks/embeddings${query ? `?${query}` : ""}`);
1303
1303
  }
1304
1304
  async function galaxyMapWriteCommand(clusters) {
1305
- const response = await adminApiPut("/api/admin/galaxies/map", {
1305
+ const response = await adminApiPut("/api/v1/admin/galaxies/map", {
1306
1306
  clusters
1307
1307
  });
1308
1308
  return response.galaxies;
@@ -1313,7 +1313,7 @@ var init_galaxies = __esm(() => {
1313
1313
 
1314
1314
  // src/commands/mixtape-api.ts
1315
1315
  async function mixtapeListCommand() {
1316
- const response = await adminApiGet("/api/admin/mixtapes");
1316
+ const response = await adminApiGet("/api/v1/admin/mixtapes");
1317
1317
  return response.mixtapes;
1318
1318
  }
1319
1319
  async function mixtapeGetCommand(idOrLogId) {
@@ -1380,20 +1380,20 @@ async function distributeYoutube(mixtapeId, videoPath, onProgress) {
1380
1380
  offset = 0;
1381
1381
  }
1382
1382
  onProgress?.("Recording the YouTube link");
1383
- await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/finalize`, { videoId });
1383
+ await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/finalize`, { videoId });
1384
1384
  return { url: `https://youtu.be/${videoId}`, videoId };
1385
1385
  }
1386
1386
  async function publishYoutubeCommand(idOrLogId) {
1387
1387
  const mixtapeId = await resolveMixtapeId(idOrLogId);
1388
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
1388
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
1389
1389
  return { url: response.url };
1390
1390
  }
1391
1391
  async function resyncYoutube(mixtapeId) {
1392
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/resync`);
1392
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/resync`);
1393
1393
  return { url: response.url, videoId: response.videoId };
1394
1394
  }
1395
1395
  async function authYoutubeCommand() {
1396
- const response = await adminApiGet("/api/admin/youtube/auth/start");
1396
+ const response = await adminApiGet("/api/v1/admin/youtube/auth/start");
1397
1397
  console.log(`Open this YouTube authorization URL:
1398
1398
 
1399
1399
  ${response.authUrl}
@@ -1401,11 +1401,11 @@ ${response.authUrl}
1401
1401
  After approving access, Google returns to the Fluncle admin callback and stores the refresh token server-side.`);
1402
1402
  }
1403
1403
  async function initiate(mixtapeId, contentLength, contentType) {
1404
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/initiate`, { contentLength, contentType });
1404
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/initiate`, { contentLength, contentType });
1405
1405
  return { accessToken: response.accessToken, sessionUri: response.sessionUri };
1406
1406
  }
1407
1407
  async function mintToken() {
1408
- const response = await adminApiPost("/api/admin/youtube/token");
1408
+ const response = await adminApiPost("/api/v1/admin/youtube/token");
1409
1409
  return response.accessToken;
1410
1410
  }
1411
1411
  async function putChunk(args) {
@@ -1465,7 +1465,7 @@ function nextOffset(rangeHeader, fallback) {
1465
1465
  return Number(match[1]) + 1;
1466
1466
  }
1467
1467
  async function resolveMixtapeId(idOrLogId) {
1468
- const response = await adminApiGet("/api/admin/mixtapes");
1468
+ const response = await adminApiGet("/api/v1/admin/mixtapes");
1469
1469
  const match = response.mixtapes.find((mixtape) => mixtape.id === idOrLogId || mixtape.logId === idOrLogId);
1470
1470
  if (!match?.id) {
1471
1471
  throw new CliError2("mixtape_not_found", `No mixtape with id or Log ID ${idOrLogId}`);
@@ -1553,18 +1553,18 @@ async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = f
1553
1553
  const externalId = result.key;
1554
1554
  const url = `https://www.mixcloud.com${result.key}`;
1555
1555
  onProgress?.("Mixcloud: recording the link…");
1556
- await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/finalize`, {
1556
+ await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/finalize`, {
1557
1557
  externalId,
1558
1558
  url
1559
1559
  });
1560
1560
  return { url };
1561
1561
  }
1562
1562
  async function resyncMixcloud(mixtapeId) {
1563
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/resync`);
1563
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/resync`);
1564
1564
  return { url: response.url };
1565
1565
  }
1566
1566
  async function authMixcloudCommand() {
1567
- const response = await adminApiGet("/api/admin/mixcloud/auth/start");
1567
+ const response = await adminApiGet("/api/v1/admin/mixcloud/auth/start");
1568
1568
  console.log(`Open this Mixcloud authorization URL:
1569
1569
 
1570
1570
  ${response.authUrl}
@@ -1591,7 +1591,7 @@ function mixtapeTags(_mixtape) {
1591
1591
  }
1592
1592
  async function fetchMixcloudToken() {
1593
1593
  try {
1594
- const response = await adminApiPost("/api/admin/mixcloud/token");
1594
+ const response = await adminApiPost("/api/v1/admin/mixcloud/token");
1595
1595
  return response.accessToken;
1596
1596
  } catch {
1597
1597
  throw new CliError2("mixcloud_not_connected", "Mixcloud is not connected. Run `fluncle admin auth mixcloud` to authorize it.");
@@ -1648,11 +1648,11 @@ __export(exports_mixtapes, {
1648
1648
  mixtapeDistributeCommand: () => mixtapeDistributeCommand
1649
1649
  });
1650
1650
  async function mixtapesCommand() {
1651
- const response = await publicApiGet("/api/mixtapes");
1651
+ const response = await publicApiGet("/api/v1/mixtapes");
1652
1652
  return response.mixtapes;
1653
1653
  }
1654
1654
  async function mixtapeUpdateCommand(id, options) {
1655
- return adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(id)}`, buildBody(options));
1655
+ return adminApiPatch(`/api/v1/admin/mixtapes/${encodeURIComponent(id)}`, buildBody(options));
1656
1656
  }
1657
1657
  async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {}) {
1658
1658
  const mixtape = await mixtapeGetCommand(idOrLogId);
@@ -1723,7 +1723,7 @@ async function mixtapeResyncCommand(idOrLogId, options, onProgress = () => {}) {
1723
1723
  let doYoutube = Boolean(options.youtube);
1724
1724
  let doMixcloud = Boolean(options.mixcloud);
1725
1725
  if (!explicit) {
1726
- const social = await adminApiGet(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/social`);
1726
+ const social = await adminApiGet(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/social`);
1727
1727
  const platforms = new Set(social.posts.map((post) => post.platform));
1728
1728
  doYoutube = platforms.has("youtube");
1729
1729
  doMixcloud = platforms.has("mixcloud");
@@ -1942,7 +1942,7 @@ async function recentCommand2(limit) {
1942
1942
  if (cursor) {
1943
1943
  params.set("cursor", cursor);
1944
1944
  }
1945
- const response = await publicApiGet(`/api/tracks?${params.toString()}`);
1945
+ const response = await publicApiGet(`/api/v1/findings?${params.toString()}`);
1946
1946
  for (const apiTrack of response.tracks) {
1947
1947
  results.push(mapTrack2(apiTrack));
1948
1948
  if (results.length >= limit) {
@@ -2031,7 +2031,7 @@ __export(exports_random, {
2031
2031
  randomCommand: () => randomCommand
2032
2032
  });
2033
2033
  async function randomCommand() {
2034
- const response = await publicApiGet("/api/tracks/random");
2034
+ const response = await publicApiGet("/api/v1/tracks/random");
2035
2035
  return response.track;
2036
2036
  }
2037
2037
  var init_random = __esm(() => {
@@ -2048,7 +2048,7 @@ async function subscribeCommand(email, json) {
2048
2048
  if (!address) {
2049
2049
  throw new CliError2("invalid_email", "Enter a valid email address.");
2050
2050
  }
2051
- await publicApiPost("/api/newsletter", {
2051
+ await publicApiPost("/api/v1/newsletter", {
2052
2052
  email: address
2053
2053
  });
2054
2054
  if (json) {
@@ -2076,7 +2076,7 @@ async function submitCommand(input) {
2076
2076
  if (!query) {
2077
2077
  throw new CliError2("missing_query", "Missing search input");
2078
2078
  }
2079
- const response = await publicApiGet(`/api/search?q=${encodeURIComponent(query)}`);
2079
+ const response = await publicApiGet(`/api/v1/search?q=${encodeURIComponent(query)}`);
2080
2080
  if (response.results.length === 0) {
2081
2081
  throw new CliError2("no_results", "No Spotify tracks found.");
2082
2082
  }
@@ -2086,7 +2086,7 @@ async function submitCommand(input) {
2086
2086
  }
2087
2087
  const note = await promptLine("Note (optional): ", PROMPT_NON_INTERACTIVE_MESSAGE2);
2088
2088
  const contact = await promptLine("Contact (optional): ", PROMPT_NON_INTERACTIVE_MESSAGE2);
2089
- await publicApiPost("/api/submissions", {
2089
+ await publicApiPost("/api/v1/submissions", {
2090
2090
  album: selected.album,
2091
2091
  artists: selected.artists,
2092
2092
  artworkUrl: selected.artworkUrl,
@@ -2229,7 +2229,7 @@ async function pollForToken(baseUrl, device) {
2229
2229
  throw new CliError2("expired_token", "The login request timed out. Run `fluncle login` again.");
2230
2230
  }
2231
2231
  async function fetchSessionUser(baseUrl, token) {
2232
- const response = await fetch(`${baseUrl}/api/me`, {
2232
+ const response = await fetch(`${baseUrl}/api/v1/me`, {
2233
2233
  headers: { Authorization: `Bearer ${token}` }
2234
2234
  });
2235
2235
  if (!response.ok) {
@@ -2264,8 +2264,8 @@ __export(exports_me, {
2264
2264
  });
2265
2265
  async function meCommand() {
2266
2266
  const [me, progress] = await Promise.all([
2267
- userApiGet("/api/me"),
2268
- userApiGet("/api/me/galaxy-progress")
2267
+ userApiGet("/api/v1/me"),
2268
+ userApiGet("/api/v1/me/galaxy-progress")
2269
2269
  ]);
2270
2270
  if (!me.user) {
2271
2271
  throw new Error("Your sign-in expired. Run `fluncle login` to link this device again.");
@@ -2442,7 +2442,7 @@ function parseVersion2(version) {
2442
2442
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2443
2443
  var init_version2 = __esm(() => {
2444
2444
  init_output();
2445
- currentVersion2 = "0.185.0".trim() ? "0.185.0".trim() : "0.1.0";
2445
+ currentVersion2 = "0.187.0".trim() ? "0.187.0".trim() : "0.1.0";
2446
2446
  });
2447
2447
 
2448
2448
  // ../../packages/registry/src/index.ts
@@ -2761,7 +2761,22 @@ var init_src = __esm(() => {
2761
2761
  {
2762
2762
  apiFormat: "application/json",
2763
2763
  discoveryUrl: `${SITE}/api/v1/openapi.json`,
2764
- exposedContent: ["the archive as JSON, cursor-paginated (limit max 48, cursor)"],
2764
+ exposedContent: [
2765
+ "the feed as JSON — findings and published mixtapes, newest found first, cursor-paginated (limit max 48, cursor)"
2766
+ ],
2767
+ kind: "api",
2768
+ name: "api.findings",
2769
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2770
+ route: "/api/v1/findings",
2771
+ url: `${SITE}/api/v1/findings`,
2772
+ weights: { status: "secondary", web: "primary" }
2773
+ },
2774
+ {
2775
+ apiFormat: "application/json",
2776
+ discoveryUrl: `${SITE}/api/v1/openapi.json`,
2777
+ exposedContent: [
2778
+ "every track Fluncle holds as JSON, newest release first, numbered pages (page); certified=true narrows to findings, certified=false to the rest, omitted returns both"
2779
+ ],
2765
2780
  kind: "api",
2766
2781
  name: "api.tracks",
2767
2782
  probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
@@ -2940,8 +2955,8 @@ var init_src = __esm(() => {
2940
2955
  name: "api.health",
2941
2956
  operatorNotes: "Linked as the `status` relation from /.well-known/api-catalog. Service `web` on /status.",
2942
2957
  probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2943
- route: "/api/health",
2944
- url: `${SITE}/api/health`,
2958
+ route: "/api/v1/health",
2959
+ url: `${SITE}/api/v1/health`,
2945
2960
  weights: { status: "tertiary", web: "tertiary" }
2946
2961
  },
2947
2962
  {
@@ -3144,7 +3159,7 @@ var init_src = __esm(() => {
3144
3159
  apiFormat: "application/json",
3145
3160
  discoveryUrl: `${SITE}/.well-known/mcp/server-card.json`,
3146
3161
  exposedContent: [
3147
- "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",
3162
+ "the archive as MCP tools (Streamable HTTP, no auth): list_findings, list_tracks, list_fresh, get_track, get_random_track, get_status, search_archive, get_artist, get_label, build_set, list_similar_artists, list_album_catalogue, list_artist_catalogue, list_label_catalogue, search_tracks, submit_track, subscribe_newsletter",
3148
3163
  "the archive as MCP resources: each finding/mixtape at fluncle://finding/<logId> or fluncle://mixtape/<logId> (its public record)",
3149
3164
  "Fluncle-voiced MCP prompts: recommend_finding, walk_recent_night, decode_coordinate"
3150
3165
  ],
@@ -3457,6 +3472,19 @@ var init_src = __esm(() => {
3457
3472
  title: "Artist edges",
3458
3473
  weights: { status: "hidden" }
3459
3474
  },
3475
+ {
3476
+ command: "fluncle admin backfills artist-credits",
3477
+ exposedContent: [
3478
+ "mint identity-true artists from MusicBrainz credits for slice 0's zero-matched residual → track_artists edges"
3479
+ ],
3480
+ kind: "cron",
3481
+ name: "cron.artist-credits",
3482
+ operatorNotes: "every 5m, run by a rave-02 HOST systemd timer (docs/agents/hermes/artist-credits-timer/). The MB CREDIT SWEEP (RFC artist-primary-capture, slice 1b): the sibling that completes what `backfill_artist_edges` (slice 0) could not. Slice 0's name-fold left a ~14.3k ZERO-MATCHED residual (a track it stamped but wrote no edge — no credited name folded to an existing identity), and those are exactly the tracks capture-authorization (slice 1) cannot reason about, since it matches BY IDENTITY through the `track_artists` graph. This sweep picks up that residual: for each zero-matched track carrying a MusicBrainz recording identity (`mb_recording_id`, or the `mb_<recording-mbid>` PK a crawler-born row carries), ONE paced `/recording/<mbid>?inc=artist-credits` lookup through the shared MusicBrainz client names its credited artists WITH their MB artist ids, and each resolves down a three-rung ladder: an EXACT `mbid` match, else an ADOPT (the credit name folds unambiguously onto an existing artist with no mbid — the common case, since the residual is dominated by compound credit strings like 'Sub Focus & Dimension' whose members Fluncle already holds as Spotify-keyed rows; adopting `coalesce`s the mbid onto that row instead of minting a duplicate, the split-identity guard), else a MINT of a fresh identity-true row (`mintArtistByMbid` — a real MBID is identity, the licence slice 0 lacked). Fail-closed on any ambiguity (a shared fold, or a fold whose row carries a different mbid) → mint, never a wrong merge. Then the `track_artists` edges are written. A zero-matched track with NO MB identity is TERMINALLY SKIPPED (stamped, never retried). METADATA / GRAPH IDENTITY ONLY — it certifies nothing and publishes nothing (agent tier, the `backfill_recording_mbids` precedent). Worker-paced (the box holds no MusicBrainz budget): a SMALL bounded batch per tick (each row is one ~1.1s MB call), 1 req/s, circuit-broken on a throttle, with a 60s response budget that pauses mid-page and resumes on the next request. The `tracks` row carries the durable per-row reliability state — its OWN `artist_credits_backfilled_at` stamp, DISTINCT from slice 0's `artist_edges_backfilled_at` (never disturbed). The ~14.3k residual drains at ~40/tick over ~a day and a half. Zero LLM tokens. Source: docs/agents/hermes/scripts/artist-credits-sweep.*. See docs/artist-relationship.md.",
3483
+ probeConfig: { cadenceMs: 5 * MINUTE_MS, cronName: "fluncle-artist-credits", kind: "cron" },
3484
+ statusDescription: "mints artists from MusicBrainz credits for the residual",
3485
+ title: "Artist credits",
3486
+ weights: { status: "hidden" }
3487
+ },
3460
3488
  {
3461
3489
  command: "fluncle admin backfills label-lineage",
3462
3490
  exposedContent: [
@@ -3883,7 +3911,7 @@ __export(exports_status, {
3883
3911
  statusCommand: () => statusCommand
3884
3912
  });
3885
3913
  async function statusCommand() {
3886
- return publicApiGet("/api/status");
3914
+ return publicApiGet("/api/v1/status");
3887
3915
  }
3888
3916
  function ago(seconds) {
3889
3917
  if (seconds < 60) {
@@ -3967,14 +3995,14 @@ __export(exports_track, {
3967
3995
  checkBundleCompleteness: () => checkBundleCompleteness
3968
3996
  });
3969
3997
  async function trackGetCommand(idOrLogId) {
3970
- return publicApiGet(`/api/tracks/${encodeURIComponent(idOrLogId)}`);
3998
+ return publicApiGet(`/api/v1/tracks/${encodeURIComponent(idOrLogId)}`);
3971
3999
  }
3972
4000
  async function trackSimilarCommand(idOrLogId, limit) {
3973
4001
  const query = limit === undefined ? "" : `?limit=${encodeURIComponent(String(limit))}`;
3974
- return publicApiGet(`/api/tracks/${encodeURIComponent(idOrLogId)}/similar${query}`);
4002
+ return publicApiGet(`/api/v1/tracks/${encodeURIComponent(idOrLogId)}/similar${query}`);
3975
4003
  }
3976
4004
  async function trackGetAdminCommand(idOrLogId) {
3977
- return adminApiGet(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}`);
4005
+ return adminApiGet(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}`);
3978
4006
  }
3979
4007
  function isPlatesOnlyUpload(files) {
3980
4008
  const hasPlate = PLATE_FIELDS.some((option) => Boolean(files[option]));
@@ -4021,7 +4049,7 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
4021
4049
  throw new CliError2("nothing_to_upload", "No bundle files resolved to upload (pass --dir <bundle> or explicit file flags).");
4022
4050
  }
4023
4051
  const platesOnly = isPlatesOnlyUpload(files);
4024
- const presign = await adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/video/uploads`, { fields: present.map((spec) => spec.field) });
4052
+ const presign = await adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/video/uploads`, { fields: present.map((spec) => spec.field) });
4025
4053
  const byField = new Map(presign.uploads.map((upload) => [upload.field, upload]));
4026
4054
  const urls = {};
4027
4055
  const STREAM_THRESHOLD_BYTES = 8 * 1024 * 1024;
@@ -4052,7 +4080,7 @@ async function trackVideoCommand(idOrLogId, files, onProgress, options = {}) {
4052
4080
  const videoModel = files.model?.trim().slice(0, 120) || manifest.model || DEFAULT_VIDEO_MODEL;
4053
4081
  const videoModelReasoning = files.reasoning?.trim().slice(0, 120) || manifest.reasoning || DEFAULT_VIDEO_REASONING;
4054
4082
  const squared = Boolean(urls["footage"] && urls["footage-social"]);
4055
- const finalize = await adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/video/finalize`, {
4083
+ const finalize = await adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/video/finalize`, {
4056
4084
  videoModel,
4057
4085
  videoModelReasoning,
4058
4086
  ...squared ? { squared: true } : {},
@@ -4083,7 +4111,7 @@ async function readManifestFields(renderPath) {
4083
4111
  return result;
4084
4112
  }
4085
4113
  async function trackDraftCommand(idOrLogId, platform2) {
4086
- return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/social/${platform2}/draft`);
4114
+ return adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/social/${platform2}/draft`);
4087
4115
  }
4088
4116
  async function trackSocialUpdateCommand(idOrLogId, platform2, options) {
4089
4117
  const body = { status: options.status };
@@ -4093,14 +4121,14 @@ async function trackSocialUpdateCommand(idOrLogId, platform2, options) {
4093
4121
  if (options.scheduledFor !== undefined) {
4094
4122
  body.scheduledFor = options.scheduledFor;
4095
4123
  }
4096
- return adminApiPatch(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/social/${platform2}`, body);
4124
+ return adminApiPatch(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/social/${platform2}`, body);
4097
4125
  }
4098
4126
  async function trackSocialShowCommand(idOrLogId) {
4099
- return adminApiGet(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/social`);
4127
+ return adminApiGet(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/social`);
4100
4128
  }
4101
4129
  async function trackSocialCaptureCommand(limit) {
4102
4130
  const body = limit === undefined ? {} : { limit: String(limit) };
4103
- return adminApiPost(`/api/admin/social/posts/capture`, body);
4131
+ return adminApiPost(`/api/v1/admin/social/posts/capture`, body);
4104
4132
  }
4105
4133
  async function trackUpdateCommand(trackId, options) {
4106
4134
  const body = {};
@@ -4146,13 +4174,13 @@ async function trackUpdateCommand(trackId, options) {
4146
4174
  if (options.analyzedAt !== undefined) {
4147
4175
  body.analyzedAt = options.analyzedAt;
4148
4176
  }
4149
- return adminApiPatch(`/api/admin/tracks/${encodeURIComponent(trackId)}`, body);
4177
+ return adminApiPatch(`/api/v1/admin/tracks/${encodeURIComponent(trackId)}`, body);
4150
4178
  }
4151
4179
  async function trackRequeueVideoCommand(idOrLogId) {
4152
- return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/video/requeue`);
4180
+ return adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/video/requeue`);
4153
4181
  }
4154
4182
  async function trackPurgeVideoCommand(idOrLogId) {
4155
- return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/video/purge`);
4183
+ return adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/video/purge`);
4156
4184
  }
4157
4185
  async function trackObserveCommand(idOrLogId, options) {
4158
4186
  const body = { script: options.script };
@@ -4174,7 +4202,7 @@ async function trackObserveCommand(idOrLogId, options) {
4174
4202
  if (options.force) {
4175
4203
  body.force = true;
4176
4204
  }
4177
- return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/observe`, body);
4205
+ return adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/observe`, body);
4178
4206
  }
4179
4207
  async function trackContextCommand(idOrLogId, options = {}) {
4180
4208
  const body = {};
@@ -4184,7 +4212,7 @@ async function trackContextCommand(idOrLogId, options = {}) {
4184
4212
  if (options.refresh) {
4185
4213
  body.refresh = true;
4186
4214
  }
4187
- return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/context`, body);
4215
+ return adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/context`, body);
4188
4216
  }
4189
4217
  async function trackNoteCommand(idOrLogId, options) {
4190
4218
  const body = { note: options.note };
@@ -4194,7 +4222,7 @@ async function trackNoteCommand(idOrLogId, options) {
4194
4222
  if (typeof options.promptVersion === "number") {
4195
4223
  body.promptVersion = options.promptVersion;
4196
4224
  }
4197
- return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
4225
+ return adminApiPost(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
4198
4226
  }
4199
4227
  var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS, RERENDER_CONTRACT_FIELDS, RERENDER_ADVISORY_FIELDS, FOOTAGE_FIELDS, PLATE_FIELDS, NON_FILE_OPTIONS, DIVERSITY_LEDGER_FIELDS;
4200
4228
  var init_track = __esm(() => {
@@ -4247,7 +4275,7 @@ __export(exports_admin_attention, {
4247
4275
  attentionQueueCommand: () => attentionQueueCommand
4248
4276
  });
4249
4277
  async function attentionQueueCommand() {
4250
- const response = await adminApiGet("/api/admin/attention");
4278
+ const response = await adminApiGet("/api/v1/admin/attention");
4251
4279
  return response.attention;
4252
4280
  }
4253
4281
  function attentionQueueLines(queue) {
@@ -4291,7 +4319,7 @@ __export(exports_add, {
4291
4319
  addCommand: () => addCommand
4292
4320
  });
4293
4321
  async function addCommand(spotifyUrl, options) {
4294
- const result = await adminApiPost("/api/admin/tracks", {
4322
+ const result = await adminApiPost("/api/v1/admin/tracks", {
4295
4323
  dryRun: options.dryRun,
4296
4324
  note: options.note,
4297
4325
  spotifyUrl
@@ -4356,7 +4384,7 @@ async function trackUpdateCommand2(trackId, options) {
4356
4384
  if (options.analyzedAt !== undefined) {
4357
4385
  body.analyzedAt = options.analyzedAt;
4358
4386
  }
4359
- return adminApiPatch(`/api/admin/tracks/${encodeURIComponent(trackId)}`, body);
4387
+ return adminApiPatch(`/api/v1/admin/tracks/${encodeURIComponent(trackId)}`, body);
4360
4388
  }
4361
4389
  var init_track2 = __esm(() => {
4362
4390
  init_api();
@@ -4382,6 +4410,7 @@ __export(exports_admin_tracks, {
4382
4410
  backfillLastfmCommand: () => backfillLastfmCommand,
4383
4411
  backfillDiscogsCommand: () => backfillDiscogsCommand,
4384
4412
  backfillArtistEdgesCommand: () => backfillArtistEdgesCommand,
4413
+ backfillArtistCreditsCommand: () => backfillArtistCreditsCommand,
4385
4414
  backfillAppleMusicCommand: () => backfillAppleMusicCommand,
4386
4415
  backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
4387
4416
  });
@@ -4433,7 +4462,7 @@ async function fetchAdminTracks(options) {
4433
4462
  if (cursor) {
4434
4463
  params.set("cursor", cursor);
4435
4464
  }
4436
- const response = await adminApiGet(`/api/admin/tracks?${params.toString()}`);
4465
+ const response = await adminApiGet(`/api/v1/admin/tracks?${params.toString()}`);
4437
4466
  for (const apiTrack of response.tracks) {
4438
4467
  const track = mapTrack2(apiTrack);
4439
4468
  if (track.type === "mixtape") {
@@ -4482,7 +4511,7 @@ async function trackWorkCommand(options) {
4482
4511
  if (options.count) {
4483
4512
  params.set("count", "true");
4484
4513
  }
4485
- const response = await adminApiGet(`/api/admin/tracks/work?${params.toString()}`);
4514
+ const response = await adminApiGet(`/api/v1/admin/tracks/work?${params.toString()}`);
4486
4515
  return { queued: response.queued, tracks: response.tracks ?? [] };
4487
4516
  }
4488
4517
  async function requeueAnalysisCommand(options) {
@@ -4547,39 +4576,46 @@ async function backfillLastfmCommand(limit, dryRun, cursor) {
4547
4576
  if (cursor) {
4548
4577
  params.set("cursor", cursor);
4549
4578
  }
4550
- return adminApiPost(`/api/admin/backfill/lastfm?${params.toString()}`);
4579
+ return adminApiPost(`/api/v1/admin/backfill/lastfm?${params.toString()}`);
4551
4580
  }
4552
4581
  async function backfillDiscogsCommand(limit, dryRun, cursor) {
4553
4582
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4554
4583
  if (cursor) {
4555
4584
  params.set("cursor", cursor);
4556
4585
  }
4557
- return adminApiPost(`/api/admin/backfill/discogs?${params.toString()}`);
4586
+ return adminApiPost(`/api/v1/admin/backfill/discogs?${params.toString()}`);
4558
4587
  }
4559
4588
  async function backfillAppleMusicCommand(limit, dryRun, cursor) {
4560
4589
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4561
4590
  if (cursor) {
4562
4591
  params.set("cursor", cursor);
4563
4592
  }
4564
- return adminApiPost(`/api/admin/backfill/apple-music?${params.toString()}`);
4593
+ return adminApiPost(`/api/v1/admin/backfill/apple-music?${params.toString()}`);
4565
4594
  }
4566
4595
  async function backfillAppleCatalogueCommand(limit, dryRun) {
4567
4596
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4568
- return adminApiPost(`/api/admin/backfill/apple-catalogue?${params.toString()}`);
4597
+ return adminApiPost(`/api/v1/admin/backfill/apple-catalogue?${params.toString()}`);
4569
4598
  }
4570
4599
  async function backfillRecordingMbidsCommand(limit, dryRun, cursor) {
4571
4600
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4572
4601
  if (cursor) {
4573
4602
  params.set("cursor", cursor);
4574
4603
  }
4575
- return adminApiPost(`/api/admin/backfill/recording-mbids?${params.toString()}`);
4604
+ return adminApiPost(`/api/v1/admin/backfill/recording-mbids?${params.toString()}`);
4576
4605
  }
4577
4606
  async function backfillArtistEdgesCommand(limit, dryRun, cursor) {
4578
4607
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4579
4608
  if (cursor) {
4580
4609
  params.set("cursor", cursor);
4581
4610
  }
4582
- return adminApiPost(`/api/admin/backfill/artist-edges?${params.toString()}`);
4611
+ return adminApiPost(`/api/v1/admin/backfill/artist-edges?${params.toString()}`);
4612
+ }
4613
+ async function backfillArtistCreditsCommand(limit, dryRun, cursor) {
4614
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4615
+ if (cursor) {
4616
+ params.set("cursor", cursor);
4617
+ }
4618
+ return adminApiPost(`/api/v1/admin/backfill/artist-credits?${params.toString()}`);
4583
4619
  }
4584
4620
  async function vehiclesCommand(limit) {
4585
4621
  const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
@@ -4599,7 +4635,7 @@ async function mixableOrderCommand(logIds, seed) {
4599
4635
  if (seed) {
4600
4636
  params.set("seed", seed);
4601
4637
  }
4602
- return adminApiGet(`/api/admin/tracks/mixable-order?${params.toString()}`);
4638
+ return adminApiGet(`/api/v1/admin/tracks/mixable-order?${params.toString()}`);
4603
4639
  }
4604
4640
  var pageSize3 = 48;
4605
4641
  var init_admin_tracks = __esm(() => {
@@ -4620,7 +4656,7 @@ async function previewArchiveUploadCommand(idOrLogId, options) {
4620
4656
  form.append("preview", Bun.file(options.file), basename(options.file));
4621
4657
  form.append("source", options.source);
4622
4658
  form.append("mime", options.mime);
4623
- return adminApiPostForm(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/preview`, form);
4659
+ return adminApiPostForm(`/api/v1/admin/tracks/${encodeURIComponent(idOrLogId)}/preview`, form);
4624
4660
  }
4625
4661
  async function previewArchiveBackfillCommand(options) {
4626
4662
  const result = {
@@ -4631,7 +4667,7 @@ async function previewArchiveBackfillCommand(options) {
4631
4667
  };
4632
4668
  let cursor;
4633
4669
  while (result.archived.length < (options.limit ?? Number.POSITIVE_INFINITY)) {
4634
- const page = await publicApiGet(`/api/tracks?limit=48${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`);
4670
+ const page = await publicApiGet(`/api/v1/findings?limit=48${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`);
4635
4671
  for (const track of page.tracks) {
4636
4672
  if (result.archived.length >= (options.limit ?? Number.POSITIVE_INFINITY)) {
4637
4673
  break;
@@ -4643,7 +4679,7 @@ async function previewArchiveBackfillCommand(options) {
4643
4679
  result.skipped.push({ reason: "no_log_id", trackId: track.trackId });
4644
4680
  continue;
4645
4681
  }
4646
- const status = await adminApiGet(`/api/admin/tracks/${encodeURIComponent(track.trackId)}/preview`);
4682
+ const status = await adminApiGet(`/api/v1/admin/tracks/${encodeURIComponent(track.trackId)}/preview`);
4647
4683
  if (status.archived) {
4648
4684
  result.skipped.push({ reason: "already_archived", trackId: track.trackId });
4649
4685
  continue;
@@ -4682,7 +4718,7 @@ async function uploadResolvedPreview(trackId, preview) {
4682
4718
  form.append("preview", new Blob([preview.bytes], { type: preview.mime }), `preview.${extension}`);
4683
4719
  form.append("source", preview.source);
4684
4720
  form.append("mime", preview.mime);
4685
- await adminApiPostForm(`/api/admin/tracks/${encodeURIComponent(trackId)}/preview`, form);
4721
+ await adminApiPostForm(`/api/v1/admin/tracks/${encodeURIComponent(trackId)}/preview`, form);
4686
4722
  }
4687
4723
  async function resolvePreview(track) {
4688
4724
  const stored = await downloadPreview(track.previewUrl, "deezer:stored");
@@ -4862,20 +4898,20 @@ async function distributeYoutube2(mixtapeId, videoPath, onProgress) {
4862
4898
  offset = 0;
4863
4899
  }
4864
4900
  onProgress?.("Recording the YouTube link");
4865
- await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/finalize`, { videoId });
4901
+ await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/finalize`, { videoId });
4866
4902
  return { url: `https://youtu.be/${videoId}`, videoId };
4867
4903
  }
4868
4904
  async function publishYoutubeCommand2(idOrLogId) {
4869
4905
  const mixtapeId = await resolveMixtapeId2(idOrLogId);
4870
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
4906
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
4871
4907
  return { url: response.url };
4872
4908
  }
4873
4909
  async function resyncYoutube2(mixtapeId) {
4874
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/resync`);
4910
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/resync`);
4875
4911
  return { url: response.url, videoId: response.videoId };
4876
4912
  }
4877
4913
  async function authYoutubeCommand2() {
4878
- const response = await adminApiGet("/api/admin/youtube/auth/start");
4914
+ const response = await adminApiGet("/api/v1/admin/youtube/auth/start");
4879
4915
  console.log(`Open this YouTube authorization URL:
4880
4916
 
4881
4917
  ${response.authUrl}
@@ -4883,11 +4919,11 @@ ${response.authUrl}
4883
4919
  After approving access, Google returns to the Fluncle admin callback and stores the refresh token server-side.`);
4884
4920
  }
4885
4921
  async function initiate2(mixtapeId, contentLength, contentType) {
4886
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/initiate`, { contentLength, contentType });
4922
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/initiate`, { contentLength, contentType });
4887
4923
  return { accessToken: response.accessToken, sessionUri: response.sessionUri };
4888
4924
  }
4889
4925
  async function mintToken2() {
4890
- const response = await adminApiPost("/api/admin/youtube/token");
4926
+ const response = await adminApiPost("/api/v1/admin/youtube/token");
4891
4927
  return response.accessToken;
4892
4928
  }
4893
4929
  async function putChunk2(args) {
@@ -4947,7 +4983,7 @@ function nextOffset2(rangeHeader, fallback) {
4947
4983
  return Number(match[1]) + 1;
4948
4984
  }
4949
4985
  async function resolveMixtapeId2(idOrLogId) {
4950
- const response = await adminApiGet("/api/admin/mixtapes");
4986
+ const response = await adminApiGet("/api/v1/admin/mixtapes");
4951
4987
  const match = response.mixtapes.find((mixtape) => mixtape.id === idOrLogId || mixtape.logId === idOrLogId);
4952
4988
  if (!match?.id) {
4953
4989
  throw new CliError2("mixtape_not_found", `No mixtape with id or Log ID ${idOrLogId}`);
@@ -4983,10 +5019,10 @@ __export(exports_publish, {
4983
5019
  publishAdvanceCommand: () => publishAdvanceCommand
4984
5020
  });
4985
5021
  async function publishAdvanceCommand() {
4986
- return adminApiPost("/api/admin/social/publish/advance", {});
5022
+ return adminApiPost("/api/v1/admin/social/publish/advance", {});
4987
5023
  }
4988
5024
  async function publishAdvancePauseCommand(paused) {
4989
- const response = await adminApiPut("/api/admin/social/publish/advance/state", { paused });
5025
+ const response = await adminApiPut("/api/v1/admin/social/publish/advance/state", { paused });
4990
5026
  return response.paused;
4991
5027
  }
4992
5028
  var init_publish = __esm(() => {
@@ -5000,10 +5036,10 @@ __export(exports_capture, {
5000
5036
  captureBudgetCommand: () => captureBudgetCommand
5001
5037
  });
5002
5038
  async function captureBudgetCommand() {
5003
- return adminApiGet("/api/admin/catalogue/capture-budget");
5039
+ return adminApiGet("/api/v1/admin/catalogue/capture-budget");
5004
5040
  }
5005
5041
  async function setCaptureBudgetCommand(input) {
5006
- return adminApiPut("/api/admin/catalogue/capture-budget", input);
5042
+ return adminApiPut("/api/v1/admin/catalogue/capture-budget", input);
5007
5043
  }
5008
5044
  var init_capture = __esm(() => {
5009
5045
  init_api();
@@ -5207,7 +5243,7 @@ __export(exports_recordings, {
5207
5243
  });
5208
5244
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
5209
5245
  async function recordingGet(id) {
5210
- const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
5246
+ const response = await adminApiGet(`/api/v1/admin/recordings/${encodeURIComponent(id)}`);
5211
5247
  return response.recording;
5212
5248
  }
5213
5249
  function formatRecordingSummary(recording) {
@@ -5225,7 +5261,7 @@ async function recordingsListCommand(options = {}) {
5225
5261
  query.set("parentId", options.parentId);
5226
5262
  }
5227
5263
  const suffix = query.toString() ? `?${query.toString()}` : "";
5228
- const response = await adminApiGet(`/api/admin/recordings${suffix}`);
5264
+ const response = await adminApiGet(`/api/v1/admin/recordings${suffix}`);
5229
5265
  if (options.json) {
5230
5266
  printJson2({ ok: true, recordings: response.recordings });
5231
5267
  return;
@@ -5255,7 +5291,7 @@ async function recordingGetCommand(id, options = {}) {
5255
5291
  }
5256
5292
  async function recordingCreateCommand(options = {}) {
5257
5293
  if (options.plan) {
5258
- const created2 = await adminApiPost("/api/admin/recordings", {
5294
+ const created2 = await adminApiPost("/api/v1/admin/recordings", {
5259
5295
  kind: "plan",
5260
5296
  recordedAt: options.recordedAt
5261
5297
  });
@@ -5277,7 +5313,7 @@ async function recordingCreateCommand(options = {}) {
5277
5313
  if (!existsSync2(options.video)) {
5278
5314
  throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
5279
5315
  }
5280
- const created = await adminApiPost("/api/admin/recordings", {
5316
+ const created = await adminApiPost("/api/v1/admin/recordings", {
5281
5317
  recordedAt: options.recordedAt,
5282
5318
  title
5283
5319
  });
@@ -5288,7 +5324,7 @@ async function recordingCreateCommand(options = {}) {
5288
5324
  }
5289
5325
  };
5290
5326
  log(`Recording ${recording.id} created, staging the set video…`);
5291
- const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
5327
+ const upload = await uploadRenditionMultipart(options.video, `/api/v1/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
5292
5328
  if (options.json) {
5293
5329
  printJson2({ key: upload.key, ok: true, recording, url: upload.url });
5294
5330
  return;
@@ -5322,7 +5358,7 @@ async function recordingUpdateCommand(id, options = {}) {
5322
5358
  if (Object.keys(body).length === 0) {
5323
5359
  throw new CliError2("no_fields", "Nothing to update. Pass --title, --recorded-at, --parent-id, or --tracklist-file");
5324
5360
  }
5325
- const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
5361
+ const response = await adminApiPatch(`/api/v1/admin/recordings/${encodeURIComponent(id)}`, body);
5326
5362
  if (options.json) {
5327
5363
  printJson2({ ok: true, recording: response.recording });
5328
5364
  return;
@@ -5333,7 +5369,7 @@ async function recordingDeleteCommand(id, options = {}) {
5333
5369
  if (!id) {
5334
5370
  throw new CliError2("missing_id", "Missing recording id for: delete");
5335
5371
  }
5336
- await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
5372
+ await adminApiDelete(`/api/v1/admin/recordings/${encodeURIComponent(id)}`);
5337
5373
  if (options.json) {
5338
5374
  printJson2({ ok: true });
5339
5375
  return;
@@ -5344,7 +5380,7 @@ async function recordingPromoteCommand(id, options = {}) {
5344
5380
  if (!id) {
5345
5381
  throw new CliError2("missing_id", "Missing recording id for: promote");
5346
5382
  }
5347
- const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
5383
+ const response = await adminApiPost(`/api/v1/admin/recordings/${encodeURIComponent(id)}/promote`);
5348
5384
  const recording = response.recording;
5349
5385
  if (options.json) {
5350
5386
  printJson2({ ok: true, recording });
@@ -5368,7 +5404,7 @@ async function recordingReplaceCuesCommand(id, options = {}) {
5368
5404
  } catch {
5369
5405
  throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
5370
5406
  }
5371
- const response = await adminApiPut(`/api/admin/recordings/${encodeURIComponent(id)}/cues`, { cues });
5407
+ const response = await adminApiPut(`/api/v1/admin/recordings/${encodeURIComponent(id)}/cues`, { cues });
5372
5408
  if (options.json) {
5373
5409
  printJson2({ ok: true, recording: response.recording });
5374
5410
  return;
@@ -5463,19 +5499,19 @@ async function clipsListCommand(filter = {}) {
5463
5499
  params.set("status", filter.status);
5464
5500
  }
5465
5501
  const query = params.toString();
5466
- const response = await adminApiGet(`/api/admin/clips${query ? `?${query}` : ""}`);
5502
+ const response = await adminApiGet(`/api/v1/admin/clips${query ? `?${query}` : ""}`);
5467
5503
  return response.clips;
5468
5504
  }
5469
5505
  async function clipPostsListCommand() {
5470
- const response = await adminApiGet("/api/admin/clips/social");
5506
+ const response = await adminApiGet("/api/v1/admin/clips/social");
5471
5507
  return response.posts;
5472
5508
  }
5473
5509
  async function clipScheduleCommand(clipId, scheduledFor) {
5474
- const response = await adminApiPatch(`/api/admin/clips/${encodeURIComponent(clipId)}/schedule`, { scheduledFor });
5510
+ const response = await adminApiPatch(`/api/v1/admin/clips/${encodeURIComponent(clipId)}/schedule`, { scheduledFor });
5475
5511
  return response.post;
5476
5512
  }
5477
5513
  async function clipDripPauseCommand(paused) {
5478
- const response = await adminApiPut("/api/admin/clips/drip/state", {
5514
+ const response = await adminApiPut("/api/v1/admin/clips/drip/state", {
5479
5515
  paused
5480
5516
  });
5481
5517
  return response.paused;
@@ -5516,9 +5552,9 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
5516
5552
  throw new CliError2("clip_too_large", `The cut is ${(sizeBytes / 1e6).toFixed(1)} MB (> 100 MB Cloudflare MT ceiling). Shorten the window or lower the bitrate cap`);
5517
5553
  }
5518
5554
  onProgress(`Clip ${clipId}: uploading ${(sizeBytes / 1e6).toFixed(1)} MB…`);
5519
- const presign = await adminApiPost(`/api/admin/clips/${encodeURIComponent(clipId)}/cut/presign`, { contentType: "video/mp4" });
5555
+ const presign = await adminApiPost(`/api/v1/admin/clips/${encodeURIComponent(clipId)}/cut/presign`, { contentType: "video/mp4" });
5520
5556
  await putClip(presign.url, presign.contentType, outputPath);
5521
- await adminApiPost(`/api/admin/clips/${encodeURIComponent(clipId)}/cut/finalize`);
5557
+ await adminApiPost(`/api/v1/admin/clips/${encodeURIComponent(clipId)}/cut/finalize`);
5522
5558
  onProgress(`Clip ${clipId}: done → ${FOUND_BASE3}/${presign.key}`);
5523
5559
  return {
5524
5560
  clipId,
@@ -5588,7 +5624,7 @@ __export(exports_recordings2, {
5588
5624
  });
5589
5625
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
5590
5626
  async function recordingGet2(id) {
5591
- const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
5627
+ const response = await adminApiGet(`/api/v1/admin/recordings/${encodeURIComponent(id)}`);
5592
5628
  return response.recording;
5593
5629
  }
5594
5630
  function formatRecordingSummary2(recording) {
@@ -5606,7 +5642,7 @@ async function recordingsListCommand2(options = {}) {
5606
5642
  query.set("parentId", options.parentId);
5607
5643
  }
5608
5644
  const suffix = query.toString() ? `?${query.toString()}` : "";
5609
- const response = await adminApiGet(`/api/admin/recordings${suffix}`);
5645
+ const response = await adminApiGet(`/api/v1/admin/recordings${suffix}`);
5610
5646
  if (options.json) {
5611
5647
  printJson2({ ok: true, recordings: response.recordings });
5612
5648
  return;
@@ -5636,7 +5672,7 @@ async function recordingGetCommand2(id, options = {}) {
5636
5672
  }
5637
5673
  async function recordingCreateCommand2(options = {}) {
5638
5674
  if (options.plan) {
5639
- const created2 = await adminApiPost("/api/admin/recordings", {
5675
+ const created2 = await adminApiPost("/api/v1/admin/recordings", {
5640
5676
  kind: "plan",
5641
5677
  recordedAt: options.recordedAt
5642
5678
  });
@@ -5658,7 +5694,7 @@ async function recordingCreateCommand2(options = {}) {
5658
5694
  if (!existsSync3(options.video)) {
5659
5695
  throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
5660
5696
  }
5661
- const created = await adminApiPost("/api/admin/recordings", {
5697
+ const created = await adminApiPost("/api/v1/admin/recordings", {
5662
5698
  recordedAt: options.recordedAt,
5663
5699
  title
5664
5700
  });
@@ -5669,7 +5705,7 @@ async function recordingCreateCommand2(options = {}) {
5669
5705
  }
5670
5706
  };
5671
5707
  log(`Recording ${recording.id} created, staging the set video…`);
5672
- const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
5708
+ const upload = await uploadRenditionMultipart(options.video, `/api/v1/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
5673
5709
  if (options.json) {
5674
5710
  printJson2({ key: upload.key, ok: true, recording, url: upload.url });
5675
5711
  return;
@@ -5703,7 +5739,7 @@ async function recordingUpdateCommand2(id, options = {}) {
5703
5739
  if (Object.keys(body).length === 0) {
5704
5740
  throw new CliError2("no_fields", "Nothing to update. Pass --title, --recorded-at, --parent-id, or --tracklist-file");
5705
5741
  }
5706
- const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
5742
+ const response = await adminApiPatch(`/api/v1/admin/recordings/${encodeURIComponent(id)}`, body);
5707
5743
  if (options.json) {
5708
5744
  printJson2({ ok: true, recording: response.recording });
5709
5745
  return;
@@ -5714,7 +5750,7 @@ async function recordingDeleteCommand2(id, options = {}) {
5714
5750
  if (!id) {
5715
5751
  throw new CliError2("missing_id", "Missing recording id for: delete");
5716
5752
  }
5717
- await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
5753
+ await adminApiDelete(`/api/v1/admin/recordings/${encodeURIComponent(id)}`);
5718
5754
  if (options.json) {
5719
5755
  printJson2({ ok: true });
5720
5756
  return;
@@ -5725,7 +5761,7 @@ async function recordingPromoteCommand2(id, options = {}) {
5725
5761
  if (!id) {
5726
5762
  throw new CliError2("missing_id", "Missing recording id for: promote");
5727
5763
  }
5728
- const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
5764
+ const response = await adminApiPost(`/api/v1/admin/recordings/${encodeURIComponent(id)}/promote`);
5729
5765
  const recording = response.recording;
5730
5766
  if (options.json) {
5731
5767
  printJson2({ ok: true, recording });
@@ -5749,7 +5785,7 @@ async function recordingReplaceCuesCommand2(id, options = {}) {
5749
5785
  } catch {
5750
5786
  throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
5751
5787
  }
5752
- const response = await adminApiPut(`/api/admin/recordings/${encodeURIComponent(id)}/cues`, { cues });
5788
+ const response = await adminApiPut(`/api/v1/admin/recordings/${encodeURIComponent(id)}/cues`, { cues });
5753
5789
  if (options.json) {
5754
5790
  printJson2({ ok: true, recording: response.recording });
5755
5791
  return;
@@ -5805,20 +5841,20 @@ function readContentFile(filePath) {
5805
5841
  }
5806
5842
  }
5807
5843
  async function newsletterDraftCommand(options) {
5808
- return adminApiPost("/api/admin/newsletter/editions", buildBody2(options, { requireContent: true }));
5844
+ return adminApiPost("/api/v1/admin/newsletter/editions", buildBody2(options, { requireContent: true }));
5809
5845
  }
5810
5846
  async function newsletterUpdateCommand(id, options) {
5811
- return adminApiPatch(`/api/admin/newsletter/editions/${encodeURIComponent(id)}`, buildBody2(options, { requireContent: false }));
5847
+ return adminApiPatch(`/api/v1/admin/newsletter/editions/${encodeURIComponent(id)}`, buildBody2(options, { requireContent: false }));
5812
5848
  }
5813
5849
  async function newsletterSendCommand(id) {
5814
- return adminApiPost(`/api/admin/newsletter/editions/${encodeURIComponent(id)}/send`);
5850
+ return adminApiPost(`/api/v1/admin/newsletter/editions/${encodeURIComponent(id)}/send`);
5815
5851
  }
5816
5852
  async function newsletterListCommand() {
5817
- const response = await adminApiGet("/api/admin/newsletter/editions");
5853
+ const response = await adminApiGet("/api/v1/admin/newsletter/editions");
5818
5854
  return response.editions;
5819
5855
  }
5820
5856
  async function newsletterDeleteCommand(id) {
5821
- return adminApiDelete(`/api/admin/newsletter/editions/${encodeURIComponent(id)}`);
5857
+ return adminApiDelete(`/api/v1/admin/newsletter/editions/${encodeURIComponent(id)}`);
5822
5858
  }
5823
5859
  var init_newsletter = __esm(() => {
5824
5860
  init_api();
@@ -5835,7 +5871,7 @@ __export(exports_admin_logbook, {
5835
5871
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
5836
5872
  async function logbookGapsCommand(limit) {
5837
5873
  const query = typeof limit === "number" ? `?limit=${encodeURIComponent(String(limit))}` : "";
5838
- const response = await adminApiGet(`/api/admin/logbook/gaps${query}`);
5874
+ const response = await adminApiGet(`/api/v1/admin/logbook/gaps${query}`);
5839
5875
  return { gaps: response.gaps, spent: response.spent };
5840
5876
  }
5841
5877
  function resolveBody(options) {
@@ -5857,14 +5893,14 @@ function requireTitle(options) {
5857
5893
  return options.title;
5858
5894
  }
5859
5895
  async function logbookCreateCommand(sector, options) {
5860
- return adminApiPost(`/api/admin/logbook/${encodeURIComponent(sector)}`, {
5896
+ return adminApiPost(`/api/v1/admin/logbook/${encodeURIComponent(sector)}`, {
5861
5897
  body: resolveBody(options),
5862
5898
  ...typeof options.promptVersion === "number" ? { promptVersion: options.promptVersion } : {},
5863
5899
  title: requireTitle(options)
5864
5900
  });
5865
5901
  }
5866
5902
  async function logbookUpdateCommand(sector, options) {
5867
- return adminApiPatch(`/api/admin/logbook/${encodeURIComponent(sector)}`, {
5903
+ return adminApiPatch(`/api/v1/admin/logbook/${encodeURIComponent(sector)}`, {
5868
5904
  body: resolveBody(options),
5869
5905
  title: requireTitle(options)
5870
5906
  });
@@ -5894,11 +5930,11 @@ __export(exports_admin_prompts, {
5894
5930
  });
5895
5931
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
5896
5932
  async function promptsListCommand() {
5897
- const response = await adminApiGet("/api/admin/prompts");
5933
+ const response = await adminApiGet("/api/v1/admin/prompts");
5898
5934
  return response.prompts;
5899
5935
  }
5900
5936
  async function promptGetCommand(slug) {
5901
- return adminApiGet(`/api/admin/prompts/${encodeURIComponent(slug)}`);
5937
+ return adminApiGet(`/api/v1/admin/prompts/${encodeURIComponent(slug)}`);
5902
5938
  }
5903
5939
  async function promptDetailCommand(slug) {
5904
5940
  const prompts = await promptsListCommand();
@@ -5924,7 +5960,7 @@ function resolveBody2(options) {
5924
5960
  async function promptUpdateCommand(slug, options) {
5925
5961
  const body = resolveBody2(options);
5926
5962
  const note = options.note?.trim();
5927
- const response = await adminApiPost(`/api/admin/prompts/${encodeURIComponent(slug)}`, note ? { body, note } : { body });
5963
+ const response = await adminApiPost(`/api/v1/admin/prompts/${encodeURIComponent(slug)}`, note ? { body, note } : { body });
5928
5964
  return { slug, version: response.version };
5929
5965
  }
5930
5966
  async function promptRollbackCommand(slug, version) {
@@ -5948,7 +5984,7 @@ async function restore(detail, input) {
5948
5984
  if (input.body.trim() === detail.activeBody.trim()) {
5949
5985
  return { from: input.from, skipped: true, slug: detail.slug, version: detail.activeVersion };
5950
5986
  }
5951
- const response = await adminApiPost(`/api/admin/prompts/${encodeURIComponent(detail.slug)}`, { body: input.body, note: input.note });
5987
+ const response = await adminApiPost(`/api/v1/admin/prompts/${encodeURIComponent(detail.slug)}`, { body: input.body, note: input.note });
5952
5988
  return { from: input.from, skipped: false, slug: detail.slug, version: response.version };
5953
5989
  }
5954
5990
  function parseAgainst(value) {
@@ -6063,7 +6099,7 @@ var init_admin_prompts = __esm(() => {
6063
6099
 
6064
6100
  // src/commands/add.ts
6065
6101
  async function addCommand2(spotifyUrl, options) {
6066
- const result = await adminApiPost("/api/admin/tracks", {
6102
+ const result = await adminApiPost("/api/v1/admin/tracks", {
6067
6103
  dryRun: options.dryRun,
6068
6104
  note: options.note,
6069
6105
  spotifyUrl
@@ -6094,7 +6130,7 @@ __export(exports_submissions, {
6094
6130
  });
6095
6131
  import { createInterface as createInterface2 } from "node:readline/promises";
6096
6132
  async function listSubmissionsCommand(options = {}) {
6097
- const response = await adminApiGet("/api/admin/submissions");
6133
+ const response = await adminApiGet("/api/v1/admin/submissions");
6098
6134
  if (options.json) {
6099
6135
  printJson2({ ok: true, submissions: response.submissions });
6100
6136
  return;
@@ -6116,7 +6152,7 @@ async function reviewSubmissionCommand(submissionId, options = {}) {
6116
6152
  console.log(formatSubmissionDetail(submission));
6117
6153
  }
6118
6154
  async function rejectSubmissionCommand(submissionId, options = {}) {
6119
- const response = await adminApiPost(`/api/admin/submissions/${encodeURIComponent(submissionId)}/reject`);
6155
+ const response = await adminApiPost(`/api/v1/admin/submissions/${encodeURIComponent(submissionId)}/reject`);
6120
6156
  if (options.json) {
6121
6157
  printJson2({ ok: true, submission: response.submission });
6122
6158
  return;
@@ -6141,7 +6177,7 @@ async function approveSubmissionCommand(submissionId, options = {}) {
6141
6177
  json: options.json,
6142
6178
  note: submission.note
6143
6179
  });
6144
- const response = await adminApiPost(`/api/admin/submissions/${encodeURIComponent(submission.id)}/approve`);
6180
+ const response = await adminApiPost(`/api/v1/admin/submissions/${encodeURIComponent(submission.id)}/approve`);
6145
6181
  if (options.json) {
6146
6182
  printJson2({ ok: true, submission: response.submission });
6147
6183
  return;
@@ -6149,7 +6185,7 @@ async function approveSubmissionCommand(submissionId, options = {}) {
6149
6185
  console.log(`Approved ${formatTrackLine(submission)}.`);
6150
6186
  }
6151
6187
  async function triageSubmissionCommand(submissionId, verdict, options = {}) {
6152
- const response = await adminApiPost(`/api/admin/submissions/${encodeURIComponent(submissionId)}/triage`, {
6188
+ const response = await adminApiPost(`/api/v1/admin/submissions/${encodeURIComponent(submissionId)}/triage`, {
6153
6189
  ...typeof options.promptVersion === "number" ? { promptVersion: options.promptVersion } : {},
6154
6190
  verdict
6155
6191
  });
@@ -6160,7 +6196,7 @@ async function triageSubmissionCommand(submissionId, verdict, options = {}) {
6160
6196
  console.log(`Triaged ${formatTrackLine(response.submission)}: ${response.submission.triageVerdict ?? ""}`);
6161
6197
  }
6162
6198
  async function fetchSubmission(submissionId) {
6163
- const response = await adminApiGet(`/api/admin/submissions/${encodeURIComponent(submissionId)}`);
6199
+ const response = await adminApiGet(`/api/v1/admin/submissions/${encodeURIComponent(submissionId)}`);
6164
6200
  return response.submission;
6165
6201
  }
6166
6202
  async function confirm(label) {
@@ -6215,7 +6251,7 @@ __export(exports_auth, {
6215
6251
  authSpotifyCommand: () => authSpotifyCommand
6216
6252
  });
6217
6253
  async function authSpotifyCommand() {
6218
- const response = await adminApiGet("/api/admin/spotify/auth/start");
6254
+ const response = await adminApiGet("/api/v1/admin/spotify/auth/start");
6219
6255
  console.log(`Open this Spotify authorization URL:
6220
6256
 
6221
6257
  ${response.authUrl}
@@ -6285,18 +6321,18 @@ async function distributeMixcloud2(mixtapeId, audioPath, onProgress, unlisted =
6285
6321
  const externalId = result.key;
6286
6322
  const url = `https://www.mixcloud.com${result.key}`;
6287
6323
  onProgress?.("Mixcloud: recording the link…");
6288
- await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/finalize`, {
6324
+ await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/finalize`, {
6289
6325
  externalId,
6290
6326
  url
6291
6327
  });
6292
6328
  return { url };
6293
6329
  }
6294
6330
  async function resyncMixcloud2(mixtapeId) {
6295
- const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/resync`);
6331
+ const response = await adminApiPost(`/api/v1/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/resync`);
6296
6332
  return { url: response.url };
6297
6333
  }
6298
6334
  async function authMixcloudCommand2() {
6299
- const response = await adminApiGet("/api/admin/mixcloud/auth/start");
6335
+ const response = await adminApiGet("/api/v1/admin/mixcloud/auth/start");
6300
6336
  console.log(`Open this Mixcloud authorization URL:
6301
6337
 
6302
6338
  ${response.authUrl}
@@ -6323,7 +6359,7 @@ function mixtapeTags2(_mixtape) {
6323
6359
  }
6324
6360
  async function fetchMixcloudToken2() {
6325
6361
  try {
6326
- const response = await adminApiPost("/api/admin/mixcloud/token");
6362
+ const response = await adminApiPost("/api/v1/admin/mixcloud/token");
6327
6363
  return response.accessToken;
6328
6364
  } catch {
6329
6365
  throw new CliError2("mixcloud_not_connected", "Mixcloud is not connected. Run `fluncle admin auth mixcloud` to authorize it.");
@@ -6375,7 +6411,7 @@ __export(exports_auth_tiktok, {
6375
6411
  authTikTokCommand: () => authTikTokCommand
6376
6412
  });
6377
6413
  async function authTikTokCommand() {
6378
- const response = await adminApiGet("/api/admin/tiktok/auth/start");
6414
+ const response = await adminApiGet("/api/v1/admin/tiktok/auth/start");
6379
6415
  console.log(`Open this TikTok authorization URL:
6380
6416
 
6381
6417
  ${response.authUrl}
@@ -6393,7 +6429,7 @@ __export(exports_auth_lastfm, {
6393
6429
  });
6394
6430
  async function authLastfmCommand(options) {
6395
6431
  if (options.token?.trim()) {
6396
- const response2 = await adminApiPost("/api/admin/lastfm/auth/session", { token: options.token.trim() });
6432
+ const response2 = await adminApiPost("/api/v1/admin/lastfm/auth/session", { token: options.token.trim() });
6397
6433
  console.log(`Last.fm connected as ${response2.name || "fluncle"}.
6398
6434
 
6399
6435
  Set this as the Worker secret LASTFM_SESSION_KEY (it does not expire):
@@ -6405,7 +6441,7 @@ ${response2.sessionKey}
6405
6441
  Also store it in 1Password (Fluncle vault) alongside the API key + shared secret.`);
6406
6442
  return;
6407
6443
  }
6408
- const response = await adminApiGet("/api/admin/lastfm/auth/start");
6444
+ const response = await adminApiGet("/api/v1/admin/lastfm/auth/start");
6409
6445
  console.log(`Open this Last.fm authorization URL (logged in as fluncle) and click "Yes, allow access":
6410
6446
 
6411
6447
  ${response.authUrl}
@@ -6426,7 +6462,7 @@ __export(exports_admin_notes, {
6426
6462
  });
6427
6463
  async function noteHeldCommand(options) {
6428
6464
  const query = options.settled ? "?open=false" : "";
6429
- return adminApiGet(`/api/admin/note-rejections${query}`);
6465
+ return adminApiGet(`/api/v1/admin/note-rejections${query}`);
6430
6466
  }
6431
6467
  async function noteGateCommand(options) {
6432
6468
  const patch = {};
@@ -6437,10 +6473,10 @@ async function noteGateCommand(options) {
6437
6473
  patch.maxOverlap = options.maxOverlap;
6438
6474
  }
6439
6475
  if (Object.keys(patch).length === 0) {
6440
- const response2 = await adminApiGet("/api/admin/note-rejections");
6476
+ const response2 = await adminApiGet("/api/v1/admin/note-rejections");
6441
6477
  return response2.gate;
6442
6478
  }
6443
- const response = await adminApiPatch("/api/admin/note-gate", patch);
6479
+ const response = await adminApiPatch("/api/v1/admin/note-gate", patch);
6444
6480
  return response.gate;
6445
6481
  }
6446
6482
  var init_admin_notes = __esm(() => {
@@ -6455,7 +6491,7 @@ __export(exports_admin_observations, {
6455
6491
  });
6456
6492
  async function observationHeldCommand(options) {
6457
6493
  const query = options.settled ? "?open=false" : "";
6458
- return adminApiGet(`/api/admin/observation-rejections${query}`);
6494
+ return adminApiGet(`/api/v1/admin/observation-rejections${query}`);
6459
6495
  }
6460
6496
  async function observationGateCommand(options) {
6461
6497
  const patch = {};
@@ -6466,10 +6502,10 @@ async function observationGateCommand(options) {
6466
6502
  patch.maxOverlap = options.maxOverlap;
6467
6503
  }
6468
6504
  if (Object.keys(patch).length === 0) {
6469
- const response2 = await adminApiGet("/api/admin/observation-rejections");
6505
+ const response2 = await adminApiGet("/api/v1/admin/observation-rejections");
6470
6506
  return response2.gate;
6471
6507
  }
6472
- const response = await adminApiPatch("/api/admin/observation-gate", patch);
6508
+ const response = await adminApiPatch("/api/v1/admin/observation-gate", patch);
6473
6509
  return response.gate;
6474
6510
  }
6475
6511
  var init_admin_observations = __esm(() => {
@@ -6485,13 +6521,13 @@ __export(exports_admin_frontier, {
6485
6521
  });
6486
6522
  async function frontierRefreshCommand(options) {
6487
6523
  const limit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
6488
- return adminApiPost("/api/admin/frontier-playlists/refresh", limit ? { limit } : {});
6524
+ return adminApiPost("/api/v1/admin/frontier-playlists/refresh", limit ? { limit } : {});
6489
6525
  }
6490
6526
  async function frontierStatusCommand() {
6491
- return adminApiGet("/api/admin/frontier/minting");
6527
+ return adminApiGet("/api/v1/admin/frontier/minting");
6492
6528
  }
6493
6529
  async function frontierSetMintingCommand(open) {
6494
- return adminApiPut("/api/admin/frontier/minting", { open });
6530
+ return adminApiPut("/api/v1/admin/frontier/minting", { open });
6495
6531
  }
6496
6532
  var init_admin_frontier = __esm(() => {
6497
6533
  init_api();
@@ -6516,11 +6552,11 @@ __export(exports_admin_catalogue, {
6516
6552
  });
6517
6553
  async function catalogueRankCommand(options) {
6518
6554
  const limit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
6519
- const response = await adminApiPost("/api/admin/catalogue/rank", limit ? { limit } : {});
6555
+ const response = await adminApiPost("/api/v1/admin/catalogue/rank", limit ? { limit } : {});
6520
6556
  return { summary: response.summary, telescope: response.telescope };
6521
6557
  }
6522
6558
  async function catalogueDemandCommand() {
6523
- const response = await adminApiPost("/api/admin/catalogue/demand", {});
6559
+ const response = await adminApiPost("/api/v1/admin/catalogue/demand", {});
6524
6560
  return response.summary;
6525
6561
  }
6526
6562
  async function catalogueListCommand(options) {
@@ -6529,34 +6565,34 @@ async function catalogueListCommand(options) {
6529
6565
  if (options.limit) {
6530
6566
  params.set("limit", options.limit);
6531
6567
  }
6532
- return adminApiGet(`/api/admin/catalogue?${params.toString()}`);
6568
+ return adminApiGet(`/api/v1/admin/catalogue?${params.toString()}`);
6533
6569
  }
6534
6570
  async function clearWrongAudioCommand(trackId) {
6535
- return adminApiPost("/api/admin/catalogue/wrong-audio/clear", {
6571
+ return adminApiPost("/api/v1/admin/catalogue/wrong-audio/clear", {
6536
6572
  trackId
6537
6573
  });
6538
6574
  }
6539
6575
  async function requeueUnmatchedCommand() {
6540
- return adminApiPost("/api/admin/catalogue/captures/requeue-unmatched", {});
6576
+ return adminApiPost("/api/v1/admin/catalogue/captures/requeue-unmatched", {});
6541
6577
  }
6542
6578
  async function forceCaptureCommand(trackId) {
6543
- return adminApiPost("/api/admin/catalogue/force-capture", {
6579
+ return adminApiPost("/api/v1/admin/catalogue/force-capture", {
6544
6580
  trackId
6545
6581
  });
6546
6582
  }
6547
6583
  async function flagWrongAudioCommand(trackId) {
6548
- return adminApiPost("/api/admin/catalogue/wrong-audio/flag", {
6584
+ return adminApiPost("/api/v1/admin/catalogue/wrong-audio/flag", {
6549
6585
  trackId
6550
6586
  });
6551
6587
  }
6552
6588
  async function certifyTrackCommand(trackId, note) {
6553
- return adminApiPost("/api/admin/catalogue/certify", {
6589
+ return adminApiPost("/api/v1/admin/catalogue/certify", {
6554
6590
  ...note ? { note } : {},
6555
6591
  trackId
6556
6592
  });
6557
6593
  }
6558
6594
  async function setTrackDismissedCommand(trackId, dismissed) {
6559
- return adminApiPut("/api/admin/catalogue/dismissed", {
6595
+ return adminApiPut("/api/v1/admin/catalogue/dismissed", {
6560
6596
  dismissed,
6561
6597
  trackId
6562
6598
  });
@@ -6566,10 +6602,10 @@ async function listUnverifiedCapturesCommand(options) {
6566
6602
  if (options.limit) {
6567
6603
  params.set("limit", options.limit);
6568
6604
  }
6569
- return adminApiGet(`/api/admin/catalogue/captures/unverified?${params.toString()}`);
6605
+ return adminApiGet(`/api/v1/admin/catalogue/captures/unverified?${params.toString()}`);
6570
6606
  }
6571
6607
  async function verifyCaptureCommand(trackId, verdict) {
6572
- return adminApiPost("/api/admin/catalogue/captures/verify", {
6608
+ return adminApiPost("/api/v1/admin/catalogue/captures/verify", {
6573
6609
  trackId,
6574
6610
  verdict
6575
6611
  });
@@ -6579,10 +6615,10 @@ async function crawlCatalogueCommand(limit, maxHop, dryRun) {
6579
6615
  if (dryRun) {
6580
6616
  params.set("dryRun", "true");
6581
6617
  }
6582
- return adminApiPost(`/api/admin/catalogue/crawl?${params.toString()}`);
6618
+ return adminApiPost(`/api/v1/admin/catalogue/crawl?${params.toString()}`);
6583
6619
  }
6584
6620
  async function crawlStatusCommand() {
6585
- return adminApiGet("/api/admin/catalogue/crawl");
6621
+ return adminApiGet("/api/v1/admin/catalogue/crawl");
6586
6622
  }
6587
6623
  var CATALOGUE_LENSES;
6588
6624
  var init_admin_catalogue = __esm(() => {
@@ -6596,7 +6632,7 @@ __export(exports_admin_reach, {
6596
6632
  reachCollectCommand: () => reachCollectCommand
6597
6633
  });
6598
6634
  async function reachCollectCommand() {
6599
- return adminApiPost("/api/admin/reach/collect", {});
6635
+ return adminApiPost("/api/v1/admin/reach/collect", {});
6600
6636
  }
6601
6637
  var init_admin_reach = __esm(() => {
6602
6638
  init_api();
@@ -6626,13 +6662,13 @@ function buildBioBody(options) {
6626
6662
  return body;
6627
6663
  }
6628
6664
  async function describeArtistCommand(slug, options) {
6629
- return adminApiPost(`/api/admin/artists/${encodeURIComponent(slug)}/bio`, buildBioBody(options));
6665
+ return adminApiPost(`/api/v1/admin/artists/${encodeURIComponent(slug)}/bio`, buildBioBody(options));
6630
6666
  }
6631
6667
  async function draftArtistBioCommand(slug) {
6632
- return adminApiGet(`/api/admin/artists/${encodeURIComponent(slug)}/bio-draft`);
6668
+ return adminApiGet(`/api/v1/admin/artists/${encodeURIComponent(slug)}/bio-draft`);
6633
6669
  }
6634
6670
  async function artistsBioQueueCommand(limit) {
6635
- const response = await adminApiGet(`/api/admin/artists/bio-queue?limit=${limit}`);
6671
+ const response = await adminApiGet(`/api/v1/admin/artists/bio-queue?limit=${limit}`);
6636
6672
  return response.artists;
6637
6673
  }
6638
6674
  async function listArtistsCommand(limit, cursor) {
@@ -6640,14 +6676,14 @@ async function listArtistsCommand(limit, cursor) {
6640
6676
  if (cursor) {
6641
6677
  params.set("cursor", cursor);
6642
6678
  }
6643
- return adminApiGet(`/api/admin/artists?${params.toString()}`);
6679
+ return adminApiGet(`/api/v1/admin/artists?${params.toString()}`);
6644
6680
  }
6645
6681
  async function resolveArtistCommand(artistId) {
6646
- return adminApiPost(`/api/admin/artists/${encodeURIComponent(artistId)}/resolve`);
6682
+ return adminApiPost(`/api/v1/admin/artists/${encodeURIComponent(artistId)}/resolve`);
6647
6683
  }
6648
6684
  async function rankArtistsCommand(options) {
6649
6685
  const limit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
6650
- const response = await adminApiPost("/api/admin/artists/rank", limit ? { limit } : {});
6686
+ const response = await adminApiPost("/api/v1/admin/artists/rank", limit ? { limit } : {});
6651
6687
  return { summary: response.summary };
6652
6688
  }
6653
6689
  async function backfillArtistImagesCommand(limit, dryRun, cursor) {
@@ -6655,14 +6691,14 @@ async function backfillArtistImagesCommand(limit, dryRun, cursor) {
6655
6691
  if (cursor) {
6656
6692
  params.set("cursor", cursor);
6657
6693
  }
6658
- return adminApiPost(`/api/admin/backfill/artist-images?${params.toString()}`);
6694
+ return adminApiPost(`/api/v1/admin/backfill/artist-images?${params.toString()}`);
6659
6695
  }
6660
6696
  async function backfillArtistsCommand(limit, dryRun, cursor) {
6661
6697
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
6662
6698
  if (cursor) {
6663
6699
  params.set("cursor", cursor);
6664
6700
  }
6665
- return adminApiPost(`/api/admin/backfill/artists?${params.toString()}`);
6701
+ return adminApiPost(`/api/v1/admin/backfill/artists?${params.toString()}`);
6666
6702
  }
6667
6703
  var init_admin_artists = __esm(() => {
6668
6704
  init_api();
@@ -6694,17 +6730,17 @@ __export(exports_admin_labels, {
6694
6730
  backfillLabelImagesCommand: () => backfillLabelImagesCommand
6695
6731
  });
6696
6732
  async function mergeLabelCommand(losingSlug, canonicalSlug) {
6697
- const response = await adminApiPost(`/api/admin/labels/${encodeURIComponent(losingSlug)}/merge`, { canonicalSlug });
6733
+ const response = await adminApiPost(`/api/v1/admin/labels/${encodeURIComponent(losingSlug)}/merge`, { canonicalSlug });
6698
6734
  return response.result;
6699
6735
  }
6700
6736
  async function describeLabelCommand(slug, options) {
6701
- return adminApiPost(`/api/admin/labels/${encodeURIComponent(slug)}/bio`, buildBioBody2(options));
6737
+ return adminApiPost(`/api/v1/admin/labels/${encodeURIComponent(slug)}/bio`, buildBioBody2(options));
6702
6738
  }
6703
6739
  async function draftLabelBioCommand(slug) {
6704
- return adminApiGet(`/api/admin/labels/${encodeURIComponent(slug)}/bio-draft`);
6740
+ return adminApiGet(`/api/v1/admin/labels/${encodeURIComponent(slug)}/bio-draft`);
6705
6741
  }
6706
6742
  async function labelsBioQueueCommand(limit) {
6707
- const response = await adminApiGet(`/api/admin/labels/bio-queue?limit=${limit}`);
6743
+ const response = await adminApiGet(`/api/v1/admin/labels/bio-queue?limit=${limit}`);
6708
6744
  return response.labels;
6709
6745
  }
6710
6746
  async function backfillLabelImagesCommand(limit, dryRun, cursor) {
@@ -6712,14 +6748,14 @@ async function backfillLabelImagesCommand(limit, dryRun, cursor) {
6712
6748
  if (cursor) {
6713
6749
  params.set("cursor", cursor);
6714
6750
  }
6715
- return adminApiPost(`/api/admin/backfill/label-images?${params.toString()}`);
6751
+ return adminApiPost(`/api/v1/admin/backfill/label-images?${params.toString()}`);
6716
6752
  }
6717
6753
  async function backfillLabelLineageCommand(limit, dryRun, cursor) {
6718
6754
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
6719
6755
  if (cursor) {
6720
6756
  params.set("cursor", cursor);
6721
6757
  }
6722
- return adminApiPost(`/api/admin/backfill/label-lineage?${params.toString()}`);
6758
+ return adminApiPost(`/api/v1/admin/backfill/label-lineage?${params.toString()}`);
6723
6759
  }
6724
6760
  var init_admin_labels = __esm(() => {
6725
6761
  init_api();
@@ -6739,7 +6775,7 @@ async function backfillCoverMastersCommand(kind, limit, dryRun, cursor, retryNon
6739
6775
  if (retryNone) {
6740
6776
  params.set("retry", "none");
6741
6777
  }
6742
- return adminApiPost(`/api/admin/backfill/cover-masters?${params.toString()}`);
6778
+ return adminApiPost(`/api/v1/admin/backfill/cover-masters?${params.toString()}`);
6743
6779
  }
6744
6780
  var init_admin_covers = __esm(() => {
6745
6781
  init_api();
@@ -6753,13 +6789,13 @@ __export(exports_admin_albums, {
6753
6789
  albumsBioQueueCommand: () => albumsBioQueueCommand
6754
6790
  });
6755
6791
  async function describeAlbumCommand(slug, options) {
6756
- return adminApiPost(`/api/admin/albums/${encodeURIComponent(slug)}/bio`, buildBioBody2(options));
6792
+ return adminApiPost(`/api/v1/admin/albums/${encodeURIComponent(slug)}/bio`, buildBioBody2(options));
6757
6793
  }
6758
6794
  async function draftAlbumBioCommand(slug) {
6759
- return adminApiGet(`/api/admin/albums/${encodeURIComponent(slug)}/bio-draft`);
6795
+ return adminApiGet(`/api/v1/admin/albums/${encodeURIComponent(slug)}/bio-draft`);
6760
6796
  }
6761
6797
  async function albumsBioQueueCommand(limit) {
6762
- const response = await adminApiGet(`/api/admin/albums/bio-queue?limit=${limit}`);
6798
+ const response = await adminApiGet(`/api/v1/admin/albums/bio-queue?limit=${limit}`);
6763
6799
  return response.albums;
6764
6800
  }
6765
6801
  var init_admin_albums = __esm(() => {
@@ -6781,7 +6817,7 @@ async function migratePreviewArchiveCommand(args) {
6781
6817
  if (args.cursor) {
6782
6818
  params.set("cursor", args.cursor);
6783
6819
  }
6784
- return adminApiPost(`/api/admin/migrations/preview-archive?${params.toString()}`);
6820
+ return adminApiPost(`/api/v1/admin/migrations/preview-archive?${params.toString()}`);
6785
6821
  }
6786
6822
  var init_preview_migration = __esm(() => {
6787
6823
  init_api();
@@ -10014,6 +10050,10 @@ function addAdminCommands(program2) {
10014
10050
  const { backfillArtistEdgesCommand: backfillArtistEdgesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
10015
10051
  await runBackfillArtistEdges(options, backfillArtistEdgesCommand2);
10016
10052
  });
10053
+ backfill.command("artist-credits").description("Mint identity-true artists from MusicBrainz credits for the zero-matched residual").option("--dry-run", "Report the eligible worklist without any vendor call or write", false).option("--limit <limit>", "Max worklist rows to visit", "40").option("--json", "Print JSON", false).action(async (options) => {
10054
+ const { backfillArtistCreditsCommand: backfillArtistCreditsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
10055
+ await runBackfillArtistCredits(options, backfillArtistCreditsCommand2);
10056
+ });
10017
10057
  backfill.command("cover-masters").description("Resolve owned \u22641200\xB2 cover masters (album/artist) into R2").option("--kind <kind>", "Which entity to drain: album | artist", "album").option("--dry-run", "Report the eligible worklist without any fetch or write", false).option("--retry-none", "First re-queue a bounded batch of terminal none rows to pending, then run the pass", false).option("--limit <limit>", "Max entities to process", "50").option("--json", "Print JSON", false).action(async (options) => {
10018
10058
  const { backfillCoverMastersCommand: backfillCoverMastersCommand2 } = await Promise.resolve().then(() => (init_admin_covers(), exports_admin_covers));
10019
10059
  await runBackfillCoverMasters(options, backfillCoverMastersCommand2);
@@ -11061,6 +11101,57 @@ async function runBackfillArtistEdges(options, backfillArtistEdgesCommand2) {
11061
11101
  const verb = dryRun ? "Would write" : "Wrote";
11062
11102
  console.log(`${verb} ${edgesWritten} track_artists edge(s) over ${scanned} track(s): ${fullyMatched.length} fully matched, ${partiallyMatched.length} partially, ${zeroMatched.length} with none; ${unmatchedNames} credited name(s) left unmatched.`);
11063
11103
  }
11104
+ async function runBackfillArtistCredits(options, backfillArtistCreditsCommand2) {
11105
+ const limit = parseListLimit(options.limit) ?? 40;
11106
+ let cursor;
11107
+ let dryRun = options.dryRun;
11108
+ let scanned = 0;
11109
+ let mintedArtists = 0;
11110
+ let matchedArtists = 0;
11111
+ let adoptedArtists = 0;
11112
+ let edgesWritten = 0;
11113
+ let skippedNoIdentity = 0;
11114
+ let throttled = false;
11115
+ while (scanned < limit) {
11116
+ const remaining = limit - scanned;
11117
+ const result = await backfillArtistCreditsCommand2(remaining, options.dryRun, cursor);
11118
+ dryRun = result.dryRun;
11119
+ scanned += result.scanned;
11120
+ mintedArtists += result.mintedArtists;
11121
+ matchedArtists += result.matchedArtists;
11122
+ adoptedArtists += result.adoptedArtists;
11123
+ edgesWritten += result.edgesWritten;
11124
+ skippedNoIdentity += result.skippedNoIdentity;
11125
+ if (!options.json) {
11126
+ const verb2 = result.dryRun ? "would visit" : "visited";
11127
+ console.log(` \u2026${verb2} ${result.scanned}; minted ${result.mintedArtists}, adopted ${result.adoptedArtists}, matched ${result.matchedArtists} artist(s); wrote ${result.edgesWritten} edge(s); ${result.skippedNoIdentity} without a MusicBrainz identity`);
11128
+ }
11129
+ if (result.rateLimited) {
11130
+ throttled = true;
11131
+ break;
11132
+ }
11133
+ if (result.nextCursor === null) {
11134
+ break;
11135
+ }
11136
+ cursor = result.nextCursor;
11137
+ }
11138
+ if (options.json) {
11139
+ printJson({
11140
+ adoptedArtists,
11141
+ dryRun,
11142
+ edgesWritten,
11143
+ matchedArtists,
11144
+ mintedArtists,
11145
+ ok: true,
11146
+ rateLimited: throttled,
11147
+ scanned,
11148
+ skippedNoIdentity
11149
+ });
11150
+ return;
11151
+ }
11152
+ const verb = dryRun ? "Would visit" : "Visited";
11153
+ console.log(`${verb} ${scanned} track(s): minted ${mintedArtists}, adopted ${adoptedArtists}, matched ${matchedArtists} artist(s); wrote ${edgesWritten} track_artists edge(s); ${skippedNoIdentity} carried no MusicBrainz identity.`);
11154
+ }
11064
11155
  async function runArtistResolveQueue(options, listArtistsCommand2) {
11065
11156
  const limit = parseListLimit(options.limit);
11066
11157
  const result = await listArtistsCommand2(limit ?? 50);