fluncle 0.197.0 → 0.199.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 +188 -99
  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.197.0".trim() ? "0.197.0".trim() : "0.1.0";
564
+ currentVersion = "0.199.0".trim() ? "0.199.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -2443,7 +2443,7 @@ function parseVersion2(version) {
2443
2443
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2444
2444
  var init_version2 = __esm(() => {
2445
2445
  init_output();
2446
- currentVersion2 = "0.197.0".trim() ? "0.197.0".trim() : "0.1.0";
2446
+ currentVersion2 = "0.199.0".trim() ? "0.199.0".trim() : "0.1.0";
2447
2447
  });
2448
2448
 
2449
2449
  // ../../packages/registry/src/index.ts
@@ -2489,7 +2489,7 @@ var init_src = __esm(() => {
2489
2489
  probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2490
2490
  route: "/logbook",
2491
2491
  url: `${SITE}/logbook`,
2492
- weights: { ssh: "tertiary", web: "primary" }
2492
+ weights: { web: "primary" }
2493
2493
  },
2494
2494
  {
2495
2495
  exposedContent: ["Fluncle's own DJ mixtapes — each a checkpoint set with an F-marked Log ID"],
@@ -2680,6 +2680,56 @@ var init_src = __esm(() => {
2680
2680
  url: `${SITE}/galaxies`,
2681
2681
  weights: { ssh: "secondary", web: "secondary" }
2682
2682
  },
2683
+ {
2684
+ exposedContent: [
2685
+ "the signed-in account door: a listener's Galaxy progress, the findings and sets they saved, the tracks they submitted, and their account settings"
2686
+ ],
2687
+ kind: "web_route",
2688
+ name: "web.account",
2689
+ operatorNotes: "A WORKSTATION, not a lore page (VOICE.md §5 The Three Areas): chrome register throughout. PUBLIC and always 200 — signed out it serves the sign-in / join door, so it is honestly HTTP-probeable; every door's data is session-scoped and read on the server. `noindex` (a private per-user surface, self-canonical), so it stays out of the sitemap and llms.txt while remaining a registered, advertised surface: the nav's `Your account` CTA points here. Source: apps/web/src/routes/account.tsx + components/account.",
2690
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2691
+ route: "/account",
2692
+ url: `${SITE}/account`,
2693
+ weights: { web: "secondary" }
2694
+ },
2695
+ {
2696
+ exposedContent: [
2697
+ "ChatDnB — a chat with Fluncle over his own archive; he answers from his certified findings"
2698
+ ],
2699
+ kind: "web_route",
2700
+ name: "web.chat",
2701
+ operatorNotes: "The public face of ChatDnB. Gated to verified-email accounts (the rollout cohort); the server route re-checks the session, the verification, the origin/CSRF, and the rate dials on every turn. PENDING = registered but dark until the rollout opens to everyone.",
2702
+ pending: true,
2703
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2704
+ route: "/chat",
2705
+ url: `${SITE}/chat`,
2706
+ weights: { web: "secondary" }
2707
+ },
2708
+ {
2709
+ exposedContent: [
2710
+ "the per-listener recommendations door: a signed-in listener names the tracks they love, the Ear lines the catalogue up against those seeds, and the result lands as a playlist on Fluncle's own Spotify, refreshed weekly"
2711
+ ],
2712
+ kind: "web_route",
2713
+ name: "web.recommendations",
2714
+ operatorNotes: "E1/E2, the recommendation machine's public door. Three states off the session (anonymous pitch, unverified pointer, verified surface); the DRAFT read is the one place the live vector scan sits on a read path, rate-limited per user and degrading to empty rather than blocking the door. PENDING = registered but dark until the rollout opens.",
2715
+ pending: true,
2716
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2717
+ route: "/recommendations",
2718
+ url: `${SITE}/recommendations`,
2719
+ weights: { web: "secondary" }
2720
+ },
2721
+ {
2722
+ exposedContent: [
2723
+ "the galaxy factory — a draggable map of a finding's whole life, from the first CMD+F through the enrichment sweeps to the launch into the Galaxy"
2724
+ ],
2725
+ kind: "web_route",
2726
+ name: "web.pipeline",
2727
+ operatorNotes: "A public console page (VOICE.md §5 The Three Areas), written in the builder's-tour register. Client-only DOM/SVG/canvas — the whole map is a chunk loaded in useEffect, so it costs the archive's bundle nothing — over an SSR shell that answers 200, so it is HTTP-probeable. `noindex` (a for-the-nerds machinery view, not a search surface), so it stays out of the sitemap; the nav's Crew section links it. Source: apps/web/src/routes/pipeline.tsx + src/pipeline.",
2728
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2729
+ route: "/pipeline",
2730
+ url: `${SITE}/pipeline`,
2731
+ weights: { web: "tertiary" }
2732
+ },
2683
2733
  {
2684
2734
  exposedContent: ["the privacy policy"],
2685
2735
  kind: "web_route",
@@ -3156,6 +3206,20 @@ var init_src = __esm(() => {
3156
3206
  url: `${SITE}/oembed`,
3157
3207
  weights: { web: "tertiary" }
3158
3208
  },
3209
+ {
3210
+ apiFormat: "text/x-shellscript",
3211
+ discoveryUrl: `${SITE}/llms.txt`,
3212
+ exposedContent: [
3213
+ "the one-line CLI installer: picks the right `fluncle` binary for the machine off the latest GitHub release and drops it in place"
3214
+ ],
3215
+ kind: "discovery",
3216
+ name: "discovery.cli-installer",
3217
+ operatorNotes: "`curl -fsSL https://www.fluncle.com/cli/latest.sh | sh`, the install path the /docs/cli page, the rave terminal's Install CLI screen, the CLI's own update hint, and llms.txt (§Tools) all point at. A machine-FETCHED document rather than a page, which is why it is catalogued as `discovery` next to robots.txt and llms.txt: nothing about it is browsable, and its only reader is a shell. Always 200 (it is generated in the route, no upstream call), so the post-deploy probe asserts a non-empty body on it. Source: apps/web/src/routes/cli/latest[.]sh.ts.",
3218
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
3219
+ route: "/cli/latest.sh",
3220
+ url: `${SITE}/cli/latest.sh`,
3221
+ weights: { web: "tertiary" }
3222
+ },
3159
3223
  {
3160
3224
  apiFormat: "application/json",
3161
3225
  discoveryUrl: `${SITE}/.well-known/mcp/server-card.json`,
@@ -3239,6 +3303,15 @@ var init_src = __esm(() => {
3239
3303
  name: "cli.labels",
3240
3304
  weights: { cli: "secondary" }
3241
3305
  },
3306
+ {
3307
+ command: "fluncle galaxies",
3308
+ exposedContent: [
3309
+ "wander Fluncle's sonic galaxies, the browse-by-feel lens (bare `slug` opens one)"
3310
+ ],
3311
+ kind: "cli",
3312
+ name: "cli.galaxies",
3313
+ weights: { cli: "secondary" }
3314
+ },
3242
3315
  {
3243
3316
  command: "fluncle search",
3244
3317
  exposedContent: ["search the archive by coordinate, track, artist, label, or album"],
@@ -3274,6 +3347,27 @@ var init_src = __esm(() => {
3274
3347
  name: "cli.submit",
3275
3348
  weights: { cli: "secondary" }
3276
3349
  },
3350
+ {
3351
+ command: "fluncle login",
3352
+ exposedContent: ["link this device to your Fluncle account, so your Galaxy progress syncs"],
3353
+ kind: "cli",
3354
+ name: "cli.login",
3355
+ weights: { cli: "secondary" }
3356
+ },
3357
+ {
3358
+ command: "fluncle me",
3359
+ exposedContent: ["your account and Galaxy progress (sign in with `fluncle login`)"],
3360
+ kind: "cli",
3361
+ name: "cli.me",
3362
+ weights: { cli: "secondary" }
3363
+ },
3364
+ {
3365
+ command: "fluncle logout",
3366
+ exposedContent: ["unlink this device from your account"],
3367
+ kind: "cli",
3368
+ name: "cli.logout",
3369
+ weights: { cli: "tertiary" }
3370
+ },
3277
3371
  {
3278
3372
  command: "fluncle tracks get",
3279
3373
  exposedContent: ["look up one finding by id or Log ID (group alias `track`)"],
@@ -3304,6 +3398,14 @@ var init_src = __esm(() => {
3304
3398
  name: "cli.version",
3305
3399
  weights: { cli: "tertiary" }
3306
3400
  },
3401
+ {
3402
+ command: "fluncle status",
3403
+ exposedContent: ["how Fluncle's services are holding up (the /status board, in the terminal)"],
3404
+ kind: "cli",
3405
+ name: "cli.status",
3406
+ operatorNotes: "Reads the public status snapshot and labels each service off `liveSurfaces()` — a registry CONSUMER as well as a registry entry. Source: apps/cli/src/commands/status.ts.",
3407
+ weights: { cli: "tertiary" }
3408
+ },
3307
3409
  {
3308
3410
  command: "fluncle admin",
3309
3411
  exposedContent: [
@@ -3324,6 +3426,17 @@ var init_src = __esm(() => {
3324
3426
  url: "https://chromewebstore.google.com/detail/efkkceaofendabikblfjhoepgejfpakk",
3325
3427
  weights: { web: "secondary" }
3326
3428
  },
3429
+ {
3430
+ exposedContent: [
3431
+ "Fluncle for iOS — the archive as a native app: the Feed, the Archive, the Decks, the Radio, and the Mixtapes, each finding opening its own /log screen, plus a submit door and push when a new banger lands"
3432
+ ],
3433
+ kind: "app",
3434
+ name: "app.ios",
3435
+ operatorNotes: "Fluncle for iOS (apps/mobile). PENDING App Store review — DARK until approval, then flip `pending` + set the assigned listing URL. No probeConfig: a vendor store listing is not one of our own health-probeable endpoints (the box prober walks web/r2/dns/ssh + the crons, never an external GET), and Apple would bot-block or redirect a bare GET and read back as a false 'down' — the extension.lens ruling, which dropped its probeConfig on going live for the same reason.",
3436
+ pending: true,
3437
+ url: "https://apps.apple.com/search?term=fluncle",
3438
+ weights: { web: "secondary" }
3439
+ },
3327
3440
  {
3328
3441
  command: "fluncle admin tracks enrich --queue",
3329
3442
  exposedContent: [
@@ -6446,6 +6559,25 @@ var init_auth_tiktok = __esm(() => {
6446
6559
  init_api();
6447
6560
  });
6448
6561
 
6562
+ // src/commands/auth-revoke-grants.ts
6563
+ var exports_auth_revoke_grants = {};
6564
+ __export(exports_auth_revoke_grants, {
6565
+ authRevokeGrantsCommand: () => authRevokeGrantsCommand
6566
+ });
6567
+ async function authRevokeGrantsCommand(options = {}) {
6568
+ const response = await adminApiPost("/api/v1/admin/auth/revoke-grants");
6569
+ if (options.json) {
6570
+ console.log(JSON.stringify(response, null, 2));
6571
+ return;
6572
+ }
6573
+ console.log(`Every admin browser session is out. Grant epoch is now ${response.epoch}.
6574
+
6575
+ Sign back in at /admin/login. Your CLI and the agent box are unaffected.`);
6576
+ }
6577
+ var init_auth_revoke_grants = __esm(() => {
6578
+ init_api();
6579
+ });
6580
+
6449
6581
  // src/commands/auth-lastfm.ts
6450
6582
  var exports_auth_lastfm = {};
6451
6583
  __export(exports_auth_lastfm, {
@@ -9804,6 +9936,10 @@ function addAdminCommands(program2) {
9804
9936
  const { authTikTokCommand: authTikTokCommand2 } = await Promise.resolve().then(() => (init_auth_tiktok(), exports_auth_tiktok));
9805
9937
  await authTikTokCommand2();
9806
9938
  });
9939
+ auth.command("revoke-grants").description("Sign every admin browser session out (bump the grant epoch)").option("--json", "Output raw JSON").action(async (options) => {
9940
+ const { authRevokeGrantsCommand: authRevokeGrantsCommand2 } = await Promise.resolve().then(() => (init_auth_revoke_grants(), exports_auth_revoke_grants));
9941
+ await authRevokeGrantsCommand2(options);
9942
+ });
9807
9943
  auth.command("lastfm").description("Authorize Last.fm access (love-on-add). Run once for the URL, then again with --token").option("--token <token>", "The approved request token, to mint the session key").action(async (options) => {
9808
9944
  const { authLastfmCommand: authLastfmCommand2 } = await Promise.resolve().then(() => (init_auth_lastfm(), exports_auth_lastfm));
9809
9945
  await authLastfmCommand2(options);
@@ -12315,10 +12451,14 @@ async function runAdminQueue(options, queueCommand2) {
12315
12451
  console.log(trackRows2(tracks).join(`
12316
12452
  `));
12317
12453
  }
12318
- async function runAdminContextQueue(options, contextQueueCommand2) {
12454
+ async function runQueueView({
12455
+ emptyMessage,
12456
+ fetch: fetch2,
12457
+ heading,
12458
+ options
12459
+ }) {
12319
12460
  const limit = parseListLimit(options.limit);
12320
- const retryEmpty = options.retryEmpty === true;
12321
- const tracks = await contextQueueCommand2(limit, retryEmpty);
12461
+ const tracks = await fetch2(limit);
12322
12462
  if (options.json) {
12323
12463
  printJson({
12324
12464
  ok: true,
@@ -12327,115 +12467,64 @@ async function runAdminContextQueue(options, contextQueueCommand2) {
12327
12467
  return;
12328
12468
  }
12329
12469
  if (tracks.length === 0) {
12330
- console.log("Every finding has its field notes. Nothing waiting on context.");
12470
+ console.log(emptyMessage);
12331
12471
  return;
12332
12472
  }
12333
12473
  const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
12334
12474
  const noun = tracks.length === 1 ? "finding" : "findings";
12335
- const scope = retryEmpty ? " (incl. empty retries)" : "";
12336
- console.log(`${tracks.length} ${noun} missing field notes${scope}, oldest first:`);
12475
+ console.log(heading(tracks.length, noun));
12337
12476
  console.log(trackRows2(tracks).join(`
12338
12477
  `));
12339
12478
  }
12479
+ async function runAdminContextQueue(options, contextQueueCommand2) {
12480
+ const retryEmpty = options.retryEmpty === true;
12481
+ const scope = retryEmpty ? " (incl. empty retries)" : "";
12482
+ await runQueueView({
12483
+ emptyMessage: "Every finding has its field notes. Nothing waiting on context.",
12484
+ fetch: (limit) => contextQueueCommand2(limit, retryEmpty),
12485
+ heading: (count, noun) => `${count} ${noun} missing field notes${scope}, oldest first:`,
12486
+ options
12487
+ });
12488
+ }
12340
12489
  async function runAdminObserveQueue(options, observeQueueCommand2) {
12341
- const limit = parseListLimit(options.limit);
12342
- const tracks = await observeQueueCommand2(limit);
12343
- if (options.json) {
12344
- printJson({
12345
- ok: true,
12346
- tracks
12347
- });
12348
- return;
12349
- }
12350
- if (tracks.length === 0) {
12351
- console.log("Every finding with notes has its observation. Nothing waiting on a voice.");
12352
- return;
12353
- }
12354
- const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
12355
- const noun = tracks.length === 1 ? "finding" : "findings";
12356
- console.log(`${tracks.length} ${noun} awaiting an observation, oldest first:`);
12357
- console.log(trackRows2(tracks).join(`
12358
- `));
12490
+ await runQueueView({
12491
+ emptyMessage: "Every finding with notes has its observation. Nothing waiting on a voice.",
12492
+ fetch: observeQueueCommand2,
12493
+ heading: (count, noun) => `${count} ${noun} awaiting an observation, oldest first:`,
12494
+ options
12495
+ });
12359
12496
  }
12360
12497
  async function runAdminNoteQueue(options, noteQueueCommand2) {
12361
- const limit = parseListLimit(options.limit);
12362
- const tracks = await noteQueueCommand2(limit);
12363
- if (options.json) {
12364
- printJson({
12365
- ok: true,
12366
- tracks
12367
- });
12368
- return;
12369
- }
12370
- if (tracks.length === 0) {
12371
- console.log("Every context'd finding has a note. Nothing waiting on the uncle's words.");
12372
- return;
12373
- }
12374
- const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
12375
- const noun = tracks.length === 1 ? "finding" : "findings";
12376
- console.log(`${tracks.length} ${noun} awaiting a note, oldest first:`);
12377
- console.log(trackRows2(tracks).join(`
12378
- `));
12498
+ await runQueueView({
12499
+ emptyMessage: "Every context'd finding has a note. Nothing waiting on the uncle's words.",
12500
+ fetch: noteQueueCommand2,
12501
+ heading: (count, noun) => `${count} ${noun} awaiting a note, oldest first:`,
12502
+ options
12503
+ });
12379
12504
  }
12380
12505
  async function runAdminEnrichQueue(options, enrichQueueCommand2) {
12381
- const limit = parseListLimit(options.limit);
12382
- const tracks = await enrichQueueCommand2(limit);
12383
- if (options.json) {
12384
- printJson({
12385
- ok: true,
12386
- tracks
12387
- });
12388
- return;
12389
- }
12390
- if (tracks.length === 0) {
12391
- console.log("Nothing awaiting enrichment. Every finding is enriched.");
12392
- return;
12393
- }
12394
- const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
12395
- const noun = tracks.length === 1 ? "finding" : "findings";
12396
- console.log(`${tracks.length} ${noun} needing (re-)enrichment, oldest first:`);
12397
- console.log(trackRows2(tracks).join(`
12398
- `));
12506
+ await runQueueView({
12507
+ emptyMessage: "Nothing awaiting enrichment. Every finding is enriched.",
12508
+ fetch: enrichQueueCommand2,
12509
+ heading: (count, noun) => `${count} ${noun} needing (re-)enrichment, oldest first:`,
12510
+ options
12511
+ });
12399
12512
  }
12400
12513
  async function runAdminEmbedQueue(options, embedQueueCommand2) {
12401
- const limit = parseListLimit(options.limit);
12402
- const tracks = await embedQueueCommand2(limit);
12403
- if (options.json) {
12404
- printJson({
12405
- ok: true,
12406
- tracks
12407
- });
12408
- return;
12409
- }
12410
- if (tracks.length === 0) {
12411
- console.log("Nothing awaiting an embedding. Every finding is embedded.");
12412
- return;
12413
- }
12414
- const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
12415
- const noun = tracks.length === 1 ? "finding" : "findings";
12416
- console.log(`${tracks.length} ${noun} needing an audio embedding, oldest first:`);
12417
- console.log(trackRows2(tracks).join(`
12418
- `));
12514
+ await runQueueView({
12515
+ emptyMessage: "Nothing awaiting an embedding. Every finding is embedded.",
12516
+ fetch: embedQueueCommand2,
12517
+ heading: (count, noun) => `${count} ${noun} needing an audio embedding, oldest first:`,
12518
+ options
12519
+ });
12419
12520
  }
12420
12521
  async function runAdminCaptureQueue(options, captureQueueCommand2) {
12421
- const limit = parseListLimit(options.limit);
12422
- const tracks = await captureQueueCommand2(limit);
12423
- if (options.json) {
12424
- printJson({
12425
- ok: true,
12426
- tracks
12427
- });
12428
- return;
12429
- }
12430
- if (tracks.length === 0) {
12431
- console.log("Nothing awaiting a capture. Every finding has its full song.");
12432
- return;
12433
- }
12434
- const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
12435
- const noun = tracks.length === 1 ? "finding" : "findings";
12436
- console.log(`${tracks.length} ${noun} needing a full-song capture, newest first:`);
12437
- console.log(trackRows2(tracks).join(`
12438
- `));
12522
+ await runQueueView({
12523
+ emptyMessage: "Nothing awaiting a capture. Every finding has its full song.",
12524
+ fetch: captureQueueCommand2,
12525
+ heading: (count, noun) => `${count} ${noun} needing a full-song capture, newest first:`,
12526
+ options
12527
+ });
12439
12528
  }
12440
12529
  var TRACK_WORK_KINDS = new Set(["analyze", "capture", "embed"]);
12441
12530
  var TRACK_WORK_SCOPES = new Set(["all", "catalogue", "findings"]);
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.197.0"
34
+ "version": "0.199.0"
35
35
  }