@bobfrankston/rmfmail 1.2.250 → 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/check-bridge-contract.mjs +9 -1
- package/bin/mailx.js +20 -0
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +21 -0
- package/client/app.bundle.js +10 -1
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +16 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +16 -0
- package/client/components/message-viewer.js +13 -2
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +13 -2
- package/client/lib/mailxapi.js +12 -0
- package/package.json +5 -5
- package/packages/mailx-service/ai-usage.d.ts +32 -0
- package/packages/mailx-service/ai-usage.d.ts.map +1 -0
- package/packages/mailx-service/ai-usage.js +98 -0
- package/packages/mailx-service/ai-usage.js.map +1 -0
- package/packages/mailx-service/ai-usage.ts +110 -0
- package/packages/mailx-service/index.d.ts +6 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +64 -2
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +66 -2
- package/packages/mailx-service/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-52956 → node_modules.npmglobalize-stash-85680}/.package-lock.json +0 -0
|
@@ -103,6 +103,7 @@ import { Store } from "@bobfrankston/mailx-store";
|
|
|
103
103
|
import { SyncQueue } from "./sync-queue.js";
|
|
104
104
|
import { Reconciler } from "./reconciler.js";
|
|
105
105
|
import { spawnDbWorker, type SpawnedDbWorker } from "./db-worker-client.js";
|
|
106
|
+
import { priceCall, recordAiUsage, summarizeAiUsage, type AiUsageSummary } from "./ai-usage.js";
|
|
106
107
|
export { spawnSyncWorker, type SpawnedSyncWorker } from "./sync-worker-client.js";
|
|
107
108
|
import { loadSettings, saveSettings, loadAccounts, loadAccountsAsync, saveAccounts, initCloudConfig, loadAllowlist, saveAllowlist, loadAutocomplete, saveAutocomplete, loadKeys, saveKeys, ensureKeysSectionExists, getStorePath, getStorageInfo, getConfigDir, loadUserDict, saveUserDict, loadReminderState, saveReminderState, cloudReadBinary, providerForDomain } from "@bobfrankston/mailx-settings";
|
|
108
109
|
import type { AccountConfig, Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings, AiTransformRequest, AiTransformResponse, ExtractedEvent, MailxApi } from "@bobfrankston/mailx-types";
|
|
@@ -1095,6 +1096,14 @@ export class MailxService implements MailxApi {
|
|
|
1095
1096
|
return { ok, status: displayStatus, statusText };
|
|
1096
1097
|
}
|
|
1097
1098
|
|
|
1099
|
+
/** AI spend since `sinceMs` (default: start of this month). Reads the
|
|
1100
|
+
* per-call log written by aiTransform. `unpricedCalls` are excluded from
|
|
1101
|
+
* `costUsd` — a model with no published price contributes tokens, not a
|
|
1102
|
+
* made-up dollar figure. */
|
|
1103
|
+
getAiUsage(sinceMs?: number): AiUsageSummary {
|
|
1104
|
+
return summarizeAiUsage(sinceMs);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1098
1107
|
/** Fetch a remote image so the WebView can put it on the clipboard.
|
|
1099
1108
|
*
|
|
1100
1109
|
* The client cannot do this itself: an image hosted by a sender is
|
|
@@ -4532,6 +4541,8 @@ Resolve relative dates ("tomorrow", "next Friday") against today's date. If no y
|
|
|
4532
4541
|
// provider-call site so the three branches stay close together.
|
|
4533
4542
|
let rawText = "";
|
|
4534
4543
|
let reason = "";
|
|
4544
|
+
// Token usage for cost accounting, filled in by whichever branch runs.
|
|
4545
|
+
let usage: { input: number; output: number; model: string } | null = null;
|
|
4535
4546
|
try {
|
|
4536
4547
|
if (cfg.provider === "ollama") {
|
|
4537
4548
|
const res = await fetch(`${cfg.ollamaUrl}/api/generate`, {
|
|
@@ -4547,6 +4558,9 @@ Resolve relative dates ("tomorrow", "next Friday") against today's date. If no y
|
|
|
4547
4558
|
if (!res.ok) return { text: "", reason: `ollama ${res.status}` };
|
|
4548
4559
|
const data = await res.json() as any;
|
|
4549
4560
|
rawText = (data.response || "").trim();
|
|
4561
|
+
// Local model — free, but still counted so "$0.00" is measured
|
|
4562
|
+
// rather than merely absent.
|
|
4563
|
+
usage = { input: data.prompt_eval_count || 0, output: data.eval_count || 0, model: cfg.ollamaModel };
|
|
4550
4564
|
} else {
|
|
4551
4565
|
// Cloud providers: read API keys from accounts.jsonc,
|
|
4552
4566
|
// fall back to the legacy preferences.cloudApiKey field.
|
|
@@ -4571,6 +4585,11 @@ Resolve relative dates ("tomorrow", "next Friday") against today's date. If no y
|
|
|
4571
4585
|
if (!res.ok) return { text: "", reason: `claude ${res.status}` };
|
|
4572
4586
|
const data = await res.json() as any;
|
|
4573
4587
|
rawText = (data.content?.[0]?.text || "").trim();
|
|
4588
|
+
usage = {
|
|
4589
|
+
input: data.usage?.input_tokens || 0,
|
|
4590
|
+
output: data.usage?.output_tokens || 0,
|
|
4591
|
+
model: data.model || cfg.cloudModel,
|
|
4592
|
+
};
|
|
4574
4593
|
} else if (cfg.provider === "openai") {
|
|
4575
4594
|
const apiKey = aiKeys.openai || cfg.cloudApiKey;
|
|
4576
4595
|
if (!apiKey) return { text: "", reason: "no OpenAI API key in accounts.jsonc keys" };
|
|
@@ -4592,13 +4611,58 @@ Resolve relative dates ("tomorrow", "next Friday") against today's date. If no y
|
|
|
4592
4611
|
if (!res.ok) return { text: "", reason: `openai ${res.status}` };
|
|
4593
4612
|
const data = await res.json() as any;
|
|
4594
4613
|
rawText = (data.choices?.[0]?.message?.content || "").trim();
|
|
4614
|
+
usage = {
|
|
4615
|
+
input: data.usage?.prompt_tokens || 0,
|
|
4616
|
+
output: data.usage?.completion_tokens || 0,
|
|
4617
|
+
model: data.model || cfg.cloudModel,
|
|
4618
|
+
};
|
|
4595
4619
|
} else {
|
|
4596
4620
|
return { text: "", reason: "no provider matched" };
|
|
4597
4621
|
}
|
|
4598
4622
|
}
|
|
4599
4623
|
} catch (e: any) {
|
|
4600
|
-
|
|
4601
|
-
|
|
4624
|
+
// Node's fetch throws a bare `TypeError: fetch failed` and buries
|
|
4625
|
+
// the actual cause (ECONNREFUSED, ENOTFOUND, cert error) in
|
|
4626
|
+
// `e.cause`. Reporting only the message told Bob nothing — the
|
|
4627
|
+
// real answer was "Ollama isn't running" (2026-08-11). Unwrap it,
|
|
4628
|
+
// and name the endpoint we couldn't reach.
|
|
4629
|
+
const cause = e?.cause?.code || e?.cause?.message || "";
|
|
4630
|
+
const where = cfg.provider === "ollama" ? cfg.ollamaUrl
|
|
4631
|
+
: cfg.provider === "claude" ? "api.anthropic.com"
|
|
4632
|
+
: cfg.provider === "openai" ? "api.openai.com" : cfg.provider;
|
|
4633
|
+
const detail = cause ? `${e.message} (${cause})` : e.message;
|
|
4634
|
+
const reasonText = /fetch failed/i.test(e?.message || "")
|
|
4635
|
+
? `can't reach ${cfg.provider} at ${where} — ${cause || "connection failed"}`
|
|
4636
|
+
: detail;
|
|
4637
|
+
console.error(` [aiTransform] ${cfg.provider} ${req.action} error: ${detail} [${where}]`);
|
|
4638
|
+
return { text: "", reason: reasonText };
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
// Cost accounting. Bob asked to monitor AI spend (2026-08-11); nothing
|
|
4642
|
+
// tracked it before. Recorded per call to ~/.rmfmail/logs/ai-usage.jsonl,
|
|
4643
|
+
// with the month-to-date total on the log line so spend is visible
|
|
4644
|
+
// without opening anything. A local model records $0, not "no data";
|
|
4645
|
+
// a model with no published price records tokens and a null cost
|
|
4646
|
+
// rather than a guessed figure.
|
|
4647
|
+
if (usage) {
|
|
4648
|
+
const costUsd = priceCall(cfg.provider, usage.model, usage.input, usage.output);
|
|
4649
|
+
recordAiUsage({
|
|
4650
|
+
ts: Date.now(),
|
|
4651
|
+
provider: cfg.provider,
|
|
4652
|
+
model: usage.model,
|
|
4653
|
+
action: req.action,
|
|
4654
|
+
inputTokens: usage.input,
|
|
4655
|
+
outputTokens: usage.output,
|
|
4656
|
+
costUsd,
|
|
4657
|
+
});
|
|
4658
|
+
const mtd = summarizeAiUsage();
|
|
4659
|
+
const costText = costUsd == null
|
|
4660
|
+
? "cost unknown (no published price for this model)"
|
|
4661
|
+
: `$${costUsd.toFixed(4)}`;
|
|
4662
|
+
const mtdText = mtd.unpricedCalls > 0
|
|
4663
|
+
? `$${mtd.costUsd.toFixed(2)} + ${mtd.unpricedCalls} unpriced call(s)`
|
|
4664
|
+
: `$${mtd.costUsd.toFixed(2)}`;
|
|
4665
|
+
console.log(` [ai-usage] ${cfg.provider}/${usage.model} ${req.action}: ${usage.input} in + ${usage.output} out = ${costText}; month-to-date ${mtdText} over ${mtd.calls} call(s)`);
|
|
4602
4666
|
}
|
|
4603
4667
|
|
|
4604
4668
|
if (req.action !== "extractEvent") return { text: rawText, reason };
|