fluncle 0.130.0 → 0.131.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 +96 -3
  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.130.0".trim() ? "0.130.0".trim() : "0.1.0";
564
+ currentVersion = "0.131.0".trim() ? "0.131.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -2189,7 +2189,7 @@ function parseVersion2(version) {
2189
2189
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2190
2190
  var init_version2 = __esm(() => {
2191
2191
  init_output();
2192
- currentVersion2 = "0.130.0".trim() ? "0.130.0".trim() : "0.1.0";
2192
+ currentVersion2 = "0.131.0".trim() ? "0.131.0".trim() : "0.1.0";
2193
2193
  });
2194
2194
 
2195
2195
  // ../../packages/registry/src/index.ts
@@ -2537,6 +2537,21 @@ var init_src = __esm(() => {
2537
2537
  url: `${SITE}/api/v1/search`,
2538
2538
  weights: { web: "secondary" }
2539
2539
  },
2540
+ {
2541
+ apiFormat: "application/json",
2542
+ discoveryUrl: `${SITE}/api/v1/openapi.json`,
2543
+ exposedContent: [
2544
+ "search Fluncle's archive: a coordinate (004.7.2I), an artist/label/album name, a bare word (FTS5), or a natural-language query",
2545
+ "sonic search — 'tracks that sound like <a real track>', ranked by MuQ embedding distance"
2546
+ ],
2547
+ kind: "api",
2548
+ name: "api.search.archive",
2549
+ operatorNotes: "The public read behind the ⌘K dialog (search_archive), and the primary navigation once the archive is deep. Four resolution tiers; only the fourth reaches an LLM (OpenRouter, 3s deadline) and it emits FILTERS, never rows. With no OPENROUTER_API_KEY it degrades to full-text and still answers — so it probes green unprovisioned.",
2550
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2551
+ route: "/api/v1/search/archive",
2552
+ url: `${SITE}/api/v1/search/archive?q=netsky`,
2553
+ weights: { web: "primary" }
2554
+ },
2540
2555
  {
2541
2556
  apiFormat: "application/json",
2542
2557
  discoveryUrl: `${SITE}/api/v1/openapi.json`,
@@ -2888,6 +2903,28 @@ var init_src = __esm(() => {
2888
2903
  probeConfig: { cadenceMs: 24 * 60 * MINUTE_MS, cronName: "fluncle-cluster", kind: "cron" },
2889
2904
  weights: { status: "hidden" }
2890
2905
  },
2906
+ {
2907
+ command: "fluncle admin catalogue crawl",
2908
+ exposedContent: [
2909
+ "walk the MusicBrainz release graph outward from the operator's enabled seed labels → uncertified catalogue rows"
2910
+ ],
2911
+ kind: "cron",
2912
+ name: "cron.crawl",
2913
+ operatorNotes: "every 10m, run by a rave-02 HOST systemd timer (docs/agents/hermes/crawl-timer/). METADATA ONLY — it writes a `tracks` row with no `findings` row, so it certifies nothing (no Log ID, no note, no video, no public surface) and it captures no audio. Worker-paced (the box holds no MusicBrainz budget): one bounded pass per tick over the durable `crawl_frontier`, so a crawl is a marathon the SCHEDULE finishes and a reboot mid-label costs one node, not one crawl. The boundary gate is the operator's seed-label allowlist + graph distance (hop 0-2), never genre inference; a label the walk discovers enters `undecided` and is NOT crawled until he rules on it. Zero LLM tokens. Source: docs/agents/hermes/scripts/crawl-sweep.*. See docs/catalogue-crawler.md.",
2914
+ probeConfig: { cadenceMs: 10 * MINUTE_MS, cronName: "fluncle-crawl", kind: "cron" },
2915
+ weights: { status: "hidden" }
2916
+ },
2917
+ {
2918
+ command: "fluncle admin catalogue rank",
2919
+ exposedContent: [
2920
+ "score each stale catalogue track against every embedded finding → its nearest finding + capture priority"
2921
+ ],
2922
+ kind: "cron",
2923
+ name: "cron.rank",
2924
+ operatorNotes: "every 30m, run by a rave-02 HOST systemd timer (docs/agents/hermes/rank-timer/). THE EAR's schedule, and it lands with the crawler on purpose: a timer ranking an empty table would be a /status row that means nothing, and the crawler is what creates rows. All the vector arithmetic runs in SQL inside the Worker; the sweep DRAINS (it loops while `remaining > 0` up to a tick budget), so a crawl that just landed 700 rows is ranked by the next tick. Self-healing: staleness is a fingerprint of the finding corpus, so logging or embedding a finding re-ranks the catalogue with no invalidation call. Writes DERIVED columns on catalogue rows only — it cannot certify. Zero LLM tokens. Source: docs/agents/hermes/scripts/rank-sweep.*. See docs/the-ear.md.",
2925
+ probeConfig: { cadenceMs: 30 * MINUTE_MS, cronName: "fluncle-rank", kind: "cron" },
2926
+ weights: { status: "hidden" }
2927
+ },
2891
2928
  {
2892
2929
  command: "fluncle admin tracks capture-audio --queue",
2893
2930
  exposedContent: [
@@ -5364,6 +5401,8 @@ var init_auth_lastfm = __esm(() => {
5364
5401
  // src/commands/admin-catalogue.ts
5365
5402
  var exports_admin_catalogue = {};
5366
5403
  __export(exports_admin_catalogue, {
5404
+ crawlStatusCommand: () => crawlStatusCommand,
5405
+ crawlCatalogueCommand: () => crawlCatalogueCommand,
5367
5406
  catalogueRankCommand: () => catalogueRankCommand,
5368
5407
  catalogueListCommand: () => catalogueListCommand
5369
5408
  });
@@ -5380,6 +5419,16 @@ async function catalogueListCommand(options) {
5380
5419
  }
5381
5420
  return adminApiGet(`/api/admin/catalogue?${params.toString()}`);
5382
5421
  }
5422
+ async function crawlCatalogueCommand(limit, maxHop, dryRun) {
5423
+ const params = new URLSearchParams({ limit: String(limit), maxHop: String(maxHop) });
5424
+ if (dryRun) {
5425
+ params.set("dryRun", "true");
5426
+ }
5427
+ return adminApiPost(`/api/admin/catalogue/crawl?${params.toString()}`);
5428
+ }
5429
+ async function crawlStatusCommand() {
5430
+ return adminApiGet("/api/admin/catalogue/crawl");
5431
+ }
5383
5432
  var init_admin_catalogue = __esm(() => {
5384
5433
  init_api();
5385
5434
  });
@@ -8321,7 +8370,7 @@ function addAdminCommands(program2) {
8321
8370
  const { galaxyMapWriteCommand: galaxyMapWriteCommand2 } = await Promise.resolve().then(() => (init_galaxies(), exports_galaxies));
8322
8371
  await runGalaxyMapWrite(options, galaxyMapWriteCommand2);
8323
8372
  });
8324
- const catalogue = configureCommand(admin.command("catalogue").description("The catalogue (uncertified tracks) + its ranking"));
8373
+ const catalogue = configureCommand(admin.command("catalogue").description("The catalogue (uncertified tracks): the crawler + its ranking"));
8325
8374
  catalogue.action(() => {
8326
8375
  catalogue.outputHelp();
8327
8376
  });
@@ -8333,6 +8382,14 @@ function addAdminCommands(program2) {
8333
8382
  const { catalogueListCommand: catalogueListCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
8334
8383
  await runCatalogueList(options, catalogueListCommand2);
8335
8384
  });
8385
+ catalogue.command("crawl").description("Run one bounded, resumable pass of the catalogue crawler").option("--dry-run", "Report the seed plan and write nothing at all", false).option("--limit <limit>", "Frontier nodes to expand this pass", "10").option("--max-hop <maxHop>", "Graph distance from a seed label (0-3)", "2").option("--json", "Print JSON", false).action(async (options) => {
8386
+ const { crawlCatalogueCommand: crawlCatalogueCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
8387
+ await runCrawlCatalogue(options, crawlCatalogueCommand2);
8388
+ });
8389
+ catalogue.command("status").description("The crawl frontier's state, the catalogue's size, and the seed set").option("--json", "Print JSON", false).action(async (options) => {
8390
+ const { crawlStatusCommand: crawlStatusCommand2 } = await Promise.resolve().then(() => (init_admin_catalogue(), exports_admin_catalogue));
8391
+ await runCrawlStatus(options, crawlStatusCommand2);
8392
+ });
8336
8393
  const backfill = configureCommand(admin.command("backfills").description("Backfill operator-only archives"));
8337
8394
  backfill.action(() => {
8338
8395
  backfill.outputHelp();
@@ -8630,6 +8687,41 @@ async function runBackfillLastfm(options, backfillLastfmCommand2) {
8630
8687
  process.exitCode = 1;
8631
8688
  }
8632
8689
  }
8690
+ async function runCrawlCatalogue(options, crawlCatalogueCommand2) {
8691
+ const limit = parseListLimit(options.limit);
8692
+ const maxHop = Number.parseInt(options.maxHop ?? "2", 10);
8693
+ const result = await crawlCatalogueCommand2(limit, Number.isInteger(maxHop) && maxHop >= 0 ? maxHop : 2, options.dryRun);
8694
+ if (options.json) {
8695
+ printJson(result);
8696
+ } else if (result.dryRun) {
8697
+ console.log(`Dry run: would seed ${result.seeded} enabled label(s); ${result.frontierPending} node(s) pending. Nothing written.`);
8698
+ } else {
8699
+ console.log(`Expanded ${result.expanded} node(s) at hop \u2264 ${result.maxHop}: ${result.tracksFound} track(s) found, ${result.tracksWritten} written, ${result.tracksSkipped} already held.`);
8700
+ console.log(` +${result.seeded} seed(s), +${result.nodesEnqueued} node(s) enqueued, ${result.frontierPending} pending, ${result.failed} failed.`);
8701
+ console.log(` ${result.anchorsFilled} Spotify anchor(s) filled.`);
8702
+ if (result.labelsDiscovered.length > 0) {
8703
+ console.log(` New labels to rule on: ${result.labelsDiscovered.join(", ")} (they are NOT crawled until enabled).`);
8704
+ }
8705
+ if (result.rateLimited) {
8706
+ console.log(" MusicBrainz throttled the pass. Stopped clean; the next tick resumes.");
8707
+ }
8708
+ }
8709
+ if (result.rateLimited) {
8710
+ process.exitCode = 1;
8711
+ }
8712
+ }
8713
+ async function runCrawlStatus(options, crawlStatusCommand2) {
8714
+ const result = await crawlStatusCommand2();
8715
+ if (options.json) {
8716
+ printJson(result);
8717
+ return;
8718
+ }
8719
+ const { frontier, frontierByKind } = result;
8720
+ console.log(`Catalogue: ${result.catalogueTracks} uncertified track(s); ${result.anchorsPending} awaiting a Spotify anchor.`);
8721
+ console.log(`Frontier: ${frontier.pending} pending, ${frontier.done} done, ${frontier.failed} failed, ${frontier.skipped} skipped (${frontierByKind.label} label, ${frontierByKind.artist} artist, ${frontierByKind.release} release).`);
8722
+ console.log(`Seeds (${result.seedLabels.length}): ${result.seedLabels.join(", ") || "none enabled"}.`);
8723
+ console.log(`Awaiting your ruling: ${result.labelsUndecided} label(s).`);
8724
+ }
8633
8725
  async function runBackfillDiscogs(options, backfillDiscogsCommand2) {
8634
8726
  const limit = parseListLimit(options.limit);
8635
8727
  const resolved = [];
@@ -10105,6 +10197,7 @@ var stringOptions = new Set([
10105
10197
  "--kind",
10106
10198
  "--lens",
10107
10199
  "--limit",
10200
+ "--max-hop",
10108
10201
  "--metrics",
10109
10202
  "--mime",
10110
10203
  "--model",
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.130.0"
34
+ "version": "0.131.0"
35
35
  }