fluncle 0.112.0 → 0.114.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 +173 -8
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -557,7 +557,7 @@ function parseVersion(version) {
557
557
  var currentVersion;
558
558
  var init_version = __esm(() => {
559
559
  init_output();
560
- currentVersion = "0.112.0".trim() ? "0.112.0".trim() : "0.1.0";
560
+ currentVersion = "0.114.0".trim() ? "0.114.0".trim() : "0.1.0";
561
561
  });
562
562
 
563
563
  // src/update-notifier.ts
@@ -2209,7 +2209,7 @@ function parseVersion2(version) {
2209
2209
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2210
2210
  var init_version2 = __esm(() => {
2211
2211
  init_output();
2212
- currentVersion2 = "0.112.0".trim() ? "0.112.0".trim() : "0.1.0";
2212
+ currentVersion2 = "0.114.0".trim() ? "0.114.0".trim() : "0.1.0";
2213
2213
  });
2214
2214
 
2215
2215
  // ../../packages/registry/src/index.ts
@@ -2244,6 +2244,18 @@ var init_src = __esm(() => {
2244
2244
  url: `${SITE}/log`,
2245
2245
  weights: { ssh: "secondary", web: "primary" }
2246
2246
  },
2247
+ {
2248
+ exposedContent: [
2249
+ "Fluncle's Logbook — the voyage as a first-person travelogue, one entry per sector-day",
2250
+ "/logbook/:sector — one day written up, the findings inlined as photos"
2251
+ ],
2252
+ kind: "web_route",
2253
+ name: "web.logbook",
2254
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2255
+ route: "/logbook",
2256
+ url: `${SITE}/logbook`,
2257
+ weights: { ssh: "tertiary", web: "primary" }
2258
+ },
2247
2259
  {
2248
2260
  exposedContent: ["Fluncle's own DJ mixtapes — each a checkpoint set with an F-marked Log ID"],
2249
2261
  kind: "web_route",
@@ -2253,6 +2265,18 @@ var init_src = __esm(() => {
2253
2265
  url: `${SITE}/mixtapes`,
2254
2266
  weights: { ssh: "secondary", web: "primary" }
2255
2267
  },
2268
+ {
2269
+ exposedContent: [
2270
+ "the mixability set-builder — chain a set from Fluncle's findings that mixes clean, share it as a link"
2271
+ ],
2272
+ kind: "web_route",
2273
+ name: "web.mix",
2274
+ operatorNotes: "ADMIN-GATED at launch (RFC mixability-engine, Decision 1): the full /mix set-builder ships behind admin auth until the archive clears the ~250-finding floor. Registered here as `pending` (dark to every public consumer — the dev-row, llms.txt, the sitemap, /status) so going public is one flip: remove `pending` + set a web weight + announce. The `list_mixable_tracks` API op already ships at its final public tier, so the flip needs no rebuild. No probeConfig — the admin gate 302s a bare GET to /admin/login, which would read as a false 'down'.",
2275
+ pending: true,
2276
+ route: "/mix",
2277
+ url: `${SITE}/mix`,
2278
+ weights: { web: "secondary" }
2279
+ },
2256
2280
  {
2257
2281
  exposedContent: [
2258
2282
  "the feed-first Stories surface — full-bleed findings",
@@ -2851,6 +2875,22 @@ var init_src = __esm(() => {
2851
2875
  probeConfig: { cadenceMs: 10 * MINUTE_MS, cronName: "fluncle-note", kind: "cron" },
2852
2876
  weights: { status: "hidden" }
2853
2877
  },
2878
+ {
2879
+ command: "fluncle admin logbook gaps",
2880
+ exposedContent: [
2881
+ "author the previous day's Logbook travelogue entry, fill-empty-only (hybrid: one claude -p call)"
2882
+ ],
2883
+ kind: "cron",
2884
+ name: "cron.logbook",
2885
+ operatorNotes: "00:40 Amsterdam daily. Hybrid --no-agent; one claude -p writes the day up. Never clobbers an operator entry; the self-healing gap window backfills history oldest-first. Source: docs/agents/hermes/scripts/logbook-sweep.{sh,ts}.",
2886
+ probeConfig: {
2887
+ cadenceMs: 24 * 60 * MINUTE_MS,
2888
+ cronName: "fluncle-logbook",
2889
+ kind: "cron",
2890
+ schedule: { time: "00:40", tz: "Europe/Amsterdam" }
2891
+ },
2892
+ weights: { status: "hidden" }
2893
+ },
2854
2894
  {
2855
2895
  command: "fluncle admin tracks observe --queue",
2856
2896
  exposedContent: [
@@ -3458,6 +3498,7 @@ __export(exports_admin_tracks, {
3458
3498
  queueCommand: () => queueCommand,
3459
3499
  observeQueueCommand: () => observeQueueCommand,
3460
3500
  noteQueueCommand: () => noteQueueCommand,
3501
+ mixableOrderCommand: () => mixableOrderCommand,
3461
3502
  listCommand: () => listCommand,
3462
3503
  enrichQueueCommand: () => enrichQueueCommand,
3463
3504
  embedQueueCommand: () => embedQueueCommand,
@@ -3637,6 +3678,13 @@ async function vehiclesCommand(limit) {
3637
3678
  vehicle: track.videoVehicle
3638
3679
  }));
3639
3680
  }
3681
+ async function mixableOrderCommand(logIds, seed) {
3682
+ const params = new URLSearchParams({ ids: logIds.join(",") });
3683
+ if (seed) {
3684
+ params.set("seed", seed);
3685
+ }
3686
+ return adminApiGet(`/api/admin/tracks/mixable-order?${params.toString()}`);
3687
+ }
3640
3688
  var pageSize3 = 48;
3641
3689
  var init_admin_tracks = __esm(() => {
3642
3690
  init_api();
@@ -4821,6 +4869,54 @@ var init_newsletter = __esm(() => {
4821
4869
  init_output();
4822
4870
  });
4823
4871
 
4872
+ // src/commands/admin-logbook.ts
4873
+ var exports_admin_logbook = {};
4874
+ __export(exports_admin_logbook, {
4875
+ logbookUpdateCommand: () => logbookUpdateCommand,
4876
+ logbookGapsCommand: () => logbookGapsCommand,
4877
+ logbookCreateCommand: () => logbookCreateCommand
4878
+ });
4879
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
4880
+ async function logbookGapsCommand(limit) {
4881
+ const query = typeof limit === "number" ? `?limit=${encodeURIComponent(String(limit))}` : "";
4882
+ const response = await adminApiGet(`/api/admin/logbook/gaps${query}`);
4883
+ return response.gaps;
4884
+ }
4885
+ function resolveBody(options) {
4886
+ if (options.body !== undefined) {
4887
+ return options.body;
4888
+ }
4889
+ if (options.bodyFile !== undefined) {
4890
+ if (!existsSync5(options.bodyFile)) {
4891
+ throw new CliError2("file_not_found", `Body file not found: ${options.bodyFile}`);
4892
+ }
4893
+ return readFileSync5(options.bodyFile, "utf-8");
4894
+ }
4895
+ throw new CliError2("missing_body", "An entry needs a body via --body <text> or --body-file <entry.md>");
4896
+ }
4897
+ function requireTitle(options) {
4898
+ if (options.title === undefined || !options.title.trim()) {
4899
+ throw new CliError2("missing_title", "An entry needs a title via --title <text>");
4900
+ }
4901
+ return options.title;
4902
+ }
4903
+ async function logbookCreateCommand(sector, options) {
4904
+ return adminApiPost(`/api/admin/logbook/${encodeURIComponent(sector)}`, {
4905
+ body: resolveBody(options),
4906
+ title: requireTitle(options)
4907
+ });
4908
+ }
4909
+ async function logbookUpdateCommand(sector, options) {
4910
+ return adminApiPatch(`/api/admin/logbook/${encodeURIComponent(sector)}`, {
4911
+ body: resolveBody(options),
4912
+ title: requireTitle(options)
4913
+ });
4914
+ }
4915
+ var init_admin_logbook = __esm(() => {
4916
+ init_api();
4917
+ init_output();
4918
+ });
4919
+
4824
4920
  // src/commands/add.ts
4825
4921
  async function addCommand2(spotifyUrl, options) {
4826
4922
  const result = await adminApiPost("/api/admin/tracks", {
@@ -5485,7 +5581,7 @@ var init_format2 = __esm(() => {
5485
5581
  });
5486
5582
 
5487
5583
  // src/cli.ts
5488
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
5584
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
5489
5585
  import path2 from "path";
5490
5586
 
5491
5587
  // ../../node_modules/commander/lib/error.js
@@ -7801,6 +7897,10 @@ function addAdminCommands(program2) {
7801
7897
  const { trackGetAdminCommand: trackGetAdminCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
7802
7898
  await runTrackGetAdmin(idOrLogId, options, trackGetAdminCommand2);
7803
7899
  });
7900
+ adminTrack.command("mixable-order").description("Order findings into a smooth proposed mix (2\u201364 Log IDs)").argument("[logIds...]", "The findings to order, by Log ID").option("--seed <logId>", "Pin the first stop").option("--json", "Print JSON", false).allowExcessArguments().action(async (logIds, options) => {
7901
+ const { mixableOrderCommand: mixableOrderCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
7902
+ await runMixableOrder(logIds, options, mixableOrderCommand2);
7903
+ });
7804
7904
  adminTrack.command("update").description("Certify a track into the archive").argument("[trackId]").option("--analyzed-at <iso>", "Analysis-write ISO timestamp (BPM/key provenance)").option("--analyzed-from <class>", "Audio class BPM/key were analyzed from: full or preview").option("--bpm <number>", "Track BPM").option("--bpm-confidence <number>", "Analyzer confidence in the BPM (0..1)").option("--bpm-source <source>", "Where the BPM came from (analyzer bpmSource)").option("--embedding <json>", "MuQ audio embedding as a JSON array of 1024 floats").option("--embedding-file <file>", "Read the MuQ embedding JSON array from a file").option("--features <json>", "Audio feature JSON").option("--json", "Print JSON", false).option("--key <key>", "Musical key").option("--key-confidence <number>", "Analyzer confidence in the key (0..1)").option("--key-source <source>", "Where the key came from (analyzer keySource)").option("--note <text>", "Operator note").option("--status <status>", "Enrichment status").option("--video-url <url>", "Rendered video URL").allowExcessArguments().action(async (trackId, options) => {
7805
7905
  const { trackUpdateCommand: trackUpdateCommand3 } = await Promise.resolve().then(() => (init_track(), exports_track));
7806
7906
  await runTrackUpdate(trackId, options, trackUpdateCommand3);
@@ -7969,6 +8069,22 @@ function addAdminCommands(program2) {
7969
8069
  const { newsletterDeleteCommand: newsletterDeleteCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
7970
8070
  await runNewsletterDelete(id, options, newsletterDeleteCommand2);
7971
8071
  });
8072
+ const adminLogbook = configureCommand(admin.command("logbook").description("Fluncle's Logbook (travelogue) commands"));
8073
+ adminLogbook.action(() => {
8074
+ adminLogbook.outputHelp();
8075
+ });
8076
+ adminLogbook.command("gaps").description("List sector-days with findings but no logbook entry (the sweep's worklist)").option("--limit <limit>", "Max gaps to return").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
8077
+ const { logbookGapsCommand: logbookGapsCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
8078
+ await runLogbookGaps(options, logbookGapsCommand2);
8079
+ });
8080
+ adminLogbook.command("create").description("Author a sector-day's entry (fills an empty sector only)").argument("[sector]").option("--title <text>", "The entry title").option("--body <text>", "The entry body (markdown; [[logId]] figure tokens)").option("--body-file <file>", "Read the body from a file (the sweep's path)").option("--json", "Print JSON", false).allowExcessArguments().action(async (sector, options) => {
8081
+ const { logbookCreateCommand: logbookCreateCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
8082
+ await runLogbookWrite(sector, options, logbookCreateCommand2);
8083
+ });
8084
+ adminLogbook.command("update").description("Create or overwrite a sector-day's entry. OPERATOR only").argument("[sector]").option("--title <text>", "The entry title").option("--body <text>", "The entry body (markdown; [[logId]] figure tokens)").option("--body-file <file>", "Read the body from a file").option("--json", "Print JSON", false).allowExcessArguments().action(async (sector, options) => {
8085
+ const { logbookUpdateCommand: logbookUpdateCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
8086
+ await runLogbookWrite(sector, options, logbookUpdateCommand2);
8087
+ });
7972
8088
  const submissions = configureCommand(admin.command("submissions").description("Review listener submissions"));
7973
8089
  submissions.option("--json", "Print JSON", false).action(async (options) => {
7974
8090
  const { listSubmissionsCommand: listSubmissionsCommand2 } = await Promise.resolve().then(() => (init_submissions(), exports_submissions));
@@ -7999,7 +8115,7 @@ function addAdminCommands(program2) {
7999
8115
  if (!submissionId) {
8000
8116
  throw new Error("Missing submission id for: triage");
8001
8117
  }
8002
- const verdict = options.verdictFile ? readFileSync5(options.verdictFile, "utf8") : options.verdict;
8118
+ const verdict = options.verdictFile ? readFileSync6(options.verdictFile, "utf8") : options.verdict;
8003
8119
  if (!verdict || !verdict.trim()) {
8004
8120
  throw new Error("Usage: fluncle admin submissions triage <submissionId> (--verdict <text> | --verdict-file <file>) [--json]");
8005
8121
  }
@@ -8084,7 +8200,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
8084
8200
  console.log(` mime: ${result.mime}`);
8085
8201
  }
8086
8202
  async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
8087
- const script = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
8203
+ const script = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
8088
8204
  if (!idOrLogId || !script || !script.trim()) {
8089
8205
  throw new Error("Usage: fluncle admin tracks observe <track_id|log_id> (--script <text> | --script-file <file>) [--voice-id <id>] [--duration-ms <ms>] [--context-note <text>] [--json]");
8090
8206
  }
@@ -8140,7 +8256,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
8140
8256
  }
8141
8257
  }
8142
8258
  async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
8143
- const note = options.scriptFile ? readFileSync5(options.scriptFile, "utf8") : options.script;
8259
+ const note = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
8144
8260
  if (!idOrLogId || !note || !note.trim()) {
8145
8261
  throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
8146
8262
  }
@@ -8453,7 +8569,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
8453
8569
  return;
8454
8570
  }
8455
8571
  const candidate = path2.join(dir, name);
8456
- return existsSync5(candidate) ? candidate : undefined;
8572
+ return existsSync6(candidate) ? candidate : undefined;
8457
8573
  };
8458
8574
  const resolveFile = (explicit, name) => {
8459
8575
  if (explicit) {
@@ -8592,6 +8708,24 @@ async function runTrackGet(idOrLogId, options, trackGetCommand2) {
8592
8708
  console.log(`Log: ${t.logPageUrl}`);
8593
8709
  }
8594
8710
  }
8711
+ async function runMixableOrder(logIds, options, mixableOrderCommand2) {
8712
+ if (logIds.length < 2) {
8713
+ throw new Error("Give 2 or more Log IDs. Usage: fluncle admin tracks mixable-order <logId...> [--seed <logId>] [--json]");
8714
+ }
8715
+ const result = await mixableOrderCommand2(logIds, options.seed);
8716
+ if (options.json) {
8717
+ printJson(result);
8718
+ return;
8719
+ }
8720
+ console.log(`Proposed mix \u2014 a smooth chain (${result.algorithm}), not an energy-shaped set. Copy into Rekordbox as advisory.
8721
+ `);
8722
+ result.order.forEach((stop, index) => {
8723
+ const into = index === 0 ? "start" : stop.flagged ? "sparse join" : stop.transitionReason?.relationship.replace(/_/g, " ") ?? "\u2014";
8724
+ const meta = [stop.key, stop.bpm ? `${Math.round(stop.bpm)} bpm` : undefined].filter(Boolean).join(" \xB7 ");
8725
+ console.log(`${String(index + 1).padStart(2)}. ${stop.logId} ${stop.artists.join(", ")} \u2014 ${stop.title}`);
8726
+ console.log(` ${[into, meta].filter(Boolean).join(" \xB7 ")}`);
8727
+ });
8728
+ }
8595
8729
  async function runTrackGetAdmin(idOrLogId, options, trackGetAdminCommand2) {
8596
8730
  if (!idOrLogId) {
8597
8731
  throw new Error("Missing id. Usage: fluncle admin tracks get <track_id|log_id> [--json]");
@@ -8648,7 +8782,7 @@ async function runTrackUpdate(trackId, options, trackUpdateCommand3) {
8648
8782
  if (options.analyzedFrom !== undefined && options.analyzedFrom !== "full" && options.analyzedFrom !== "preview") {
8649
8783
  throw new Error(`Invalid --analyzed-from: ${options.analyzedFrom} (expected full or preview)`);
8650
8784
  }
8651
- const embeddingRaw = options.embeddingFile ? readFileSync5(options.embeddingFile, "utf8") : options.embedding;
8785
+ const embeddingRaw = options.embeddingFile ? readFileSync6(options.embeddingFile, "utf8") : options.embedding;
8652
8786
  const embedding = parseEmbeddingArg(embeddingRaw);
8653
8787
  const result = await trackUpdateCommand3(trackId, {
8654
8788
  analyzedAt: options.analyzedAt,
@@ -8911,6 +9045,37 @@ async function runNewsletterList(options, newsletterListCommand2) {
8911
9045
  console.log(`${label} ${edition.status} ${edition.id} ${subject}`);
8912
9046
  }
8913
9047
  }
9048
+ async function runLogbookGaps(options, logbookGapsCommand2) {
9049
+ const limit = options.limit === undefined ? undefined : Number.parseInt(options.limit, 10);
9050
+ const gaps = await logbookGapsCommand2(limit !== undefined && Number.isFinite(limit) ? limit : undefined);
9051
+ if (options.json) {
9052
+ printJson({ gaps, ok: true });
9053
+ return;
9054
+ }
9055
+ if (gaps.length === 0) {
9056
+ console.log("No gaps \u2014 every past sector-day with findings has an entry.");
9057
+ return;
9058
+ }
9059
+ for (const gap of gaps) {
9060
+ console.log(`sector ${gap.sector} ${gap.date.slice(0, 10)} ${gap.findings.length} findings`);
9061
+ }
9062
+ }
9063
+ async function runLogbookWrite(sector, options, writeCommand) {
9064
+ if (!sector) {
9065
+ throw new Error("Missing sector for the logbook entry");
9066
+ }
9067
+ const result = await writeCommand(sector, {
9068
+ body: options.body,
9069
+ bodyFile: options.bodyFile,
9070
+ title: options.title
9071
+ });
9072
+ if (options.json) {
9073
+ printJson(result);
9074
+ return;
9075
+ }
9076
+ const skipped = "skipped" in result && result.skipped;
9077
+ console.log(skipped ? `sector ${result.entry.sector}: an entry already stands \u2014 no-op (${result.entry.generatedBy})` : `sector ${result.entry.sector}: ${result.entry.title}`);
9078
+ }
8914
9079
  async function runAdd(spotifyUrl, options, addCommand3) {
8915
9080
  if (!spotifyUrl) {
8916
9081
  throw new Error("Missing Spotify track URL");
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.112.0"
34
+ "version": "0.114.0"
35
35
  }