@bobfrankston/rmfmail 1.2.251 → 1.2.252

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.
package/bin/mailx.ts CHANGED
@@ -425,6 +425,27 @@ if (hasFlag("register-share") || hasFlag("unregister-share")) {
425
425
  process.exit(r.ok ? 0 : 1);
426
426
  }
427
427
 
428
+ // AI spend report. Reads the per-call log written by aiTransform — no daemon
429
+ // needed, no IPC, so it works whether or not rmfmail is running. Bob asked to
430
+ // monitor AI costs (2026-08-11); this is the surface that answers it.
431
+ if (hasFlag("aiusage") || hasFlag("ai-usage")) {
432
+ const { summarizeAiUsage } = await import("@bobfrankston/mailx-service/ai-usage.js");
433
+ const all = summarizeAiUsage(0);
434
+ const mtd = summarizeAiUsage();
435
+ const fmt = (s: typeof all, label: string): void => {
436
+ console.log(`\n${label}: ${s.calls} call(s), ${s.inputTokens} input + ${s.outputTokens} output tokens`);
437
+ console.log(` cost: $${s.costUsd.toFixed(4)}${s.unpricedCalls ? ` (+ ${s.unpricedCalls} call(s) with no published price — tokens counted, cost not)` : ""}`);
438
+ for (const [model, m] of Object.entries(s.byModel)) {
439
+ console.log(` ${model}: ${m.calls} call(s), ${m.inputTokens} in / ${m.outputTokens} out, $${m.costUsd.toFixed(4)}`);
440
+ }
441
+ };
442
+ fmt(mtd, "This month");
443
+ fmt(all, "All time");
444
+ console.log("\nLocal models (ollama) run on this machine and cost $0 — they are counted so the total is measured, not assumed.");
445
+ console.log(`Per-call log: ${path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail", "logs", "ai-usage.jsonl")}`);
446
+ process.exit(0);
447
+ }
448
+
428
449
  // Read our own version once — used for the instance file + upgrade check below.
429
450
  const __selfRoot = path.join(import.meta.dirname, "..");
430
451
  const __selfVersion: string = (() => {
@@ -2066,7 +2066,10 @@ async function translateAndShow(text) {
2066
2066
  if (status)
2067
2067
  status.textContent = "";
2068
2068
  } else {
2069
- body.innerHTML = `<div style="color:var(--muted, #888);">No result.</div><div style="margin-top:8px;font-size:12px;color:var(--muted, #888);">${r.reason || ""}</div><div style="margin-top:14px;font-size:12px;">Enable AI translate in Settings \u2192 AI features (off by default).</div>`;
2069
+ const reason = r.reason || "no result";
2070
+ const isDisabled = /disabled in settings|not configured/i.test(reason);
2071
+ const hint = isDisabled ? "Enable AI translate in Settings \u2192 AI features (off by default)." : /can't reach/i.test(reason) ? "The AI provider isn't answering. Check Settings \u2192 AI features, or start the local model server." : "";
2072
+ body.innerHTML = `<div style="color:var(--muted, #888);">No result.</div><div style="margin-top:8px;font-size:12px;color:var(--muted, #888);">${escapeHtml3(reason)}</div>` + (hint ? `<div style="margin-top:14px;font-size:12px;">${hint}</div>` : "");
2070
2073
  if (status)
2071
2074
  status.textContent = `Translate: ${r.reason || "no result"}`;
2072
2075
  }
@@ -11957,6 +11960,12 @@ function closeToolbarDropdowns() {
11957
11960
  if (dd && !dd.hidden) hideDropdownHard(dd);
11958
11961
  }
11959
11962
  }
11963
+ window.addEventListener("message", (e) => {
11964
+ if (e.data?.type !== "iframePointerDown") return;
11965
+ closeToolbarDropdowns();
11966
+ document.getElementById("rail-menu-backdrop")?.remove();
11967
+ refreshToolbarOverlayShield();
11968
+ });
11960
11969
  function refreshToolbarOverlayShield() {
11961
11970
  const dropdownOpen = ["settings-dropdown", "view-dropdown", "restart-dropdown"].some((id) => {
11962
11971
  const el = document.getElementById(id);