fluncle 0.65.0 → 0.67.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 +314 -41
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -423,7 +423,7 @@ function parseVersion(version) {
423
423
  var currentVersion;
424
424
  var init_version = __esm(() => {
425
425
  init_output();
426
- currentVersion = "0.65.0".trim() ? "0.65.0".trim() : "0.1.0";
426
+ currentVersion = "0.67.0".trim() ? "0.67.0".trim() : "0.1.0";
427
427
  });
428
428
 
429
429
  // src/update-notifier.ts
@@ -657,7 +657,68 @@ var init_env = __esm(() => {
657
657
  envProfiles2 = ["local", "production"];
658
658
  });
659
659
 
660
+ // src/user-token.ts
661
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
662
+ import { homedir as homedir3 } from "node:os";
663
+ import { dirname as dirname2, join as join3 } from "node:path";
664
+ function activeProfile() {
665
+ const profile = process.env.FLUNCLE_ENV ?? defaultEnvProfile2;
666
+ return envProfiles3.includes(profile) ? profile : defaultEnvProfile2;
667
+ }
668
+ function userTokenPath() {
669
+ return join3(homedir3(), ".config", "fluncle", `user.${activeProfile()}.json`);
670
+ }
671
+ function isStoredUserToken(value) {
672
+ if (typeof value !== "object" || value === null) {
673
+ return false;
674
+ }
675
+ const record = value;
676
+ return typeof record.token === "string" && typeof record.baseUrl === "string";
677
+ }
678
+ function readUserToken() {
679
+ try {
680
+ const parsed = JSON.parse(readFileSync(userTokenPath(), "utf8"));
681
+ return isStoredUserToken(parsed) ? parsed : undefined;
682
+ } catch {
683
+ return;
684
+ }
685
+ }
686
+ function writeUserToken(value) {
687
+ const path2 = userTokenPath();
688
+ mkdirSync(dirname2(path2), { recursive: true });
689
+ writeFileSync(path2, `${JSON.stringify(value, null, 2)}
690
+ `, { mode: 384 });
691
+ }
692
+ function clearUserToken() {
693
+ const path2 = userTokenPath();
694
+ try {
695
+ readFileSync(path2);
696
+ } catch {
697
+ return false;
698
+ }
699
+ rmSync(path2, { force: true });
700
+ return true;
701
+ }
702
+ var envProfiles3, defaultEnvProfile2 = "production";
703
+ var init_user_token = __esm(() => {
704
+ envProfiles3 = ["local", "production"];
705
+ });
706
+
660
707
  // src/api.ts
708
+ async function userApiGet(path2) {
709
+ return apiRequest(path2, {
710
+ headers: userHeaders()
711
+ });
712
+ }
713
+ function userHeaders() {
714
+ const stored = readUserToken();
715
+ if (!stored) {
716
+ throw new CliError2("not_logged_in", "You're not signed in. Run `fluncle login` to link this device to your account.");
717
+ }
718
+ return {
719
+ Authorization: `Bearer ${stored.token}`
720
+ };
721
+ }
661
722
  async function publicApiGet(path2) {
662
723
  return apiRequest(path2);
663
724
  }
@@ -747,6 +808,7 @@ function parseJson(text) {
747
808
  var init_api = __esm(() => {
748
809
  init_env();
749
810
  init_output();
811
+ init_user_token();
750
812
  });
751
813
 
752
814
  // src/commands/recent.ts
@@ -1163,7 +1225,7 @@ __export(exports_mixtapes, {
1163
1225
  mixtapeDeleteCommand: () => mixtapeDeleteCommand,
1164
1226
  mixtapeCreateCommand: () => mixtapeCreateCommand
1165
1227
  });
1166
- import { existsSync, readFileSync } from "node:fs";
1228
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1167
1229
  async function mixtapesCommand() {
1168
1230
  const response = await publicApiGet("/api/mixtapes");
1169
1231
  return response.mixtapes;
@@ -1294,7 +1356,7 @@ function parseCueFile(filePath) {
1294
1356
  if (!existsSync(filePath)) {
1295
1357
  throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
1296
1358
  }
1297
- const text = readFileSync(filePath, "utf-8");
1359
+ const text = readFileSync2(filePath, "utf-8");
1298
1360
  const trimmed = text.trim();
1299
1361
  if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
1300
1362
  try {
@@ -1505,6 +1567,36 @@ var init_interactive = __esm(() => {
1505
1567
  // src/links.ts
1506
1568
  var spotifyPlaylistUrl2 = "https://open.spotify.com/playlist/1m5LADqpLjiBERdtqrIiL0", telegramUrl2 = "https://t.me/fluncle";
1507
1569
 
1570
+ // src/open-external.ts
1571
+ async function openExternal(target) {
1572
+ const command = platformOpenCommand();
1573
+ if (!command) {
1574
+ console.log(target);
1575
+ throw new CliError2("unsupported_platform", `Automatic opening is only supported on macOS and Linux. Open this manually: ${target}`);
1576
+ }
1577
+ const child = Bun.spawn([command, target], {
1578
+ stderr: "pipe",
1579
+ stdout: "pipe"
1580
+ });
1581
+ const exitCode = await child.exited;
1582
+ if (exitCode !== 0) {
1583
+ const stderr = await new Response(child.stderr).text();
1584
+ throw new CliError2("open_failed", stderr.trim() || `Could not open ${target} with ${command}`);
1585
+ }
1586
+ }
1587
+ function platformOpenCommand() {
1588
+ if (process.platform === "darwin") {
1589
+ return "open";
1590
+ }
1591
+ if (process.platform === "linux") {
1592
+ return "xdg-open";
1593
+ }
1594
+ return;
1595
+ }
1596
+ var init_open_external = __esm(() => {
1597
+ init_output();
1598
+ });
1599
+
1508
1600
  // src/commands/recent.ts
1509
1601
  function mapTrack2(track) {
1510
1602
  if (track.type === "mixtape") {
@@ -1598,31 +1690,6 @@ function spotifyPlaylistUrlToAppUri(playlistUrl) {
1598
1690
  }
1599
1691
  return `spotify:playlist:${playlistId}`;
1600
1692
  }
1601
- async function openExternal(target) {
1602
- const command = platformOpenCommand();
1603
- if (!command) {
1604
- console.log(target);
1605
- throw new CliError2("unsupported_platform", `Automatic opening is only supported on macOS and Linux. Open this manually: ${target}`);
1606
- }
1607
- const child = Bun.spawn([command, target], {
1608
- stderr: "pipe",
1609
- stdout: "pipe"
1610
- });
1611
- const exitCode = await child.exited;
1612
- if (exitCode !== 0) {
1613
- const stderr = await new Response(child.stderr).text();
1614
- throw new CliError2("open_failed", stderr.trim() || `Could not open ${target} with ${command}`);
1615
- }
1616
- }
1617
- function platformOpenCommand() {
1618
- if (process.platform === "darwin") {
1619
- return "open";
1620
- }
1621
- if (process.platform === "linux") {
1622
- return "xdg-open";
1623
- }
1624
- return;
1625
- }
1626
1693
  async function selectRecentTrack(tracks) {
1627
1694
  return await selectWithKeyboard(tracks, {
1628
1695
  nonInteractiveMessage: SELECT_NON_INTERACTIVE_MESSAGE,
@@ -1649,6 +1716,7 @@ var TELEGRAM_APP_URI = "tg://resolve?domain=fluncle", SPOTIFY_PLAYLIST_OPEN_URL,
1649
1716
  var init_open = __esm(() => {
1650
1717
  init_format();
1651
1718
  init_interactive();
1719
+ init_open_external();
1652
1720
  init_output();
1653
1721
  init_recent2();
1654
1722
  SPOTIFY_PLAYLIST_OPEN_URL = `${spotifyPlaylistUrl2}?si=054d3c6cbcf14a36`;
@@ -1756,6 +1824,162 @@ var init_submit = __esm(() => {
1756
1824
  init_output();
1757
1825
  });
1758
1826
 
1827
+ // src/commands/login.ts
1828
+ var exports_login = {};
1829
+ __export(exports_login, {
1830
+ logoutCommand: () => logoutCommand,
1831
+ loginCommand: () => loginCommand
1832
+ });
1833
+ async function loginCommand() {
1834
+ const baseUrl = getApiBaseUrl();
1835
+ const existing = readUserToken();
1836
+ if (existing) {
1837
+ const who2 = existing.user?.username ?? existing.user?.name ?? "your account";
1838
+ console.log(`Already signed in as ${who2}. Run \`fluncle logout\` first to switch accounts.`);
1839
+ return;
1840
+ }
1841
+ const device = await requestDeviceCode(baseUrl);
1842
+ const verificationUrl = device.verification_uri_complete ?? device.verification_uri;
1843
+ console.log(`Linking this device to your Fluncle account.
1844
+ `);
1845
+ console.log(` Code: ${device.user_code}`);
1846
+ console.log(` Open: ${verificationUrl}
1847
+ `);
1848
+ console.log("Opening your browser to approve… (sign in if you aren't already)");
1849
+ try {
1850
+ await openExternal(verificationUrl);
1851
+ } catch {
1852
+ console.log("Couldn't open a browser automatically — visit the URL above to approve.");
1853
+ }
1854
+ const token = await pollForToken(baseUrl, device);
1855
+ const user = await fetchSessionUser(baseUrl, token.access_token);
1856
+ writeUserToken({
1857
+ baseUrl,
1858
+ token: token.access_token,
1859
+ user
1860
+ });
1861
+ const who = user?.username ?? user?.name ?? "cosmonaut";
1862
+ console.log(`
1863
+ Aboard, ${who}. This device is linked to your Galaxy.`);
1864
+ console.log("Try `fluncle me` to see your progress.");
1865
+ }
1866
+ async function logoutCommand() {
1867
+ const stored = readUserToken();
1868
+ if (stored) {
1869
+ await revokeSession(stored.baseUrl, stored.token).catch(() => {
1870
+ return;
1871
+ });
1872
+ }
1873
+ const cleared = clearUserToken();
1874
+ console.log(cleared ? "Signed out. This device is no longer linked to your account." : "Not signed in — nothing to do.");
1875
+ }
1876
+ async function requestDeviceCode(baseUrl) {
1877
+ const response = await fetch(`${baseUrl}/api/auth/device/code`, {
1878
+ body: JSON.stringify({ client_id: CLI_CLIENT_ID, scope: DEVICE_SCOPE }),
1879
+ headers: { "Content-Type": "application/json" },
1880
+ method: "POST"
1881
+ });
1882
+ const data = await response.json().catch(() => {
1883
+ return;
1884
+ });
1885
+ if (!response.ok || !data?.device_code || !data.user_code) {
1886
+ throw new CliError2("device_code_failed", `Couldn't start the login flow (${response.status}). Try again in a moment.`);
1887
+ }
1888
+ return data;
1889
+ }
1890
+ async function pollForToken(baseUrl, device) {
1891
+ let intervalMs = Math.max(device.interval, 1) * 1000;
1892
+ const deadline = Date.now() + Math.max(device.expires_in, 1) * 1000;
1893
+ await sleep(intervalMs);
1894
+ while (Date.now() < deadline) {
1895
+ const response = await fetch(`${baseUrl}/api/auth/device/token`, {
1896
+ body: JSON.stringify({
1897
+ client_id: CLI_CLIENT_ID,
1898
+ device_code: device.device_code,
1899
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1900
+ }),
1901
+ headers: { "Content-Type": "application/json" },
1902
+ method: "POST"
1903
+ });
1904
+ const data = await response.json().catch(() => {
1905
+ return;
1906
+ });
1907
+ if (response.ok && data && "access_token" in data && data.access_token) {
1908
+ return data;
1909
+ }
1910
+ const pending = data && "error" in data ? data : undefined;
1911
+ switch (pending?.error) {
1912
+ case "authorization_pending":
1913
+ break;
1914
+ case "slow_down":
1915
+ intervalMs += 5000;
1916
+ break;
1917
+ case "access_denied":
1918
+ throw new CliError2("access_denied", "Sign-in was denied in the browser. Nothing linked.");
1919
+ case "expired_token":
1920
+ throw new CliError2("expired_token", "The login request timed out. Run `fluncle login` again.");
1921
+ default:
1922
+ throw new CliError2("device_token_failed", pending?.error_description ?? `Login failed (${response.status}).`);
1923
+ }
1924
+ await sleep(intervalMs);
1925
+ }
1926
+ throw new CliError2("expired_token", "The login request timed out. Run `fluncle login` again.");
1927
+ }
1928
+ async function fetchSessionUser(baseUrl, token) {
1929
+ const response = await fetch(`${baseUrl}/api/me`, {
1930
+ headers: { Authorization: `Bearer ${token}` }
1931
+ });
1932
+ if (!response.ok) {
1933
+ return;
1934
+ }
1935
+ const data = await response.json().catch(() => {
1936
+ return;
1937
+ });
1938
+ return data?.user ?? undefined;
1939
+ }
1940
+ async function revokeSession(baseUrl, token) {
1941
+ await fetch(`${baseUrl}/api/auth/sign-out`, {
1942
+ headers: { Authorization: `Bearer ${token}` },
1943
+ method: "POST"
1944
+ });
1945
+ }
1946
+ function sleep(ms) {
1947
+ return new Promise((resolve) => setTimeout(resolve, ms));
1948
+ }
1949
+ var CLI_CLIENT_ID = "fluncle-cli", DEVICE_SCOPE = "galaxy-sync";
1950
+ var init_login = __esm(() => {
1951
+ init_env();
1952
+ init_open_external();
1953
+ init_output();
1954
+ init_user_token();
1955
+ });
1956
+
1957
+ // src/commands/me.ts
1958
+ var exports_me = {};
1959
+ __export(exports_me, {
1960
+ meCommand: () => meCommand
1961
+ });
1962
+ async function meCommand() {
1963
+ const [me, progress] = await Promise.all([
1964
+ userApiGet("/api/me"),
1965
+ userApiGet("/api/me/galaxy-progress")
1966
+ ]);
1967
+ if (!me.user) {
1968
+ throw new Error("Your sign-in expired. Run `fluncle login` to link this device again.");
1969
+ }
1970
+ return {
1971
+ collectedCount: progress.collectedLogIds.length,
1972
+ deaths: progress.deaths,
1973
+ joinedAt: me.user.createdAt,
1974
+ name: me.user.displayUsername ?? me.user.username ?? "cosmonaut",
1975
+ userId: me.user.id,
1976
+ wins: progress.wins
1977
+ };
1978
+ }
1979
+ var init_me = __esm(() => {
1980
+ init_api();
1981
+ });
1982
+
1759
1983
  // src/brand.ts
1760
1984
  var fluncleAsciiLogo2 = `███████╗██╗ ██╗ ██╗███╗ ██╗ ██████╗██╗ ███████╗
1761
1985
  ██╔════╝██║ ██║ ██║████╗ ██║██╔════╝██║ ██╔════╝
@@ -1914,10 +2138,13 @@ function parseVersion2(version) {
1914
2138
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
1915
2139
  var init_version2 = __esm(() => {
1916
2140
  init_output();
1917
- currentVersion2 = "0.65.0".trim() ? "0.65.0".trim() : "0.1.0";
2141
+ currentVersion2 = "0.67.0".trim() ? "0.67.0".trim() : "0.1.0";
1918
2142
  });
1919
2143
 
1920
2144
  // ../../packages/registry/src/index.ts
2145
+ function liveSurfaces() {
2146
+ return SURFACES.filter((surface) => surface.pending !== true);
2147
+ }
1921
2148
  var SITE = "https://www.fluncle.com", PROBE_CADENCE_MS, PROBE_TIMEOUT_MS, MINUTE_MS, SURFACES;
1922
2149
  var init_src = __esm(() => {
1923
2150
  PROBE_CADENCE_MS = 10 * 60 * 1000;
@@ -2440,6 +2667,18 @@ var init_src = __esm(() => {
2440
2667
  operatorNotes: "Authenticated admin/agent tier. The enrichment crons drive a subset of these.",
2441
2668
  weights: { cli: "hidden" }
2442
2669
  },
2670
+ {
2671
+ exposedContent: [
2672
+ "Fluncle Lens — the browser extension that finds fluncle:// coordinates on any page and links each to its /log/<coord> finding (with a hover card from the public API)"
2673
+ ],
2674
+ kind: "extension",
2675
+ name: "extension.lens",
2676
+ operatorNotes: "Fluncle Lens (apps/extension), MV3. PENDING Chrome Web Store review — DARK until approval, then flip `pending` + set the assigned listing URL. The placeholder `url` is the store search for the listing; probeConfig is the post-approval /status check.",
2677
+ pending: true,
2678
+ probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2679
+ url: "https://chromewebstore.google.com/search/Fluncle%20Lens",
2680
+ weights: { web: "secondary" }
2681
+ },
2443
2682
  {
2444
2683
  command: "fluncle admin tracks enrich --queue",
2445
2684
  exposedContent: [
@@ -2596,7 +2835,7 @@ var init_status = __esm(() => {
2596
2835
  init_api();
2597
2836
  serviceLabels = (() => {
2598
2837
  const labels = new Map;
2599
- for (const surface of SURFACES) {
2838
+ for (const surface of liveSurfaces()) {
2600
2839
  const match = surface.operatorNotes?.match(/service `([a-z0-9-]+)`/);
2601
2840
  const id = match?.[1];
2602
2841
  const label = surface.exposedContent[0];
@@ -2820,7 +3059,7 @@ __export(exports_admin_tracks, {
2820
3059
  backfillDiscogsCommand: () => backfillDiscogsCommand
2821
3060
  });
2822
3061
  async function fetchAdminTracks(options) {
2823
- const { hasContext, hasNote, hasObservation, hasVideo, max, order, status } = options;
3062
+ const { hasContext, hasNote, hasObservation, hasVideo, max, order, retryEmptyContext, status } = options;
2824
3063
  const results = [];
2825
3064
  let cursor;
2826
3065
  do {
@@ -2831,6 +3070,9 @@ async function fetchAdminTracks(options) {
2831
3070
  if (hasContext !== undefined) {
2832
3071
  params.set("hasContext", String(hasContext));
2833
3072
  }
3073
+ if (retryEmptyContext) {
3074
+ params.set("retryEmptyContext", "true");
3075
+ }
2834
3076
  if (hasNote !== undefined) {
2835
3077
  params.set("hasNote", String(hasNote));
2836
3078
  }
@@ -2870,8 +3112,8 @@ async function queueCommand(limit, filters = {}) {
2870
3112
  async function enrichQueueCommand(limit) {
2871
3113
  return fetchAdminTracks({ max: limit, order: "asc", status: "queue" });
2872
3114
  }
2873
- async function contextQueueCommand(limit) {
2874
- return fetchAdminTracks({ hasContext: false, max: limit, order: "asc" });
3115
+ async function contextQueueCommand(limit, retryEmptyContext = false) {
3116
+ return fetchAdminTracks({ hasContext: false, max: limit, order: "asc", retryEmptyContext });
2875
3117
  }
2876
3118
  async function observeQueueCommand(limit) {
2877
3119
  return fetchAdminTracks({
@@ -3280,7 +3522,7 @@ __export(exports_newsletter, {
3280
3522
  newsletterListCommand: () => newsletterListCommand,
3281
3523
  newsletterDraftCommand: () => newsletterDraftCommand
3282
3524
  });
3283
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
3525
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
3284
3526
  function buildBody2(options, { requireContent }) {
3285
3527
  const body = {};
3286
3528
  if (options.contentFile !== undefined) {
@@ -3303,7 +3545,7 @@ function readContentFile(filePath) {
3303
3545
  if (!existsSync2(filePath)) {
3304
3546
  throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
3305
3547
  }
3306
- const text = readFileSync2(filePath, "utf-8");
3548
+ const text = readFileSync3(filePath, "utf-8");
3307
3549
  try {
3308
3550
  return JSON.parse(text);
3309
3551
  } catch (error) {
@@ -3937,7 +4179,7 @@ var COORD_FALLBACK2 = "—";
3937
4179
  var init_format2 = () => {};
3938
4180
 
3939
4181
  // src/cli.ts
3940
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
4182
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
3941
4183
  import path2 from "path";
3942
4184
 
3943
4185
  // ../../node_modules/commander/lib/error.js
@@ -6057,6 +6299,7 @@ ${fluncleAsciiLogo}
6057
6299
  `).addHelpText("after", rootHelpSections);
6058
6300
  addListenCommands(program2);
6059
6301
  addShareCommands(program2);
6302
+ addAccountCommands(program2);
6060
6303
  addMetaCommands(program2);
6061
6304
  addTrackCommands(program2);
6062
6305
  addAdminCommands(program2);
@@ -6120,6 +6363,20 @@ function addShareCommands(program2) {
6120
6363
  await submitCommand2(input.join(" ") || undefined);
6121
6364
  });
6122
6365
  }
6366
+ function addAccountCommands(program2) {
6367
+ program2.command("login").description("Link this device to your Fluncle account (sync your Galaxy)").action(async () => {
6368
+ const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), exports_login));
6369
+ await loginCommand2();
6370
+ });
6371
+ program2.command("logout").description("Unlink this device from your account").action(async () => {
6372
+ const { logoutCommand: logoutCommand2 } = await Promise.resolve().then(() => (init_login(), exports_login));
6373
+ await logoutCommand2();
6374
+ });
6375
+ program2.command("me").description("Your account and Galaxy progress (sign in with `fluncle login`)").option("--json", "Print JSON", false).action(async (options) => {
6376
+ const { meCommand: meCommand2 } = await Promise.resolve().then(() => (init_me(), exports_me));
6377
+ await runMe(options, meCommand2);
6378
+ });
6379
+ }
6123
6380
  function addMetaCommands(program2) {
6124
6381
  program2.command("about").description("Fluncle, and where to find him").action(async () => {
6125
6382
  const { aboutCommand: aboutCommand2 } = await Promise.resolve().then(() => (init_about(), exports_about));
@@ -6232,7 +6489,7 @@ function addAdminCommands(program2) {
6232
6489
  const { trackObserveCommand: trackObserveCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
6233
6490
  await runTrackObserve(idOrLogId, options, trackObserveCommand2);
6234
6491
  });
6235
- adminTrack.command("context").description("Gather the field notes for a finding (facts only; observe speaks from them)").argument("[idOrLogId]").option("--queue", "Show the context worklist (findings missing field notes), oldest first", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--query <text>", "Override the fact-search query (else the Worker builds one)").option("--refresh", "Re-run the fetch even if a note exists (backfill/sharpen)", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
6492
+ adminTrack.command("context").description("Gather the field notes for a finding (facts only; observe speaks from them)").argument("[idOrLogId]").option("--queue", "Show the context worklist (findings missing field notes), oldest first", false).option("--retry-empty", "With --queue: also re-pick finds confirmed empty last pass (widen the net)", false).option("--limit <limit>", "Number of findings to show with --queue", "10").option("--query <text>", "Override the fact-search query (else the Worker builds one)").option("--refresh", "Re-run the fetch even if a note exists (backfill/sharpen)", false).option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
6236
6493
  if (options.queue) {
6237
6494
  const { contextQueueCommand: contextQueueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
6238
6495
  await runAdminContextQueue(options, contextQueueCommand2);
@@ -6389,7 +6646,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
6389
6646
  console.log(` mime: ${result.mime}`);
6390
6647
  }
6391
6648
  async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
6392
- const script = options.scriptFile ? readFileSync3(options.scriptFile, "utf8") : options.script;
6649
+ const script = options.scriptFile ? readFileSync4(options.scriptFile, "utf8") : options.script;
6393
6650
  if (!idOrLogId || !script || !script.trim()) {
6394
6651
  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]");
6395
6652
  }
@@ -6445,7 +6702,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
6445
6702
  }
6446
6703
  }
6447
6704
  async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
6448
- const note = options.scriptFile ? readFileSync3(options.scriptFile, "utf8") : options.script;
6705
+ const note = options.scriptFile ? readFileSync4(options.scriptFile, "utf8") : options.script;
6449
6706
  if (!idOrLogId || !note || !note.trim()) {
6450
6707
  throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
6451
6708
  }
@@ -7042,7 +7299,8 @@ async function runAdminQueue(options, queueCommand2) {
7042
7299
  }
7043
7300
  async function runAdminContextQueue(options, contextQueueCommand2) {
7044
7301
  const limit = parseListLimit(options.limit);
7045
- const tracks = await contextQueueCommand2(limit);
7302
+ const retryEmpty = options.retryEmpty === true;
7303
+ const tracks = await contextQueueCommand2(limit, retryEmpty);
7046
7304
  if (options.json) {
7047
7305
  printJson({
7048
7306
  ok: true,
@@ -7056,7 +7314,8 @@ async function runAdminContextQueue(options, contextQueueCommand2) {
7056
7314
  }
7057
7315
  const { trackRows: trackRows2 } = await Promise.resolve().then(() => (init_format2(), exports_format));
7058
7316
  const noun = tracks.length === 1 ? "finding" : "findings";
7059
- console.log(`${tracks.length} ${noun} missing field notes, oldest first:`);
7317
+ const scope = retryEmpty ? " (incl. empty retries)" : "";
7318
+ console.log(`${tracks.length} ${noun} missing field notes${scope}, oldest first:`);
7060
7319
  console.log(trackRows2(tracks).join(`
7061
7320
  `));
7062
7321
  }
@@ -7200,6 +7459,20 @@ async function runStatus(options, statusCommand2) {
7200
7459
  console.log(statusLines2(snapshot).join(`
7201
7460
  `));
7202
7461
  }
7462
+ async function runMe(options, meCommand2) {
7463
+ const me = await meCommand2();
7464
+ if (options.json) {
7465
+ printJson({ ok: true, ...me });
7466
+ return;
7467
+ }
7468
+ console.log(me.name);
7469
+ console.log([
7470
+ `${me.collectedCount} ${me.collectedCount === 1 ? "finding" : "findings"} collected`,
7471
+ `${me.wins} ${me.wins === 1 ? "win" : "wins"}`,
7472
+ `${me.deaths} ${me.deaths === 1 ? "death" : "deaths"}`
7473
+ ].join(" \xB7 "));
7474
+ console.log(`Aboard since ${new Date(me.joinedAt).toLocaleDateString()}`);
7475
+ }
7203
7476
  function rejectUnexpectedPositionals(positionals) {
7204
7477
  if (positionals.length === 0) {
7205
7478
  return;
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.65.0"
34
+ "version": "0.67.0"
35
35
  }