fluncle 0.135.0 → 0.137.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 +447 -20
  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.135.0".trim() ? "0.135.0".trim() : "0.1.0";
564
+ currentVersion = "0.137.0".trim() ? "0.137.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.135.0".trim() ? "0.135.0".trim() : "0.1.0";
2192
+ currentVersion2 = "0.137.0".trim() ? "0.137.0".trim() : "0.1.0";
2193
2193
  });
2194
2194
 
2195
2195
  // ../../packages/registry/src/index.ts
@@ -2247,11 +2247,11 @@ var init_src = __esm(() => {
2247
2247
  },
2248
2248
  {
2249
2249
  exposedContent: [
2250
- "the mixability set-builderchain a set from Fluncle's findings that mixes clean, share it as a link"
2250
+ "the free mixing tool name artists you like, and Fluncle ranks what mixes in clean next from the whole archive; chain a set and share it as a link"
2251
2251
  ],
2252
2252
  kind: "web_route",
2253
2253
  name: "web.mix",
2254
- 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'.",
2254
+ operatorNotes: "PUBLIC-CAPABLE, gated by a SELF-LIFTING DEPTH MEASUREMENT (not admin auth). The old ~250-finding / admin gate is gone: `/mix` now measures the live archive on every load (`getMixChainDepth` — can the median track reach a full set + rail by a named harmonic move?) and opens to the world on its own the day the catalogue lands enough keyed depth. Until then a stranger is redirected home and the operator still gets in to dogfood. This entry stays `pending` (dark to the dev-row, llms.txt, the sitemap, /status) ONLY so we do not advertise a URL that still redirects the flip is: confirm the depth gate has opened in prod, remove `pending`, announce. The web weight is pre-set, so the flip needs no other change. No probeConfig — a closed gate 302s a bare GET, which would read as a false 'down'.",
2255
2255
  pending: true,
2256
2256
  route: "/mix",
2257
2257
  url: `${SITE}/mix`,
@@ -3443,6 +3443,9 @@ async function trackObserveCommand(idOrLogId, options) {
3443
3443
  if (options.durationTargetSec !== undefined) {
3444
3444
  body.durationTargetSec = options.durationTargetSec;
3445
3445
  }
3446
+ if (typeof options.promptVersion === "number") {
3447
+ body.promptVersion = options.promptVersion;
3448
+ }
3446
3449
  if (options.contextNote !== undefined) {
3447
3450
  body.contextNote = options.contextNote;
3448
3451
  }
@@ -3466,6 +3469,9 @@ async function trackNoteCommand(idOrLogId, options) {
3466
3469
  if (options.dryRun) {
3467
3470
  body.dryRun = true;
3468
3471
  }
3472
+ if (typeof options.promptVersion === "number") {
3473
+ body.promptVersion = options.promptVersion;
3474
+ }
3469
3475
  return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
3470
3476
  }
3471
3477
  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;
@@ -5018,6 +5024,9 @@ function buildBody2(options, { requireContent }) {
5018
5024
  } else if (requireContent) {
5019
5025
  throw new CliError2("missing_content", "A draft needs the structured content payload via --content-file <edition.json>");
5020
5026
  }
5027
+ if (requireContent && typeof options.promptVersion === "number") {
5028
+ body.promptVersion = options.promptVersion;
5029
+ }
5021
5030
  if (options.subject !== undefined) {
5022
5031
  body.subject = options.subject;
5023
5032
  }
@@ -5095,6 +5104,7 @@ function requireTitle(options) {
5095
5104
  async function logbookCreateCommand(sector, options) {
5096
5105
  return adminApiPost(`/api/admin/logbook/${encodeURIComponent(sector)}`, {
5097
5106
  body: resolveBody(options),
5107
+ ...typeof options.promptVersion === "number" ? { promptVersion: options.promptVersion } : {},
5098
5108
  title: requireTitle(options)
5099
5109
  });
5100
5110
  }
@@ -5109,6 +5119,193 @@ var init_admin_logbook = __esm(() => {
5109
5119
  init_output();
5110
5120
  });
5111
5121
 
5122
+ // src/commands/admin-prompts.ts
5123
+ var exports_admin_prompts = {};
5124
+ __export(exports_admin_prompts, {
5125
+ renderDiff: () => renderDiff,
5126
+ promptsListCommand: () => promptsListCommand,
5127
+ promptUpdateCommand: () => promptUpdateCommand,
5128
+ promptRows: () => promptRows,
5129
+ promptRollbackCommand: () => promptRollbackCommand,
5130
+ promptResetCommand: () => promptResetCommand,
5131
+ promptGetCommand: () => promptGetCommand,
5132
+ promptDiffCommand: () => promptDiffCommand,
5133
+ promptDetailCommand: () => promptDetailCommand,
5134
+ parseVersion: () => parseVersion3,
5135
+ parseAgainst: () => parseAgainst,
5136
+ historyRows: () => historyRows,
5137
+ diffLines: () => diffLines,
5138
+ bodyLines: () => bodyLines
5139
+ });
5140
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
5141
+ async function promptsListCommand() {
5142
+ const response = await adminApiGet("/api/admin/prompts");
5143
+ return response.prompts;
5144
+ }
5145
+ async function promptGetCommand(slug) {
5146
+ return adminApiGet(`/api/admin/prompts/${encodeURIComponent(slug)}`);
5147
+ }
5148
+ async function promptDetailCommand(slug) {
5149
+ const prompts = await promptsListCommand();
5150
+ const detail = prompts.find((prompt) => prompt.slug === slug);
5151
+ if (!detail) {
5152
+ throw new CliError2("unknown_prompt", `No prompt goes by "${slug}". The registered ones: ${prompts.map((prompt) => prompt.slug).join(", ")}`);
5153
+ }
5154
+ return detail;
5155
+ }
5156
+ function resolveBody2(options) {
5157
+ if (options.bodyFile === undefined) {
5158
+ throw new CliError2("missing_body", "An edit needs a body via --body-file <prompt.txt>. Start from `fluncle admin prompts get <slug> --json | jq -r .body`.");
5159
+ }
5160
+ if (!existsSync6(options.bodyFile)) {
5161
+ throw new CliError2("file_not_found", `Body file not found: ${options.bodyFile}`);
5162
+ }
5163
+ const body = readFileSync6(options.bodyFile, "utf-8");
5164
+ if (body.trim().length === 0) {
5165
+ throw new CliError2("empty_body", "A prompt body cannot be empty. Nothing was appended.");
5166
+ }
5167
+ return body;
5168
+ }
5169
+ async function promptUpdateCommand(slug, options) {
5170
+ const body = resolveBody2(options);
5171
+ const note = options.note?.trim();
5172
+ const response = await adminApiPost(`/api/admin/prompts/${encodeURIComponent(slug)}`, note ? { body, note } : { body });
5173
+ return { slug, version: response.version };
5174
+ }
5175
+ async function promptRollbackCommand(slug, version) {
5176
+ const detail = await promptDetailCommand(slug);
5177
+ const target = detail.versions.find((candidate) => candidate.version === version);
5178
+ if (!target) {
5179
+ const known = detail.versions.map((candidate) => `v${candidate.version}`).join(", ");
5180
+ throw new CliError2("unknown_version", known.length > 0 ? `${slug} has no v${version}. On file: ${known}. For the repo's baked default, run \`fluncle admin prompts reset ${slug}\`.` : `${slug} has no history yet: the repo's baked default is what runs. There is nothing to roll back to.`);
5181
+ }
5182
+ return restore(detail, { body: target.body, from: version, note: `rolled back to v${version}` });
5183
+ }
5184
+ async function promptResetCommand(slug) {
5185
+ const detail = await promptDetailCommand(slug);
5186
+ return restore(detail, {
5187
+ body: detail.defaultBody,
5188
+ from: 0,
5189
+ note: "reset to the repo's baked default"
5190
+ });
5191
+ }
5192
+ async function restore(detail, input) {
5193
+ if (input.body.trim() === detail.activeBody.trim()) {
5194
+ return { from: input.from, skipped: true, slug: detail.slug, version: detail.activeVersion };
5195
+ }
5196
+ const response = await adminApiPost(`/api/admin/prompts/${encodeURIComponent(detail.slug)}`, { body: input.body, note: input.note });
5197
+ return { from: input.from, skipped: false, slug: detail.slug, version: response.version };
5198
+ }
5199
+ function parseAgainst(value) {
5200
+ if (value === undefined || value.trim().toLowerCase() === "default") {
5201
+ return { kind: "default" };
5202
+ }
5203
+ const version = parseVersion3(value);
5204
+ if (version === undefined) {
5205
+ throw new CliError2("invalid_against", `--against takes a version (3, or v3) or the word default. Got "${value}".`);
5206
+ }
5207
+ return { kind: "version", version };
5208
+ }
5209
+ function parseVersion3(value) {
5210
+ const digits = /^v?(\d+)$/.exec(value.trim());
5211
+ const parsed = digits?.[1];
5212
+ if (parsed === undefined) {
5213
+ return;
5214
+ }
5215
+ const version = Number.parseInt(parsed, 10);
5216
+ return Number.isFinite(version) && version > 0 ? version : undefined;
5217
+ }
5218
+ function bodyLines(body) {
5219
+ return body.replace(/\r\n/g, `
5220
+ `).replace(/\n+$/, "").split(`
5221
+ `);
5222
+ }
5223
+ function diffLines(before, after) {
5224
+ const rows = before.length;
5225
+ const cols = after.length;
5226
+ const width = cols + 1;
5227
+ const lengths = new Int32Array((rows + 1) * width);
5228
+ const lcs = (row2, col2) => lengths[row2 * width + col2] ?? 0;
5229
+ for (let row2 = rows - 1;row2 >= 0; row2 -= 1) {
5230
+ for (let col2 = cols - 1;col2 >= 0; col2 -= 1) {
5231
+ lengths[row2 * width + col2] = before[row2] === after[col2] ? lcs(row2 + 1, col2 + 1) + 1 : Math.max(lcs(row2 + 1, col2), lcs(row2, col2 + 1));
5232
+ }
5233
+ }
5234
+ const lines = [];
5235
+ let row = 0;
5236
+ let col = 0;
5237
+ while (row < rows && col < cols) {
5238
+ if (before[row] === after[col]) {
5239
+ lines.push({ kind: "context", text: before[row] ?? "" });
5240
+ row += 1;
5241
+ col += 1;
5242
+ continue;
5243
+ }
5244
+ if (lcs(row + 1, col) >= lcs(row, col + 1)) {
5245
+ lines.push({ kind: "remove", text: before[row] ?? "" });
5246
+ row += 1;
5247
+ continue;
5248
+ }
5249
+ lines.push({ kind: "add", text: after[col] ?? "" });
5250
+ col += 1;
5251
+ }
5252
+ while (row < rows) {
5253
+ lines.push({ kind: "remove", text: before[row] ?? "" });
5254
+ row += 1;
5255
+ }
5256
+ while (col < cols) {
5257
+ lines.push({ kind: "add", text: after[col] ?? "" });
5258
+ col += 1;
5259
+ }
5260
+ return lines;
5261
+ }
5262
+ function renderDiff(lines) {
5263
+ return lines.map((line) => {
5264
+ const marker = line.kind === "add" ? "+" : line.kind === "remove" ? "-" : " ";
5265
+ return `${marker} ${line.text}`;
5266
+ });
5267
+ }
5268
+ async function promptDiffCommand(slug, against) {
5269
+ const detail = await promptDetailCommand(slug);
5270
+ const from = against.kind === "default" ? { body: detail.defaultBody, label: "the repo's baked default", version: 0 } : resolveAgainstVersion(detail, against.version);
5271
+ const lines = diffLines(bodyLines(from.body), bodyLines(detail.activeBody));
5272
+ return {
5273
+ added: lines.filter((line) => line.kind === "add").length,
5274
+ against: { label: from.label, version: from.version },
5275
+ lines,
5276
+ live: { source: detail.source, version: detail.activeVersion },
5277
+ removed: lines.filter((line) => line.kind === "remove").length,
5278
+ slug: detail.slug
5279
+ };
5280
+ }
5281
+ function resolveAgainstVersion(detail, version) {
5282
+ const target = detail.versions.find((candidate) => candidate.version === version);
5283
+ if (!target) {
5284
+ const known = detail.versions.map((candidate) => `v${candidate.version}`).join(", ");
5285
+ throw new CliError2("unknown_version", known.length > 0 ? `${detail.slug} has no v${version}. On file: ${known}.` : `${detail.slug} has no history yet. Diff against the repo's default instead: drop --against.`);
5286
+ }
5287
+ return { body: target.body, label: `v${version}`, version };
5288
+ }
5289
+ function promptRows(prompts) {
5290
+ const slugWidth = prompts.reduce((width, prompt) => Math.max(width, prompt.slug.length), 0);
5291
+ const surfaceWidth = prompts.reduce((width, prompt) => Math.max(width, prompt.surface.length), 0);
5292
+ return prompts.map((prompt) => {
5293
+ const live = prompt.source === "override" ? `v${prompt.activeVersion}` : "default";
5294
+ return `${prompt.slug.padEnd(slugWidth)} ${prompt.surface.padEnd(surfaceWidth)} ${live.padEnd(7)} ${prompt.title}`;
5295
+ });
5296
+ }
5297
+ function historyRows(versions) {
5298
+ return versions.map((version) => {
5299
+ const when = version.createdAt.slice(0, 10);
5300
+ const note = version.note?.trim();
5301
+ return `v${version.version} ${when} ${version.createdBy.padEnd(8)} ${note && note.length > 0 ? note : "(no note)"}`;
5302
+ });
5303
+ }
5304
+ var init_admin_prompts = __esm(() => {
5305
+ init_api();
5306
+ init_output();
5307
+ });
5308
+
5112
5309
  // src/commands/add.ts
5113
5310
  async function addCommand2(spotifyUrl, options) {
5114
5311
  const result = await adminApiPost("/api/admin/tracks", {
@@ -5197,7 +5394,10 @@ async function approveSubmissionCommand(submissionId, options = {}) {
5197
5394
  console.log(`Approved ${formatTrackLine(submission)}.`);
5198
5395
  }
5199
5396
  async function triageSubmissionCommand(submissionId, verdict, options = {}) {
5200
- const response = await adminApiPost(`/api/admin/submissions/${encodeURIComponent(submissionId)}/triage`, { verdict });
5397
+ const response = await adminApiPost(`/api/admin/submissions/${encodeURIComponent(submissionId)}/triage`, {
5398
+ ...typeof options.promptVersion === "number" ? { promptVersion: options.promptVersion } : {},
5399
+ verdict
5400
+ });
5201
5401
  if (options.json) {
5202
5402
  printJson2({ ok: true, submission: response.submission });
5203
5403
  return;
@@ -5546,6 +5746,22 @@ var init_admin_artists = __esm(() => {
5546
5746
  init_api();
5547
5747
  });
5548
5748
 
5749
+ // src/commands/admin-labels.ts
5750
+ var exports_admin_labels = {};
5751
+ __export(exports_admin_labels, {
5752
+ backfillLabelImagesCommand: () => backfillLabelImagesCommand
5753
+ });
5754
+ async function backfillLabelImagesCommand(limit, dryRun, cursor) {
5755
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
5756
+ if (cursor) {
5757
+ params.set("cursor", cursor);
5758
+ }
5759
+ return adminApiPost(`/api/admin/backfill/label-images?${params.toString()}`);
5760
+ }
5761
+ var init_admin_labels = __esm(() => {
5762
+ init_api();
5763
+ });
5764
+
5549
5765
  // src/commands/preview-migration.ts
5550
5766
  var exports_preview_migration = {};
5551
5767
  __export(exports_preview_migration, {
@@ -5845,7 +6061,7 @@ var init_format2 = __esm(() => {
5845
6061
  });
5846
6062
 
5847
6063
  // src/cli.ts
5848
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
6064
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
5849
6065
  import path2 from "path";
5850
6066
 
5851
6067
  // ../../node_modules/commander/lib/error.js
@@ -8210,7 +8426,7 @@ function addAdminCommands(program2) {
8210
8426
  const { previewArchiveUploadCommand: previewArchiveUploadCommand2 } = await Promise.resolve().then(() => (init_preview_archive(), exports_preview_archive));
8211
8427
  await runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCommand2);
8212
8428
  });
8213
- adminTrack.command("observe").description("Render Fluncle's spoken field observation for a track (Cartesia, Worker-side)").argument("[idOrLogId]").option("--queue", "Show the observe worklist (notes but no observation yet), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--script <text>", "The voice-gated observation script (the spoken text)").option("--script-file <file>", "Read the observation script from a file (e.g. observation.txt)").option("--voice-id <id>", "Override the configured Cartesia voice id").option("--duration-ms <ms>", "Probed audio duration in ms (else derived from word timestamps)").option("--duration-target-sec <sec>", "Target observation length in seconds (20\u201345)").option("--context-note <text>", "Pre-fetched factual context (else the Worker firecrawls)").option("--force", "Re-render even if an observation already exists (voice re-tune / fix)", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
8429
+ adminTrack.command("observe").description("Render Fluncle's spoken field observation for a track (Cartesia, Worker-side)").argument("[idOrLogId]").option("--queue", "Show the observe worklist (notes but no observation yet), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--script <text>", "The voice-gated observation script (the spoken text)").option("--script-file <file>", "Read the observation script from a file (e.g. observation.txt)").option("--voice-id <id>", "Override the configured Cartesia voice id").option("--duration-ms <ms>", "Probed audio duration in ms (else derived from word timestamps)").option("--duration-target-sec <sec>", "Target observation length in seconds (20\u201345)").option("--context-note <text>", "Pre-fetched factual context (else the Worker firecrawls)").option("--prompt-version <n>", "The prompt-registry version that authored this (the sweep sends it; it is the artifact's provenance)").option("--force", "Re-render even if an observation already exists (voice re-tune / fix)", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
8214
8430
  if (options.queue) {
8215
8431
  const { observeQueueCommand: observeQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
8216
8432
  await runAdminObserveQueue(options, observeQueueCommand2);
@@ -8228,7 +8444,7 @@ function addAdminCommands(program2) {
8228
8444
  const { trackContextCommand: trackContextCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
8229
8445
  await runTrackContext(idOrLogId, options, trackContextCommand2);
8230
8446
  });
8231
- adminTrack.command("note").description("Author the editorial note for a finding (fills an empty note only)").argument("[idOrLogId]").option("--queue", "Show the note worklist (context'd findings with no note yet), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--script <text>", "The voice-gated editorial note").option("--script-file <file>", "Read the editorial note from a file").option("--dry-run", "Run the voice + echo gates and report the verdict without storing anything", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
8447
+ adminTrack.command("note").description("Author the editorial note for a finding (fills an empty note only)").argument("[idOrLogId]").option("--queue", "Show the note worklist (context'd findings with no note yet), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--script <text>", "The voice-gated editorial note").option("--script-file <file>", "Read the editorial note from a file").option("--prompt-version <n>", "The prompt-registry version that authored the note (the sweep sends this; it is the note's provenance)").option("--dry-run", "Run the voice + echo gates and report the verdict without storing anything", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
8232
8448
  if (options.queue) {
8233
8449
  const { noteQueueCommand: noteQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
8234
8450
  await runAdminNoteQueue(options, noteQueueCommand2);
@@ -8361,9 +8577,9 @@ function addAdminCommands(program2) {
8361
8577
  adminNewsletter.action(() => {
8362
8578
  adminNewsletter.outputHelp();
8363
8579
  });
8364
- adminNewsletter.command("draft").description("Persist a newsletter edition draft (the agent authors it, you send it)").option("--content-file <file>", "Structured edition content payload (JSON)").option("--subject <text>", "Email subject line").option("--window-since <date>", "Discovery-window start (ISO)").option("--window-until <date>", "Discovery-window end (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
8580
+ adminNewsletter.command("draft").description("Persist a newsletter edition draft (the agent authors it, you send it)").option("--content-file <file>", "Structured edition content payload (JSON)").option("--subject <text>", "Email subject line").option("--window-since <date>", "Discovery-window start (ISO)").option("--window-until <date>", "Discovery-window end (ISO)").option("--prompt-version <n>", "The prompt-registry version that authored this (the sweep sends it; it is the edition's provenance)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
8365
8581
  const { newsletterDraftCommand: newsletterDraftCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
8366
- await runNewsletterDraft(options, newsletterDraftCommand2);
8582
+ await runNewsletterDraft({ ...options, promptVersion: parsePromptVersion(options.promptVersion) }, newsletterDraftCommand2);
8367
8583
  });
8368
8584
  adminNewsletter.command("update").description("Update a draft edition's payload, subject, or window").argument("[id]").option("--content-file <file>", "Structured edition content payload (JSON)").option("--subject <text>", "Email subject line").option("--window-since <date>", "Discovery-window start (ISO)").option("--window-until <date>", "Discovery-window end (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
8369
8585
  const { newsletterUpdateCommand: newsletterUpdateCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
@@ -8389,7 +8605,7 @@ function addAdminCommands(program2) {
8389
8605
  const { logbookGapsCommand: logbookGapsCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
8390
8606
  await runLogbookGaps(options, logbookGapsCommand2);
8391
8607
  });
8392
- 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) => {
8608
+ 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("--prompt-version <n>", "The prompt-registry version that authored this (the sweep sends it; it is the artifact's provenance)").option("--json", "Print JSON", false).allowExcessArguments().action(async (sector, options) => {
8393
8609
  const { logbookCreateCommand: logbookCreateCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
8394
8610
  await runLogbookWrite(sector, options, logbookCreateCommand2);
8395
8611
  });
@@ -8397,6 +8613,38 @@ function addAdminCommands(program2) {
8397
8613
  const { logbookUpdateCommand: logbookUpdateCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
8398
8614
  await runLogbookWrite(sector, options, logbookUpdateCommand2);
8399
8615
  });
8616
+ const adminPrompts = configureCommand(admin.command("prompts").description("The prompt registry: what Fluncle tells the models"));
8617
+ adminPrompts.action(() => {
8618
+ adminPrompts.outputHelp();
8619
+ });
8620
+ adminPrompts.command("list").description("Every prompt: where it runs, and whether the repo default or an edit is live").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
8621
+ const { promptsListCommand: promptsListCommand2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
8622
+ await runPromptsList(options, promptsListCommand2);
8623
+ });
8624
+ adminPrompts.command("get").description("The body running right now, with its version and source").argument("[slug]").option("--json", "Print JSON", false).allowExcessArguments().action(async (slug, options) => {
8625
+ const { promptGetCommand: promptGetCommand2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
8626
+ await runPromptGet(slug, options, promptGetCommand2);
8627
+ });
8628
+ adminPrompts.command("history").description("Every version of a prompt, newest first: when, who, and the why").argument("[slug]").option("--json", "Print JSON", false).allowExcessArguments().action(async (slug, options) => {
8629
+ const { promptDetailCommand: promptDetailCommand2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
8630
+ await runPromptHistory(slug, options, promptDetailCommand2);
8631
+ });
8632
+ adminPrompts.command("diff").description("Line diff: the live body against the repo default, or against a version").argument("[slug]").option("--against <version>", "A version (3, or v3) or the word default", "default").option("--json", "Print JSON", false).allowExcessArguments().action(async (slug, options) => {
8633
+ const { parseAgainst: parseAgainst2, promptDiffCommand: promptDiffCommand2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
8634
+ await runPromptDiff(slug, options, { parseAgainst: parseAgainst2, promptDiffCommand: promptDiffCommand2 });
8635
+ });
8636
+ adminPrompts.command("update").description("Append an edited body as a new version. OPERATOR only").argument("[slug]").option("--body-file <file>", "Read the new prompt body from a file").option("--note <text>", 'The why, for the history ("shortened the neighbour block")').option("--json", "Print JSON", false).allowExcessArguments().action(async (slug, options) => {
8637
+ const { promptUpdateCommand: promptUpdateCommand2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
8638
+ await runPromptUpdate(slug, options, promptUpdateCommand2);
8639
+ });
8640
+ adminPrompts.command("rollback").description("Put an old version's body back, as a new version. OPERATOR only").argument("[slug]").argument("[version]").option("--json", "Print JSON", false).allowExcessArguments().action(async (slug, version, options) => {
8641
+ const { parseVersion: parseVersion4, promptRollbackCommand: promptRollbackCommand2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
8642
+ await runPromptRollback(slug, version, options, { parseVersion: parseVersion4, promptRollbackCommand: promptRollbackCommand2 });
8643
+ });
8644
+ adminPrompts.command("reset").description("Put the repo's baked default back, as a new version. OPERATOR only").argument("[slug]").option("--json", "Print JSON", false).allowExcessArguments().action(async (slug, options) => {
8645
+ const { promptResetCommand: promptResetCommand2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
8646
+ await runPromptReset(slug, options, promptResetCommand2);
8647
+ });
8400
8648
  const submissions = configureCommand(admin.command("submissions").description("Review listener submissions"));
8401
8649
  submissions.option("--json", "Print JSON", false).action(async (options) => {
8402
8650
  const { listSubmissionsCommand: listSubmissionsCommand2 } = await Promise.resolve().then(() => (init_submissions(), exports_submissions));
@@ -8423,16 +8671,19 @@ function addAdminCommands(program2) {
8423
8671
  const { approveSubmissionCommand: approveSubmissionCommand2 } = await Promise.resolve().then(() => (init_submissions(), exports_submissions));
8424
8672
  await approveSubmissionCommand2(submissionId, options);
8425
8673
  });
8426
- submissions.command("triage").description("Write the pre-chew triage verdict onto a pending submission").argument("[submissionId]").option("--verdict <text>", "The triage verdict one-liner").option("--verdict-file <file>", "Read the verdict from a file").option("--json", "Print JSON", false).action(async (submissionId, options) => {
8674
+ submissions.command("triage").description("Write the pre-chew triage verdict onto a pending submission").argument("[submissionId]").option("--verdict <text>", "The triage verdict one-liner").option("--verdict-file <file>", "Read the verdict from a file").option("--prompt-version <n>", "The prompt-registry version that authored this (the sweep sends it; it is the artifact's provenance)").option("--json", "Print JSON", false).action(async (submissionId, options) => {
8427
8675
  if (!submissionId) {
8428
8676
  throw new Error("Missing submission id for: triage");
8429
8677
  }
8430
- const verdict = options.verdictFile ? readFileSync6(options.verdictFile, "utf8") : options.verdict;
8678
+ const verdict = options.verdictFile ? readFileSync7(options.verdictFile, "utf8") : options.verdict;
8431
8679
  if (!verdict || !verdict.trim()) {
8432
8680
  throw new Error("Usage: fluncle admin submissions triage <submissionId> (--verdict <text> | --verdict-file <file>) [--json]");
8433
8681
  }
8434
8682
  const { triageSubmissionCommand: triageSubmissionCommand2 } = await Promise.resolve().then(() => (init_submissions(), exports_submissions));
8435
- await triageSubmissionCommand2(submissionId, verdict, { json: options.json });
8683
+ await triageSubmissionCommand2(submissionId, verdict, {
8684
+ json: options.json,
8685
+ promptVersion: parsePromptVersion(options.promptVersion)
8686
+ });
8436
8687
  });
8437
8688
  const auth = configureCommand(admin.command("auth").description("Authentication commands"));
8438
8689
  auth.command("spotify").description("Authorize Spotify access").action(async () => {
@@ -8527,6 +8778,10 @@ function addAdminCommands(program2) {
8527
8778
  const { backfillArtistImagesCommand: backfillArtistImagesCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
8528
8779
  await runBackfillArtistImages(options, backfillArtistImagesCommand2);
8529
8780
  });
8781
+ backfill.command("label-images").description("Resolve label logos (Discogs \u2192 Wikidata) into R2 for existing labels").option("--dry-run", "Report the eligible worklist without any vendor call or write", false).option("--limit <limit>", "Max labels to process", "50").option("--json", "Print JSON", false).action(async (options) => {
8782
+ const { backfillLabelImagesCommand: backfillLabelImagesCommand2 } = await Promise.resolve().then(() => (init_admin_labels(), exports_admin_labels));
8783
+ await runBackfillLabelImages(options, backfillLabelImagesCommand2);
8784
+ });
8530
8785
  artists.command("resolve").description("Resolve an artist's social identity (MB url-rels + Firecrawl gap-fill)").argument("[artistId]").option("--queue", "Show the resolve worklist (artists awaiting resolution), oldest first", false).option("--limit <limit>", "Number of artists to show with --queue", "50").option("--json", "Print JSON", false).allowExcessArguments().action(async (artistId, options) => {
8531
8786
  if (options.queue) {
8532
8787
  const { listArtistsCommand: listArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
@@ -8564,7 +8819,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
8564
8819
  console.log(` mime: ${result.mime}`);
8565
8820
  }
8566
8821
  async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
8567
- const script = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
8822
+ const script = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
8568
8823
  if (!idOrLogId || !script || !script.trim()) {
8569
8824
  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]");
8570
8825
  }
@@ -8581,6 +8836,7 @@ async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
8581
8836
  durationMs,
8582
8837
  durationTargetSec,
8583
8838
  force: options.force,
8839
+ promptVersion: parsePromptVersion(options.promptVersion),
8584
8840
  script: script.trim(),
8585
8841
  voiceId: options.voiceId
8586
8842
  });
@@ -8619,14 +8875,22 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
8619
8875
  console.log(` sources: ${result.sources.join(", ")}`);
8620
8876
  }
8621
8877
  }
8878
+ function parsePromptVersion(raw) {
8879
+ if (raw === undefined) {
8880
+ return;
8881
+ }
8882
+ const parsed = Number(raw);
8883
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
8884
+ }
8622
8885
  async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
8623
- const note = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
8886
+ const note = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
8624
8887
  if (!idOrLogId || !note || !note.trim()) {
8625
8888
  throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--dry-run] [--json]");
8626
8889
  }
8627
8890
  const result = await trackNoteCommand2(idOrLogId, {
8628
8891
  dryRun: options.dryRun,
8629
- note: note.trim()
8892
+ note: note.trim(),
8893
+ promptVersion: parsePromptVersion(options.promptVersion)
8630
8894
  });
8631
8895
  if (options.json) {
8632
8896
  printJson(result);
@@ -9009,6 +9273,63 @@ async function runBackfillArtistImages(options, backfillArtistImagesCommand2) {
9009
9273
  process.exitCode = 1;
9010
9274
  }
9011
9275
  }
9276
+ async function runBackfillLabelImages(options, backfillLabelImagesCommand2) {
9277
+ const limit = parseListLimit(options.limit);
9278
+ const resolved = [];
9279
+ const none = [];
9280
+ const failed = [];
9281
+ let cursor;
9282
+ let dryRun = options.dryRun;
9283
+ let throttled = false;
9284
+ while (resolved.length + none.length + failed.length < limit) {
9285
+ const remaining = limit - (resolved.length + none.length + failed.length);
9286
+ const result = await backfillLabelImagesCommand2(remaining, options.dryRun, cursor);
9287
+ dryRun = result.dryRun;
9288
+ resolved.push(...result.resolved);
9289
+ none.push(...result.none);
9290
+ failed.push(...result.failed);
9291
+ if (!options.json) {
9292
+ const verb2 = result.dryRun ? "would resolve" : "resolved";
9293
+ console.log(` \u2026${verb2} ${result.resolvedCount}; ${result.noneCount} without an image; ${result.failedCount} failed`);
9294
+ }
9295
+ if (result.rateLimited) {
9296
+ throttled = true;
9297
+ break;
9298
+ }
9299
+ if (result.nextCursor === null) {
9300
+ break;
9301
+ }
9302
+ cursor = result.nextCursor;
9303
+ }
9304
+ if (options.json) {
9305
+ printJson({
9306
+ dryRun,
9307
+ failed,
9308
+ failedCount: failed.length,
9309
+ none,
9310
+ noneCount: none.length,
9311
+ ok: true,
9312
+ rateLimited: throttled,
9313
+ resolved,
9314
+ resolvedCount: resolved.length
9315
+ });
9316
+ if (failed.length > 0) {
9317
+ process.exitCode = 1;
9318
+ }
9319
+ return;
9320
+ }
9321
+ const verb = dryRun ? "Would resolve" : "Resolved";
9322
+ console.log(`${verb} ${resolved.length} label logo(s); ${none.length} without an image; ${failed.length} failed.`);
9323
+ for (const slug of resolved) {
9324
+ console.log(` ${slug}`);
9325
+ }
9326
+ for (const item of failed) {
9327
+ console.log(` ${item.slug}: ${item.error}`);
9328
+ }
9329
+ if (failed.length > 0) {
9330
+ process.exitCode = 1;
9331
+ }
9332
+ }
9012
9333
  async function runArtistResolveQueue(options, listArtistsCommand2) {
9013
9334
  const limit = parseListLimit(options.limit);
9014
9335
  const result = await listArtistsCommand2(limit ?? 50);
@@ -9059,7 +9380,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
9059
9380
  return;
9060
9381
  }
9061
9382
  const candidate = path2.join(dir, name);
9062
- return existsSync6(candidate) ? candidate : undefined;
9383
+ return existsSync7(candidate) ? candidate : undefined;
9063
9384
  };
9064
9385
  const resolveFile = (explicit, name) => {
9065
9386
  if (explicit) {
@@ -9271,7 +9592,7 @@ async function runTrackUpdate(trackId, options, trackUpdateCommand3) {
9271
9592
  if (options.analyzedFrom !== undefined && options.analyzedFrom !== "full" && options.analyzedFrom !== "preview") {
9272
9593
  throw new Error(`Invalid --analyzed-from: ${options.analyzedFrom} (expected full or preview)`);
9273
9594
  }
9274
- const embeddingRaw = options.embeddingFile ? readFileSync6(options.embeddingFile, "utf8") : options.embedding;
9595
+ const embeddingRaw = options.embeddingFile ? readFileSync7(options.embeddingFile, "utf8") : options.embedding;
9275
9596
  const embedding = parseEmbeddingArg(embeddingRaw);
9276
9597
  const result = await trackUpdateCommand3(trackId, {
9277
9598
  analyzedAt: options.analyzedAt,
@@ -9644,6 +9965,7 @@ async function runLogbookWrite(sector, options, writeCommand) {
9644
9965
  const result = await writeCommand(sector, {
9645
9966
  body: options.body,
9646
9967
  bodyFile: options.bodyFile,
9968
+ promptVersion: parsePromptVersion(options.promptVersion),
9647
9969
  title: options.title
9648
9970
  });
9649
9971
  if (options.json) {
@@ -9653,6 +9975,109 @@ async function runLogbookWrite(sector, options, writeCommand) {
9653
9975
  const skipped = "skipped" in result && result.skipped;
9654
9976
  console.log(skipped ? `sector ${result.entry.sector}: an entry already stands \u2014 no-op (${result.entry.generatedBy})` : `sector ${result.entry.sector}: ${result.entry.title}`);
9655
9977
  }
9978
+ function requirePromptSlug(slug) {
9979
+ if (!slug) {
9980
+ throw new Error("Missing prompt slug. `fluncle admin prompts list` names them all.");
9981
+ }
9982
+ return slug;
9983
+ }
9984
+ async function runPromptsList(options, promptsListCommand2) {
9985
+ const prompts = await promptsListCommand2();
9986
+ if (options.json) {
9987
+ printJson({ ok: true, prompts });
9988
+ return;
9989
+ }
9990
+ const { promptRows: promptRows2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
9991
+ for (const row of promptRows2(prompts)) {
9992
+ console.log(row);
9993
+ }
9994
+ }
9995
+ async function runPromptGet(slug, options, promptGetCommand2) {
9996
+ const resolved = await promptGetCommand2(requirePromptSlug(slug));
9997
+ if (options.json) {
9998
+ printJson(resolved);
9999
+ return;
10000
+ }
10001
+ const live = resolved.source === "override" ? `v${resolved.version}, an operator edit` : "the repo's baked default (v0)";
10002
+ console.log(`${resolved.slug}: ${live}`);
10003
+ console.log("");
10004
+ console.log(resolved.body);
10005
+ }
10006
+ async function runPromptHistory(slug, options, promptDetailCommand2) {
10007
+ const detail = await promptDetailCommand2(requirePromptSlug(slug));
10008
+ if (options.json) {
10009
+ printJson({
10010
+ activeVersion: detail.activeVersion,
10011
+ ok: true,
10012
+ slug: detail.slug,
10013
+ source: detail.source,
10014
+ versions: detail.versions
10015
+ });
10016
+ return;
10017
+ }
10018
+ if (detail.versions.length === 0) {
10019
+ console.log(`${detail.slug}: never edited. The repo's baked default is what runs.`);
10020
+ return;
10021
+ }
10022
+ const { historyRows: historyRows2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
10023
+ for (const row of historyRows2(detail.versions)) {
10024
+ console.log(row);
10025
+ }
10026
+ }
10027
+ async function runPromptDiff(slug, options, commands) {
10028
+ const against = commands.parseAgainst(options.against);
10029
+ const result = await commands.promptDiffCommand(requirePromptSlug(slug), against);
10030
+ if (options.json) {
10031
+ printJson({ ok: true, ...result });
10032
+ return;
10033
+ }
10034
+ const live = result.live.source === "override" ? `v${result.live.version}` : "the repo's baked default";
10035
+ if (result.added === 0 && result.removed === 0) {
10036
+ console.log(`${result.slug}: ${live} and ${result.against.label} are the same body.`);
10037
+ return;
10038
+ }
10039
+ const { renderDiff: renderDiff2 } = await Promise.resolve().then(() => (init_admin_prompts(), exports_admin_prompts));
10040
+ console.log(`${result.slug}: ${result.against.label} (-) against ${live} (+)`);
10041
+ for (const line of renderDiff2(result.lines)) {
10042
+ console.log(line);
10043
+ }
10044
+ console.log(`${result.added} added, ${result.removed} removed.`);
10045
+ }
10046
+ async function runPromptUpdate(slug, options, promptUpdateCommand2) {
10047
+ const result = await promptUpdateCommand2(requirePromptSlug(slug), {
10048
+ bodyFile: options.bodyFile,
10049
+ note: options.note
10050
+ });
10051
+ if (options.json) {
10052
+ printJson({ ok: true, ...result });
10053
+ return;
10054
+ }
10055
+ console.log(`${result.slug}: v${result.version} is live. Nothing to deploy.`);
10056
+ }
10057
+ async function runPromptRollback(slug, version, options, commands) {
10058
+ const promptSlug = requirePromptSlug(slug);
10059
+ if (!version) {
10060
+ throw new Error(`Missing version. \`fluncle admin prompts history ${promptSlug}\` lists what you can go back to.`);
10061
+ }
10062
+ const parsed = commands.parseVersion(version);
10063
+ if (parsed === undefined) {
10064
+ throw new Error(`A version reads as 3 or v3. Got "${version}".`);
10065
+ }
10066
+ const result = await commands.promptRollbackCommand(promptSlug, parsed);
10067
+ if (options.json) {
10068
+ printJson({ ok: true, ...result });
10069
+ return;
10070
+ }
10071
+ console.log(result.skipped ? `${result.slug}: v${result.from} is already the body running. Nothing appended.` : `${result.slug}: back on v${result.from}'s body, live as v${result.version}.`);
10072
+ }
10073
+ async function runPromptReset(slug, options, promptResetCommand2) {
10074
+ const result = await promptResetCommand2(requirePromptSlug(slug));
10075
+ if (options.json) {
10076
+ printJson({ ok: true, ...result });
10077
+ return;
10078
+ }
10079
+ console.log(result.skipped ? `${result.slug}: already running the repo's baked default. Nothing appended.` : `${result.slug}: back on the repo's baked default, live as v${result.version}.`);
10080
+ }
9656
10081
  async function runAdd(spotifyUrl, options, addCommand3) {
9657
10082
  if (!spotifyUrl) {
9658
10083
  throw new Error("Missing Spotify track URL");
@@ -9858,7 +10283,7 @@ async function runGalaxyEmbeddings(options, galaxyEmbeddingsCommand2) {
9858
10283
  console.log(`${result.embeddings.length} embedding(s)${result.nextCursor ? ` \xB7 nextCursor: ${result.nextCursor}` : " \xB7 end of corpus"}`);
9859
10284
  }
9860
10285
  async function runGalaxyMapWrite(options, galaxyMapWriteCommand2) {
9861
- const raw = JSON.parse(readFileSync6(options.file, "utf8"));
10286
+ const raw = JSON.parse(readFileSync7(options.file, "utf8"));
9862
10287
  const clusters = Array.isArray(raw) || raw === null ? raw : raw.clusters;
9863
10288
  if (!Array.isArray(clusters)) {
9864
10289
  throw new Error("Invalid --file: expected { clusters: [...] } or a JSON array of cluster rows");
@@ -10400,6 +10825,7 @@ function normalizeCommanderError(error) {
10400
10825
  return new Error(message);
10401
10826
  }
10402
10827
  var stringOptions = new Set([
10828
+ "--against",
10403
10829
  "--analyzed-at",
10404
10830
  "--analyzed-from",
10405
10831
  "--at",
@@ -10451,6 +10877,7 @@ var stringOptions = new Set([
10451
10877
  "--plate-background",
10452
10878
  "--platform",
10453
10879
  "--poster",
10880
+ "--prompt-version",
10454
10881
  "--props",
10455
10882
  "--query",
10456
10883
  "--reasoning",
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.135.0"
34
+ "version": "0.137.0"
35
35
  }