@bobfrankston/rmfmail 1.0.524 → 1.0.526
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/client/components/calendar-sidebar.js +144 -32
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +140 -25
- package/client/components/message-list.js +19 -2
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +17 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +6 -1
- package/package.json +3 -3
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +108 -51
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +102 -49
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-types/index.d.ts +27 -3
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +28 -3
- package/packages/mailx-types/package.json +1 -1
|
@@ -2712,6 +2712,10 @@ export class MailxService {
|
|
|
2712
2712
|
translate: cfg.translateEnabled,
|
|
2713
2713
|
proofread: cfg.proofreadEnabled,
|
|
2714
2714
|
summarize: cfg.proofreadEnabled, // bundled with proofread for now
|
|
2715
|
+
// extractEvent is gated on the same flag so the user has one
|
|
2716
|
+
// toggle for "AI features that send your text to the cloud".
|
|
2717
|
+
// Local-Ollama users get it for free without changing the flag.
|
|
2718
|
+
extractEvent: cfg.proofreadEnabled,
|
|
2715
2719
|
};
|
|
2716
2720
|
if (!featureGate[req.action])
|
|
2717
2721
|
return { text: "", reason: `AI ${req.action} disabled in settings` };
|
|
@@ -2719,6 +2723,7 @@ export class MailxService {
|
|
|
2719
2723
|
if (!text.trim())
|
|
2720
2724
|
return { text: "", reason: "no input" };
|
|
2721
2725
|
const target = req.targetLang || "en";
|
|
2726
|
+
const nowISO = req.nowISO || new Date().toISOString();
|
|
2722
2727
|
let systemPrompt;
|
|
2723
2728
|
let userPrompt;
|
|
2724
2729
|
switch (req.action) {
|
|
@@ -2734,7 +2739,27 @@ export class MailxService {
|
|
|
2734
2739
|
systemPrompt = `You are a summarizer. Render the user's text as a short paragraph (2-4 sentences). Output ONLY the summary.`;
|
|
2735
2740
|
userPrompt = text;
|
|
2736
2741
|
break;
|
|
2742
|
+
case "extractEvent":
|
|
2743
|
+
// Strict JSON-only response — anything else makes the client
|
|
2744
|
+
// throw on JSON.parse. Today's date is supplied so the model
|
|
2745
|
+
// can resolve "tomorrow"/"next Tuesday" consistently with the
|
|
2746
|
+
// user's wall clock (the service may not share it).
|
|
2747
|
+
systemPrompt = `You parse natural-language event descriptions into structured calendar events. Today is ${nowISO.slice(0, 10)} (current time ${nowISO}). Output ONLY a JSON object with these fields, no prose, no markdown fences:
|
|
2748
|
+
- title (string, required) — short event name
|
|
2749
|
+
- startISO (string) — ISO 8601 local time, e.g. "2026-05-06T12:00:00". Omit if no time mentioned.
|
|
2750
|
+
- endISO (string) — ISO 8601 local time. If duration is unstated, default to 1 hour after start.
|
|
2751
|
+
- allDay (boolean) — true when no time-of-day was given.
|
|
2752
|
+
- location (string) — venue / address. Omit if absent.
|
|
2753
|
+
- notes (string) — extra detail not captured in title. Omit if absent.
|
|
2754
|
+
Resolve relative dates ("tomorrow", "next Friday") against today's date. If no year is given, use the next future occurrence. If only a date is given (no time), set allDay=true and omit endISO.`;
|
|
2755
|
+
userPrompt = text;
|
|
2756
|
+
break;
|
|
2737
2757
|
}
|
|
2758
|
+
// Get the raw text from the active provider, then post-process for
|
|
2759
|
+
// structured-output actions (extractEvent → JSON.parse). Single
|
|
2760
|
+
// provider-call site so the three branches stay close together.
|
|
2761
|
+
let rawText = "";
|
|
2762
|
+
let reason = "";
|
|
2738
2763
|
try {
|
|
2739
2764
|
if (cfg.provider === "ollama") {
|
|
2740
2765
|
const res = await fetch(`${cfg.ollamaUrl}/api/generate`, {
|
|
@@ -2750,64 +2775,96 @@ export class MailxService {
|
|
|
2750
2775
|
if (!res.ok)
|
|
2751
2776
|
return { text: "", reason: `ollama ${res.status}` };
|
|
2752
2777
|
const data = await res.json();
|
|
2753
|
-
|
|
2754
|
-
}
|
|
2755
|
-
// Same migration as autocomplete: read keys from accounts.jsonc,
|
|
2756
|
-
// fall back to legacy preferences.cloudApiKey.
|
|
2757
|
-
const aiKeys = loadKeys();
|
|
2758
|
-
if (cfg.provider === "claude") {
|
|
2759
|
-
const apiKey = aiKeys.anthropic || cfg.cloudApiKey;
|
|
2760
|
-
if (!apiKey)
|
|
2761
|
-
return { text: "", reason: "no Anthropic API key in accounts.jsonc keys" };
|
|
2762
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
2763
|
-
method: "POST",
|
|
2764
|
-
headers: {
|
|
2765
|
-
"Content-Type": "application/json",
|
|
2766
|
-
"x-api-key": apiKey,
|
|
2767
|
-
"anthropic-version": "2023-06-01",
|
|
2768
|
-
},
|
|
2769
|
-
body: JSON.stringify({
|
|
2770
|
-
model: cfg.cloudModel,
|
|
2771
|
-
max_tokens: 2048,
|
|
2772
|
-
system: systemPrompt,
|
|
2773
|
-
messages: [{ role: "user", content: userPrompt }],
|
|
2774
|
-
}),
|
|
2775
|
-
});
|
|
2776
|
-
if (!res.ok)
|
|
2777
|
-
return { text: "", reason: `claude ${res.status}` };
|
|
2778
|
-
const data = await res.json();
|
|
2779
|
-
return { text: (data.content?.[0]?.text || "").trim() };
|
|
2778
|
+
rawText = (data.response || "").trim();
|
|
2780
2779
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
"
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2780
|
+
else {
|
|
2781
|
+
// Cloud providers: read API keys from accounts.jsonc,
|
|
2782
|
+
// fall back to the legacy preferences.cloudApiKey field.
|
|
2783
|
+
const aiKeys = loadKeys();
|
|
2784
|
+
if (cfg.provider === "claude") {
|
|
2785
|
+
const apiKey = aiKeys.anthropic || cfg.cloudApiKey;
|
|
2786
|
+
if (!apiKey)
|
|
2787
|
+
return { text: "", reason: "no Anthropic API key in accounts.jsonc keys" };
|
|
2788
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
2789
|
+
method: "POST",
|
|
2790
|
+
headers: {
|
|
2791
|
+
"Content-Type": "application/json",
|
|
2792
|
+
"x-api-key": apiKey,
|
|
2793
|
+
"anthropic-version": "2023-06-01",
|
|
2794
|
+
},
|
|
2795
|
+
body: JSON.stringify({
|
|
2796
|
+
model: cfg.cloudModel,
|
|
2797
|
+
max_tokens: 2048,
|
|
2798
|
+
system: systemPrompt,
|
|
2799
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
2800
|
+
}),
|
|
2801
|
+
});
|
|
2802
|
+
if (!res.ok)
|
|
2803
|
+
return { text: "", reason: `claude ${res.status}` };
|
|
2804
|
+
const data = await res.json();
|
|
2805
|
+
rawText = (data.content?.[0]?.text || "").trim();
|
|
2806
|
+
}
|
|
2807
|
+
else if (cfg.provider === "openai") {
|
|
2808
|
+
const apiKey = aiKeys.openai || cfg.cloudApiKey;
|
|
2809
|
+
if (!apiKey)
|
|
2810
|
+
return { text: "", reason: "no OpenAI API key in accounts.jsonc keys" };
|
|
2811
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
2812
|
+
method: "POST",
|
|
2813
|
+
headers: {
|
|
2814
|
+
"Content-Type": "application/json",
|
|
2815
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
2816
|
+
},
|
|
2817
|
+
body: JSON.stringify({
|
|
2818
|
+
model: cfg.cloudModel,
|
|
2819
|
+
max_tokens: 2048,
|
|
2820
|
+
messages: [
|
|
2821
|
+
{ role: "system", content: systemPrompt },
|
|
2822
|
+
{ role: "user", content: userPrompt },
|
|
2823
|
+
],
|
|
2824
|
+
}),
|
|
2825
|
+
});
|
|
2826
|
+
if (!res.ok)
|
|
2827
|
+
return { text: "", reason: `openai ${res.status}` };
|
|
2828
|
+
const data = await res.json();
|
|
2829
|
+
rawText = (data.choices?.[0]?.message?.content || "").trim();
|
|
2830
|
+
}
|
|
2831
|
+
else {
|
|
2832
|
+
return { text: "", reason: "no provider matched" };
|
|
2833
|
+
}
|
|
2804
2834
|
}
|
|
2805
2835
|
}
|
|
2806
2836
|
catch (e) {
|
|
2807
2837
|
console.error(` [aiTransform] ${cfg.provider} ${req.action} error: ${e.message}`);
|
|
2808
2838
|
return { text: "", reason: e.message };
|
|
2809
2839
|
}
|
|
2810
|
-
|
|
2840
|
+
if (req.action !== "extractEvent")
|
|
2841
|
+
return { text: rawText, reason };
|
|
2842
|
+
// extractEvent post-processing: pull a JSON object out of rawText.
|
|
2843
|
+
// Models sometimes wrap output in ```json fences or add a leading
|
|
2844
|
+
// sentence even when told not to. Locate the first { ... } block
|
|
2845
|
+
// and parse that. Empty event with reason "no JSON" lets the UI
|
|
2846
|
+
// fall back to "use the raw description as the event title".
|
|
2847
|
+
const match = rawText.match(/\{[\s\S]*\}/);
|
|
2848
|
+
if (!match)
|
|
2849
|
+
return { text: rawText, reason: "AI did not return JSON" };
|
|
2850
|
+
try {
|
|
2851
|
+
const parsed = JSON.parse(match[0]);
|
|
2852
|
+
if (typeof parsed?.title !== "string" || !parsed.title.trim()) {
|
|
2853
|
+
return { text: rawText, reason: "AI JSON missing title" };
|
|
2854
|
+
}
|
|
2855
|
+
const ev = {
|
|
2856
|
+
title: parsed.title.trim(),
|
|
2857
|
+
startISO: typeof parsed.startISO === "string" ? parsed.startISO : undefined,
|
|
2858
|
+
endISO: typeof parsed.endISO === "string" ? parsed.endISO : undefined,
|
|
2859
|
+
allDay: typeof parsed.allDay === "boolean" ? parsed.allDay : undefined,
|
|
2860
|
+
location: typeof parsed.location === "string" ? parsed.location : undefined,
|
|
2861
|
+
notes: typeof parsed.notes === "string" ? parsed.notes : undefined,
|
|
2862
|
+
};
|
|
2863
|
+
return { text: JSON.stringify(ev), event: ev };
|
|
2864
|
+
}
|
|
2865
|
+
catch (e) {
|
|
2866
|
+
return { text: rawText, reason: `AI JSON parse failed: ${e.message}` };
|
|
2867
|
+
}
|
|
2811
2868
|
}
|
|
2812
2869
|
}
|
|
2813
2870
|
/** Trim suggestion: remove leading/trailing whitespace, cap at sentence boundary */
|