@bobfrankston/mailx-settings 0.1.6 → 0.1.9
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/cloud.d.ts +29 -4
- package/cloud.d.ts.map +1 -0
- package/cloud.js +69 -49
- package/copy-docs.js +31 -0
- package/docs/accounts.md +46 -0
- package/docs/allowlist.md +27 -0
- package/docs/clients.md +24 -0
- package/docs/config.md +32 -0
- package/docs/contact-rules.md +40 -0
- package/docs/contacts.md +48 -0
- package/docs/editor.md +92 -0
- package/docs/preferences.md +43 -0
- package/index.d.ts +52 -5
- package/index.d.ts.map +1 -0
- package/index.js +365 -43
- package/package.json +13 -3
- package/tsconfig.tsbuildinfo +0 -1
package/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The old settings.jsonc is still supported for backward compatibility.
|
|
17
17
|
*/
|
|
18
|
-
import type { MailxSettings, AccountConfig, AutocompleteSettings } from "@bobfrankston/mailx-types";
|
|
18
|
+
import type { MailxSettings, AccountConfig, AutocompleteSettings, AiKeys } from "@bobfrankston/mailx-types";
|
|
19
19
|
declare const LOCAL_DIR: string;
|
|
20
20
|
/** Subscribers notified whenever lastCloudError changes (push to UI immediately). */
|
|
21
21
|
type CloudErrorListener = (error: string | null, context?: {
|
|
@@ -38,6 +38,9 @@ export declare function getStorageInfo(): {
|
|
|
38
38
|
provider: string;
|
|
39
39
|
mode: "mount" | "api" | "local";
|
|
40
40
|
cloudPath?: string;
|
|
41
|
+
folderName?: string;
|
|
42
|
+
folderId?: string;
|
|
43
|
+
configDir?: string;
|
|
41
44
|
cloudError?: string;
|
|
42
45
|
};
|
|
43
46
|
/** Fill in provider defaults for an account based on email domain.
|
|
@@ -78,7 +81,11 @@ declare const DEFAULT_ALLOWLIST: {
|
|
|
78
81
|
};
|
|
79
82
|
/** Load account configs */
|
|
80
83
|
export declare function loadAccounts(): AccountConfig[];
|
|
81
|
-
/** Load accounts
|
|
84
|
+
/** Load accounts, preferring the cloud copy when cloud is configured.
|
|
85
|
+
* The local file in `~/.rmfmail/` is an offline cache, NOT the source of
|
|
86
|
+
* truth — earlier versions returned the local copy whenever it was non-empty,
|
|
87
|
+
* which left the desktop reading a stale single-account file forever after
|
|
88
|
+
* the GDrive folder lookup got fixed. */
|
|
82
89
|
export declare function loadAccountsAsync(): Promise<AccountConfig[]>;
|
|
83
90
|
/** Strip default-valued fields from a normalized AccountConfig so the
|
|
84
91
|
* serialized JSONC stays compact and human-editable. The previous version
|
|
@@ -100,8 +107,32 @@ export declare function denormalizeAccount(acct: AccountConfig, globalName?: str
|
|
|
100
107
|
/** Save account configs */
|
|
101
108
|
/** Save accounts — merges with cloud copy by email (multi-client safe).
|
|
102
109
|
* Writes the lean form via denormalizeAccount so accounts.jsonc stays
|
|
103
|
-
* compact and human-editable.
|
|
110
|
+
* compact and human-editable. Preserves the top-level `keys` field
|
|
111
|
+
* (AI API keys) if present in the existing cloud or local file. */
|
|
104
112
|
export declare function saveAccounts(accounts: AccountConfig[]): Promise<void>;
|
|
113
|
+
/** Load top-level AI keys from accounts.jsonc. Returns empty placeholders
|
|
114
|
+
* ({ anthropic: "", openai: "" }) when the file lacks a `keys` section,
|
|
115
|
+
* so callers can call this unconditionally without missing-section guards.
|
|
116
|
+
* Reads from the same shared/local path resolution as loadAccounts. */
|
|
117
|
+
export declare function loadKeys(): AiKeys;
|
|
118
|
+
/** Save AI keys back to accounts.jsonc, preserving the rest of the file
|
|
119
|
+
* (accounts list, file-level name). Used by Settings → AI to persist a
|
|
120
|
+
* key the user just entered. Cloud-synced via saveFile. */
|
|
121
|
+
export declare function saveKeys(keys: AiKeys): Promise<void>;
|
|
122
|
+
/** First-run helper: ensure `keys: { anthropic: "", openai: "" }` is
|
|
123
|
+
* materialized in accounts.jsonc so the user sees placeholders for every
|
|
124
|
+
* supported provider when they open the config editor. Adds missing
|
|
125
|
+
* fields without disturbing existing values, so `keys: {}` becomes
|
|
126
|
+
* `keys: { anthropic: "", openai: "" }` and `keys: { anthropic: "sk-..." }`
|
|
127
|
+
* becomes `keys: { anthropic: "sk-...", openai: "" }`. Idempotent. */
|
|
128
|
+
export declare function ensureKeysSectionExists(): Promise<void>;
|
|
129
|
+
/** First-run helper: materialize the `autocomplete` block in
|
|
130
|
+
* preferences.jsonc with explicit defaults. loadPreferences() merges
|
|
131
|
+
* defaults at read time, but the file on disk often has no
|
|
132
|
+
* `autocomplete` key, so a user opening Settings → Edit config files
|
|
133
|
+
* doesn't see the AI provider knobs. This writes the defaults out so
|
|
134
|
+
* the user can edit them. Idempotent. */
|
|
135
|
+
export declare function ensureAutocompletePrefsExist(): Promise<void>;
|
|
105
136
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
106
137
|
export declare function loadPreferences(): typeof DEFAULT_PREFERENCES;
|
|
107
138
|
/** Save preferences */
|
|
@@ -127,8 +158,8 @@ export { getSharedDir };
|
|
|
127
158
|
/** Initialize local config if it doesn't exist */
|
|
128
159
|
export declare function initLocalConfig(sharedDir?: string, storePath?: string): void;
|
|
129
160
|
/** Initialize config with Google Drive cloud storage.
|
|
130
|
-
* Finds or creates the app-owned "
|
|
131
|
-
* No mount scanning — API only. Existing settings at other paths (e.g., home/.
|
|
161
|
+
* Finds or creates the app-owned ".rmfmail" folder via Drive API and stores its ID.
|
|
162
|
+
* No mount scanning — API only. Existing settings at other paths (e.g., home/.rmfmail
|
|
132
163
|
* from Desktop sync) must be migrated manually or via config.jsonc importPath. */
|
|
133
164
|
export declare function initCloudConfig(provider?: "gdrive"): Promise<void>;
|
|
134
165
|
declare const DEFAULT_SETTINGS: MailxSettings;
|
|
@@ -137,4 +168,20 @@ export declare function getHistoryDays(accountId?: string): number;
|
|
|
137
168
|
/** Get prefetch setting: download bodies during sync (default true) */
|
|
138
169
|
export declare function getPrefetch(): boolean;
|
|
139
170
|
export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
|
|
171
|
+
/** Deploy the app-owned `.md` reference docs to the shared cloud folder so
|
|
172
|
+
* users can read them next to the `.jsonc` files they document. The .md
|
|
173
|
+
* files live in the app bundle (`<workspace>/app/docs/` in dev, or
|
|
174
|
+
* `<package>/docs/` in published builds) and are overwritten on every
|
|
175
|
+
* release — the file header documents that fact, so users know not to
|
|
176
|
+
* edit them.
|
|
177
|
+
*
|
|
178
|
+
* Skip-path: a manifest file `.docs-version` on the cloud records the
|
|
179
|
+
* app version that last deployed. We re-deploy only when the running
|
|
180
|
+
* app's version differs, so startup doesn't pound Drive on every launch.
|
|
181
|
+
*
|
|
182
|
+
* This function is fire-and-forget at the call site; failures are logged
|
|
183
|
+
* but never throw. The `.md` files are reference material, not load-
|
|
184
|
+
* bearing — the app works without them.
|
|
185
|
+
*/
|
|
186
|
+
export declare function deployDocs(appVersion: string): Promise<void>;
|
|
140
187
|
//# sourceMappingURL=index.d.ts.map
|
package/index.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
package/index.js
CHANGED
|
@@ -18,9 +18,131 @@
|
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
21
|
-
import { getCloudProvider, gDriveFindOrCreateFolder } from "./cloud.js";
|
|
21
|
+
import { getCloudProvider, gDriveFindOrCreateFolder, gDriveValidateCachedFolder } from "./cloud.js";
|
|
22
|
+
const __dirname = import.meta.dirname;
|
|
22
23
|
// ── Paths ──
|
|
23
|
-
|
|
24
|
+
// REMOVE 2026-06-15 — one-shot migration of legacy ~/.mailx → ~/.rmfmail.
|
|
25
|
+
// Five cases handled (idempotent — running twice is safe):
|
|
26
|
+
// 1. only ~/.mailx: rename to ~/.rmfmail (atomic single move).
|
|
27
|
+
// 2. ~/.mailx contains ONLY a `mailxstore/` subdir while ~/.rmfmail is
|
|
28
|
+
// already the active config — move the body store across, rewrite
|
|
29
|
+
// body_path rows in the DB, then remove the empty ~/.mailx. This is
|
|
30
|
+
// the hybrid-state user (their config jumped to .rmfmail but the
|
|
31
|
+
// stored body files lived under .mailx because storePath was an
|
|
32
|
+
// absolute path).
|
|
33
|
+
// 3. both exist with other content in ~/.mailx: rename to ~/.mailx-old
|
|
34
|
+
// so it's visibly cruft.
|
|
35
|
+
// 4. only ~/.rmfmail (or neither): do nothing.
|
|
36
|
+
// 5 covers config.jsonc with absolute storePath pointing at ~/.mailx —
|
|
37
|
+
// rewritten to a relative "mailxstore" so the path follows the config dir.
|
|
38
|
+
{
|
|
39
|
+
const home = process.env.USERPROFILE || process.env.HOME || ".";
|
|
40
|
+
const oldDir = path.join(home, ".mailx");
|
|
41
|
+
const newDir = path.join(home, ".rmfmail");
|
|
42
|
+
const asideDir = path.join(home, ".mailx-old");
|
|
43
|
+
if (!fs.existsSync(newDir) && fs.existsSync(oldDir)) {
|
|
44
|
+
fs.renameSync(oldDir, newDir);
|
|
45
|
+
console.log("[migrate] ~/.mailx → ~/.rmfmail");
|
|
46
|
+
}
|
|
47
|
+
else if (fs.existsSync(newDir) && fs.existsSync(oldDir)) {
|
|
48
|
+
// Hybrid: figure out whether `.mailx/` is just a leftover store or
|
|
49
|
+
// has meaningful content.
|
|
50
|
+
let entries = [];
|
|
51
|
+
try {
|
|
52
|
+
entries = fs.readdirSync(oldDir);
|
|
53
|
+
}
|
|
54
|
+
catch { /* */ }
|
|
55
|
+
const onlyStore = entries.length === 1 && entries[0] === "mailxstore";
|
|
56
|
+
if (onlyStore) {
|
|
57
|
+
const oldStore = path.join(oldDir, "mailxstore");
|
|
58
|
+
const newStore = path.join(newDir, "mailxstore");
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(newStore, { recursive: true });
|
|
61
|
+
// Recursive move (rename per top-level entry; rename across
|
|
62
|
+
// dirs on the same volume is atomic, fall back to copy+unlink
|
|
63
|
+
// if it fails — e.g. when the volumes differ).
|
|
64
|
+
const moveRecursive = (src, dst) => {
|
|
65
|
+
const stat = fs.statSync(src);
|
|
66
|
+
if (stat.isDirectory()) {
|
|
67
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
68
|
+
for (const child of fs.readdirSync(src))
|
|
69
|
+
moveRecursive(path.join(src, child), path.join(dst, child));
|
|
70
|
+
try {
|
|
71
|
+
fs.rmdirSync(src);
|
|
72
|
+
}
|
|
73
|
+
catch { /* */ }
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
try {
|
|
77
|
+
fs.renameSync(src, dst);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
fs.copyFileSync(src, dst);
|
|
81
|
+
fs.unlinkSync(src);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
for (const child of fs.readdirSync(oldStore)) {
|
|
86
|
+
moveRecursive(path.join(oldStore, child), path.join(newStore, child));
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
fs.rmdirSync(oldStore);
|
|
90
|
+
}
|
|
91
|
+
catch { /* */ }
|
|
92
|
+
try {
|
|
93
|
+
fs.rmdirSync(oldDir);
|
|
94
|
+
}
|
|
95
|
+
catch { /* may still have files; let next case handle */ }
|
|
96
|
+
console.log("[migrate] moved ~/.mailx/mailxstore/* → ~/.rmfmail/mailxstore/");
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn(`[migrate] mailxstore move failed: ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (!fs.existsSync(asideDir)) {
|
|
103
|
+
fs.renameSync(oldDir, asideDir);
|
|
104
|
+
console.log("[migrate] ~/.mailx → ~/.mailx-old (safe to delete)");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Body-path rewrite: any DB rows still pointing at the old absolute
|
|
108
|
+
// store path get translated to the new one. Fire-and-forget — the DB
|
|
109
|
+
// module exposes a method called from the daemon's startup; this
|
|
110
|
+
// module-level migration just touches config.jsonc.
|
|
111
|
+
const localCfg = path.join(newDir, "config.jsonc");
|
|
112
|
+
if (fs.existsSync(localCfg)) {
|
|
113
|
+
try {
|
|
114
|
+
const raw = fs.readFileSync(localCfg, "utf-8");
|
|
115
|
+
const stripped = raw.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
|
|
116
|
+
const cfg = JSON.parse(stripped);
|
|
117
|
+
// Drop storePath entirely when it points at the legacy dir or
|
|
118
|
+
// is the default — config.jsonc shouldn't pin paths that follow
|
|
119
|
+
// from where LOCAL_DIR is. Users who actually need a custom
|
|
120
|
+
// store location keep that override; everyone else gets the
|
|
121
|
+
// default (~/.rmfmail/mailxstore) without ceremony.
|
|
122
|
+
if (typeof cfg.storePath === "string") {
|
|
123
|
+
const p = cfg.storePath;
|
|
124
|
+
const isLegacyAbs = /[\\/]\.mailx[\\/]mailxstore$/i.test(p);
|
|
125
|
+
const isDefaultRel = p === "mailxstore";
|
|
126
|
+
const isDefaultAbs = /[\\/]\.rmfmail[\\/]mailxstore$/i.test(p);
|
|
127
|
+
if (isLegacyAbs || isDefaultRel || isDefaultAbs) {
|
|
128
|
+
delete cfg.storePath;
|
|
129
|
+
fs.writeFileSync(localCfg, JSON.stringify(cfg, null, 2));
|
|
130
|
+
console.log("[migrate] config.jsonc storePath dropped (using default ~/.rmfmail/mailxstore)");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Same for sharedDir.path label (cosmetic — folderId is what matters).
|
|
134
|
+
if (cfg.sharedDir && typeof cfg.sharedDir === "object" && cfg.sharedDir.path === "home/.mailx") {
|
|
135
|
+
cfg.sharedDir.path = "home/.rmfmail";
|
|
136
|
+
fs.writeFileSync(localCfg, JSON.stringify(cfg, null, 2));
|
|
137
|
+
console.log("[migrate] config.jsonc sharedDir.path → home/.rmfmail");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
console.warn(`[migrate] config.jsonc rewrite failed: ${e.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const LOCAL_DIR = path.join(process.env.USERPROFILE || process.env.HOME || ".", ".rmfmail");
|
|
24
146
|
const LOCAL_CONFIG_PATH = path.join(LOCAL_DIR, "config.jsonc");
|
|
25
147
|
const LEGACY_CONFIG_PATH = path.join(LOCAL_DIR, "config.json");
|
|
26
148
|
const DEFAULT_STORE_PATH = path.join(LOCAL_DIR, "mailxstore");
|
|
@@ -107,10 +229,25 @@ function getSharedDir() {
|
|
|
107
229
|
// Legacy settingsPath no longer used for shared dir — use loadLegacySettings() for reading only.
|
|
108
230
|
return LOCAL_DIR;
|
|
109
231
|
}
|
|
232
|
+
/** Once-per-process validation flag — verify the cached folderId points at
|
|
233
|
+
* an `.rmfmail` folder before trusting it. Catches the cross-version-cache
|
|
234
|
+
* corruption case where 1.0.504 cached a wrong ID and never re-discovered. */
|
|
235
|
+
let folderIdValidated = false;
|
|
110
236
|
/** Read a file via cloud API (when filesystem mount not available) */
|
|
111
237
|
export async function cloudRead(filename) {
|
|
112
238
|
if (!pendingCloudConfig)
|
|
113
239
|
return null;
|
|
240
|
+
// Validate cached folderId once per process. If it points somewhere
|
|
241
|
+
// other than an `.rmfmail` folder (e.g. an orphan from a buggy version),
|
|
242
|
+
// drop it and re-discover via gDriveFindOrCreateFolder.
|
|
243
|
+
if (pendingCloudConfig.folderId && !folderIdValidated) {
|
|
244
|
+
folderIdValidated = true;
|
|
245
|
+
const ok = await gDriveValidateCachedFolder(pendingCloudConfig.folderId);
|
|
246
|
+
if (!ok) {
|
|
247
|
+
pendingCloudConfig.folderId = undefined;
|
|
248
|
+
clearFolderIdInConfig();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
114
251
|
// Ensure we have a folder ID
|
|
115
252
|
if (!pendingCloudConfig.folderId) {
|
|
116
253
|
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
@@ -187,12 +324,25 @@ function saveFolderIdToConfig(folderId) {
|
|
|
187
324
|
}
|
|
188
325
|
catch { /* non-critical */ }
|
|
189
326
|
}
|
|
327
|
+
/** Drop a stale folder ID from config.jsonc so the next cloudRead/cloudWrite
|
|
328
|
+
* re-discovers via gDriveFindOrCreateFolder. */
|
|
329
|
+
function clearFolderIdInConfig() {
|
|
330
|
+
try {
|
|
331
|
+
const config = readLocalConfig();
|
|
332
|
+
if (config.sharedDir && typeof config.sharedDir === "object" && !Array.isArray(config.sharedDir)) {
|
|
333
|
+
delete config.sharedDir.folderId;
|
|
334
|
+
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch { /* non-critical */ }
|
|
338
|
+
}
|
|
190
339
|
/** Whether cloud API fallback is active */
|
|
191
340
|
export function isCloudMode() {
|
|
192
341
|
return pendingCloudConfig !== null;
|
|
193
342
|
}
|
|
194
343
|
/** Get storage provider info for display */
|
|
195
344
|
export function getStorageInfo() {
|
|
345
|
+
const configDir = LOCAL_DIR;
|
|
196
346
|
const config = readLocalConfig();
|
|
197
347
|
if (config.sharedDir) {
|
|
198
348
|
const entries = Array.isArray(config.sharedDir) ? config.sharedDir : [config.sharedDir];
|
|
@@ -200,7 +350,7 @@ export function getStorageInfo() {
|
|
|
200
350
|
if (typeof entry === "string") {
|
|
201
351
|
const resolved = resolveSharedEntry(entry);
|
|
202
352
|
if (resolved && resolved !== LOCAL_DIR) {
|
|
203
|
-
return { provider: "local", mode: "local", cloudPath: resolved };
|
|
353
|
+
return { provider: "local", mode: "local", cloudPath: resolved, configDir };
|
|
204
354
|
}
|
|
205
355
|
continue;
|
|
206
356
|
}
|
|
@@ -208,20 +358,20 @@ export function getStorageInfo() {
|
|
|
208
358
|
const name = (entry.provider === "gdrive" || entry.provider === "google") ? "gdrive" : entry.provider;
|
|
209
359
|
if (entry.folderId) {
|
|
210
360
|
// Has folder ID → API mode (don't scan filesystem for mounts)
|
|
211
|
-
return { provider: name, mode: "api", cloudPath: entry.path, cloudError: lastCloudError || undefined };
|
|
361
|
+
return { provider: name, mode: "api", cloudPath: entry.path, folderName: entry.path, folderId: entry.folderId, configDir, cloudError: lastCloudError || undefined };
|
|
212
362
|
}
|
|
213
363
|
const resolved = resolveSharedEntry(entry);
|
|
214
364
|
if (resolved && resolved !== LOCAL_DIR) {
|
|
215
|
-
return { provider: name, mode: "mount", cloudPath: entry.path };
|
|
365
|
+
return { provider: name, mode: "mount", cloudPath: entry.path, folderName: entry.path, configDir };
|
|
216
366
|
}
|
|
217
367
|
}
|
|
218
368
|
// Not mounted and no folderId — check pendingCloudConfig from initCloudConfig()
|
|
219
369
|
if (pendingCloudConfig) {
|
|
220
370
|
const name = (pendingCloudConfig.provider === "gdrive" || pendingCloudConfig.provider === "google") ? "gdrive" : pendingCloudConfig.provider;
|
|
221
|
-
return { provider: name, mode: "api", cloudPath: pendingCloudConfig.path, cloudError: lastCloudError || undefined };
|
|
371
|
+
return { provider: name, mode: "api", cloudPath: pendingCloudConfig.path, folderName: pendingCloudConfig.path, configDir, cloudError: lastCloudError || undefined };
|
|
222
372
|
}
|
|
223
373
|
}
|
|
224
|
-
return { provider: "local", mode: "local" };
|
|
374
|
+
return { provider: "local", mode: "local", configDir };
|
|
225
375
|
}
|
|
226
376
|
// ── File helpers ──
|
|
227
377
|
/** Read JSON or JSONC file. If exact path not found, tries .json/.jsonc variant. */
|
|
@@ -422,7 +572,7 @@ const DEFAULT_PREFERENCES = {
|
|
|
422
572
|
ollamaUrl: "http://localhost:11434",
|
|
423
573
|
ollamaModel: "qwen2.5-coder:1.5b",
|
|
424
574
|
cloudApiKey: "",
|
|
425
|
-
cloudModel: "claude-sonnet-4-
|
|
575
|
+
cloudModel: "claude-sonnet-4-5",
|
|
426
576
|
debounceMs: 600,
|
|
427
577
|
maxTokens: 60,
|
|
428
578
|
},
|
|
@@ -433,7 +583,7 @@ const DEFAULT_AUTOCOMPLETE = {
|
|
|
433
583
|
ollamaUrl: "http://localhost:11434",
|
|
434
584
|
ollamaModel: "qwen2.5-coder:1.5b",
|
|
435
585
|
cloudApiKey: "",
|
|
436
|
-
cloudModel: "claude-sonnet-4-
|
|
586
|
+
cloudModel: "claude-sonnet-4-5",
|
|
437
587
|
debounceMs: 600,
|
|
438
588
|
maxTokens: 60,
|
|
439
589
|
};
|
|
@@ -532,27 +682,30 @@ function applyAccountOverrides(accounts) {
|
|
|
532
682
|
}
|
|
533
683
|
return accounts;
|
|
534
684
|
}
|
|
535
|
-
/** Load accounts
|
|
685
|
+
/** Load accounts, preferring the cloud copy when cloud is configured.
|
|
686
|
+
* The local file in `~/.rmfmail/` is an offline cache, NOT the source of
|
|
687
|
+
* truth — earlier versions returned the local copy whenever it was non-empty,
|
|
688
|
+
* which left the desktop reading a stale single-account file forever after
|
|
689
|
+
* the GDrive folder lookup got fixed. */
|
|
536
690
|
export async function loadAccountsAsync() {
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// Try cloud API fallback
|
|
691
|
+
// Make sure pendingCloudConfig is initialized (calling getSharedDir as a
|
|
692
|
+
// side effect — it sets pendingCloudConfig from the local config.jsonc).
|
|
693
|
+
getSharedDir();
|
|
694
|
+
// Cloud configured → cloud is canonical.
|
|
542
695
|
if (pendingCloudConfig) {
|
|
543
|
-
console.log(" [cloud] Trying cloud API for accounts...");
|
|
544
696
|
const content = await cloudRead("accounts.jsonc");
|
|
545
697
|
if (content) {
|
|
546
698
|
const data = parseJsonc(content);
|
|
547
699
|
if (data?.accounts || Array.isArray(data)) {
|
|
548
700
|
const raw = data.accounts || data;
|
|
549
701
|
const globalName = data.name || "";
|
|
702
|
+
// cloudRead has already cached content to LOCAL_DIR.
|
|
550
703
|
return applyAccountOverrides(raw.map((a) => normalizeAccount(a, globalName)));
|
|
551
704
|
}
|
|
552
705
|
}
|
|
553
|
-
//
|
|
706
|
+
// Cloud unreachable / unparseable — fall through to local cache.
|
|
554
707
|
}
|
|
555
|
-
return
|
|
708
|
+
return loadAccounts();
|
|
556
709
|
}
|
|
557
710
|
/** Strip default-valued fields from a normalized AccountConfig so the
|
|
558
711
|
* serialized JSONC stays compact and human-editable. The previous version
|
|
@@ -647,28 +800,44 @@ export function denormalizeAccount(acct, globalName) {
|
|
|
647
800
|
/** Save account configs */
|
|
648
801
|
/** Save accounts — merges with cloud copy by email (multi-client safe).
|
|
649
802
|
* Writes the lean form via denormalizeAccount so accounts.jsonc stays
|
|
650
|
-
* compact and human-editable.
|
|
803
|
+
* compact and human-editable. Preserves the top-level `keys` field
|
|
804
|
+
* (AI API keys) if present in the existing cloud or local file. */
|
|
651
805
|
export async function saveAccounts(accounts) {
|
|
652
|
-
//
|
|
806
|
+
// Read existing top-level fields we need to preserve (keys, etc.) before
|
|
807
|
+
// we rewrite. Prefer cloud over local.
|
|
808
|
+
let preservedKeys;
|
|
809
|
+
let cloudCopy;
|
|
653
810
|
try {
|
|
654
811
|
const cloudContent = await cloudRead("accounts.jsonc");
|
|
655
|
-
if (cloudContent)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
812
|
+
if (cloudContent)
|
|
813
|
+
cloudCopy = parseJsonc(cloudContent);
|
|
814
|
+
}
|
|
815
|
+
catch { /* ignore */ }
|
|
816
|
+
if (!cloudCopy) {
|
|
817
|
+
try {
|
|
818
|
+
const localPath = path.join(LOCAL_DIR, "accounts.jsonc");
|
|
819
|
+
if (fs.existsSync(localPath))
|
|
820
|
+
cloudCopy = readJsonc(localPath);
|
|
821
|
+
}
|
|
822
|
+
catch { /* ignore */ }
|
|
823
|
+
}
|
|
824
|
+
if (cloudCopy?.keys && typeof cloudCopy.keys === "object")
|
|
825
|
+
preservedKeys = cloudCopy.keys;
|
|
826
|
+
// Merge with cloud: keep all accounts, deduplicate by normalized email
|
|
827
|
+
if (cloudCopy) {
|
|
828
|
+
const cloudAccts = cloudCopy.accounts || (Array.isArray(cloudCopy) ? cloudCopy : []);
|
|
829
|
+
if (cloudAccts.length > 0) {
|
|
830
|
+
const seen = new Set(accounts.map(a => normalizeEmail(a.email)));
|
|
831
|
+
for (const ca of cloudAccts) {
|
|
832
|
+
if (ca.email && !seen.has(normalizeEmail(ca.email))) {
|
|
833
|
+
// Cloud entries are already lean — feed through
|
|
834
|
+
// normalizeAccount to coerce to AccountConfig shape.
|
|
835
|
+
accounts.push(normalizeAccount(ca));
|
|
836
|
+
seen.add(normalizeEmail(ca.email));
|
|
667
837
|
}
|
|
668
838
|
}
|
|
669
839
|
}
|
|
670
840
|
}
|
|
671
|
-
catch { /* cloud read failed — save local version */ }
|
|
672
841
|
// Promote a shared "name" to file level when every account has the same
|
|
673
842
|
// name — keeps the JSONC tidy ({ "name": "Bob Frankston", "accounts": [...] }
|
|
674
843
|
// instead of repeating "name" on each entry).
|
|
@@ -676,8 +845,106 @@ export async function saveAccounts(accounts) {
|
|
|
676
845
|
const globalName = names.size === 1 ? [...names][0] : undefined;
|
|
677
846
|
const lean = accounts.map(a => denormalizeAccount(a, globalName));
|
|
678
847
|
const payload = globalName ? { name: globalName, accounts: lean } : { accounts: lean };
|
|
848
|
+
if (preservedKeys)
|
|
849
|
+
payload.keys = preservedKeys;
|
|
679
850
|
saveFile("accounts.jsonc", payload);
|
|
680
851
|
}
|
|
852
|
+
/** Load top-level AI keys from accounts.jsonc. Returns empty placeholders
|
|
853
|
+
* ({ anthropic: "", openai: "" }) when the file lacks a `keys` section,
|
|
854
|
+
* so callers can call this unconditionally without missing-section guards.
|
|
855
|
+
* Reads from the same shared/local path resolution as loadAccounts. */
|
|
856
|
+
export function loadKeys() {
|
|
857
|
+
const sharedDir = getSharedDir();
|
|
858
|
+
const sharedPath = path.join(sharedDir, "accounts.jsonc");
|
|
859
|
+
const localPath = path.join(LOCAL_DIR, "accounts.jsonc");
|
|
860
|
+
let raw = readJsonc(sharedPath);
|
|
861
|
+
if (!raw)
|
|
862
|
+
raw = readJsonc(localPath);
|
|
863
|
+
const keys = raw?.keys;
|
|
864
|
+
return {
|
|
865
|
+
anthropic: typeof keys?.anthropic === "string" ? keys.anthropic : "",
|
|
866
|
+
openai: typeof keys?.openai === "string" ? keys.openai : "",
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/** Save AI keys back to accounts.jsonc, preserving the rest of the file
|
|
870
|
+
* (accounts list, file-level name). Used by Settings → AI to persist a
|
|
871
|
+
* key the user just entered. Cloud-synced via saveFile. */
|
|
872
|
+
export async function saveKeys(keys) {
|
|
873
|
+
let existing = {};
|
|
874
|
+
try {
|
|
875
|
+
const cloudContent = await cloudRead("accounts.jsonc");
|
|
876
|
+
if (cloudContent)
|
|
877
|
+
existing = parseJsonc(cloudContent) || {};
|
|
878
|
+
}
|
|
879
|
+
catch { /* ignore */ }
|
|
880
|
+
if (!existing.accounts) {
|
|
881
|
+
try {
|
|
882
|
+
const localPath = path.join(LOCAL_DIR, "accounts.jsonc");
|
|
883
|
+
if (fs.existsSync(localPath))
|
|
884
|
+
existing = readJsonc(localPath) || existing;
|
|
885
|
+
}
|
|
886
|
+
catch { /* ignore */ }
|
|
887
|
+
}
|
|
888
|
+
const merged = { ...existing.keys, ...keys };
|
|
889
|
+
// Drop empty-string fields so the on-disk file stays tidy when the user
|
|
890
|
+
// clears a key (writes `""` from the UI) — keep only meaningful values.
|
|
891
|
+
const cleaned = {};
|
|
892
|
+
if (merged.anthropic)
|
|
893
|
+
cleaned.anthropic = merged.anthropic;
|
|
894
|
+
if (merged.openai)
|
|
895
|
+
cleaned.openai = merged.openai;
|
|
896
|
+
existing.keys = cleaned;
|
|
897
|
+
saveFile("accounts.jsonc", existing);
|
|
898
|
+
}
|
|
899
|
+
/** First-run helper: ensure `keys: { anthropic: "", openai: "" }` is
|
|
900
|
+
* materialized in accounts.jsonc so the user sees placeholders for every
|
|
901
|
+
* supported provider when they open the config editor. Adds missing
|
|
902
|
+
* fields without disturbing existing values, so `keys: {}` becomes
|
|
903
|
+
* `keys: { anthropic: "", openai: "" }` and `keys: { anthropic: "sk-..." }`
|
|
904
|
+
* becomes `keys: { anthropic: "sk-...", openai: "" }`. Idempotent. */
|
|
905
|
+
export async function ensureKeysSectionExists() {
|
|
906
|
+
const sharedDir = getSharedDir();
|
|
907
|
+
const sharedPath = path.join(sharedDir, "accounts.jsonc");
|
|
908
|
+
const raw = readJsonc(sharedPath);
|
|
909
|
+
if (!raw)
|
|
910
|
+
return; // no accounts file yet — first-time setup will handle it
|
|
911
|
+
const existing = (raw.keys && typeof raw.keys === "object") ? raw.keys : {};
|
|
912
|
+
const merged = {
|
|
913
|
+
anthropic: typeof existing.anthropic === "string" ? existing.anthropic : "",
|
|
914
|
+
openai: typeof existing.openai === "string" ? existing.openai : "",
|
|
915
|
+
};
|
|
916
|
+
// Skip the write if the on-disk shape already matches — avoids touching
|
|
917
|
+
// accounts.jsonc on every startup and the spurious "config changed" banner.
|
|
918
|
+
if (raw.keys
|
|
919
|
+
&& typeof raw.keys === "object"
|
|
920
|
+
&& raw.keys.anthropic === merged.anthropic
|
|
921
|
+
&& raw.keys.openai === merged.openai
|
|
922
|
+
&& Object.keys(raw.keys).length === 2)
|
|
923
|
+
return;
|
|
924
|
+
raw.keys = merged;
|
|
925
|
+
saveFile("accounts.jsonc", raw);
|
|
926
|
+
}
|
|
927
|
+
/** First-run helper: materialize the `autocomplete` block in
|
|
928
|
+
* preferences.jsonc with explicit defaults. loadPreferences() merges
|
|
929
|
+
* defaults at read time, but the file on disk often has no
|
|
930
|
+
* `autocomplete` key, so a user opening Settings → Edit config files
|
|
931
|
+
* doesn't see the AI provider knobs. This writes the defaults out so
|
|
932
|
+
* the user can edit them. Idempotent. */
|
|
933
|
+
export async function ensureAutocompletePrefsExist() {
|
|
934
|
+
const sharedDir = getSharedDir();
|
|
935
|
+
const sharedPath = path.join(sharedDir, "preferences.jsonc");
|
|
936
|
+
let raw = readJsonc(sharedPath);
|
|
937
|
+
if (!raw)
|
|
938
|
+
raw = {};
|
|
939
|
+
if (raw.autocomplete && typeof raw.autocomplete === "object")
|
|
940
|
+
return; // already present
|
|
941
|
+
raw.autocomplete = { ...DEFAULT_AUTOCOMPLETE };
|
|
942
|
+
// Strip the cloudApiKey field — the API key now lives in
|
|
943
|
+
// accounts.jsonc.keys, not here. Writing "" would mislead users into
|
|
944
|
+
// editing the wrong file.
|
|
945
|
+
delete raw.autocomplete.cloudApiKey;
|
|
946
|
+
saveFile("preferences.jsonc", raw);
|
|
947
|
+
}
|
|
681
948
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
682
949
|
export function loadPreferences() {
|
|
683
950
|
let shared = loadFile("preferences.jsonc", DEFAULT_PREFERENCES);
|
|
@@ -797,27 +1064,27 @@ export function initLocalConfig(sharedDir, storePath) {
|
|
|
797
1064
|
const config = {
|
|
798
1065
|
...existing,
|
|
799
1066
|
sharedDir: resolvedSharedDir,
|
|
800
|
-
|
|
1067
|
+
// storePath is now optional — omit when default. Resolves to
|
|
1068
|
+
// ~/.rmfmail/mailxstore via getStorePath() if not specified.
|
|
1069
|
+
...(storePath || existing.storePath ? { storePath: storePath || existing.storePath } : {}),
|
|
801
1070
|
};
|
|
802
1071
|
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
803
1072
|
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
804
1073
|
}
|
|
805
1074
|
/** Initialize config with Google Drive cloud storage.
|
|
806
|
-
* Finds or creates the app-owned "
|
|
807
|
-
* No mount scanning — API only. Existing settings at other paths (e.g., home/.
|
|
1075
|
+
* Finds or creates the app-owned ".rmfmail" folder via Drive API and stores its ID.
|
|
1076
|
+
* No mount scanning — API only. Existing settings at other paths (e.g., home/.rmfmail
|
|
808
1077
|
* from Desktop sync) must be migrated manually or via config.jsonc importPath. */
|
|
809
1078
|
export async function initCloudConfig(provider = "gdrive") {
|
|
810
1079
|
const existing = readLocalConfig();
|
|
811
1080
|
if (existing.sharedDir)
|
|
812
1081
|
return; // Already configured
|
|
813
|
-
// Find or create the settings folder via Drive API
|
|
814
|
-
//
|
|
815
|
-
//
|
|
1082
|
+
// Find or create the settings folder via Drive API. Stored path is the
|
|
1083
|
+
// canonical post-rebrand location; the folderId is what actually drives
|
|
1084
|
+
// subsequent reads/writes, so even legacy users with the old folder name
|
|
1085
|
+
// work fine since gDriveFindOrCreateFolder resolves whichever exists.
|
|
816
1086
|
const folderId = await gDriveFindOrCreateFolder();
|
|
817
|
-
|
|
818
|
-
// (gDriveFindOrCreateFolder logs it). For now use "mailx" as default
|
|
819
|
-
// label — the folderId is what matters for subsequent reads/writes.
|
|
820
|
-
const sharedDir = { provider, path: "home/.mailx", folderId: folderId || undefined };
|
|
1087
|
+
const sharedDir = { provider, path: "home/.rmfmail", folderId: folderId || undefined };
|
|
821
1088
|
const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };
|
|
822
1089
|
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
823
1090
|
atomicWrite(LOCAL_CONFIG_PATH, config);
|
|
@@ -848,4 +1115,59 @@ export function getPrefetch() {
|
|
|
848
1115
|
return prefs.sync.prefetch !== false;
|
|
849
1116
|
}
|
|
850
1117
|
export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
|
|
1118
|
+
/** Deploy the app-owned `.md` reference docs to the shared cloud folder so
|
|
1119
|
+
* users can read them next to the `.jsonc` files they document. The .md
|
|
1120
|
+
* files live in the app bundle (`<workspace>/app/docs/` in dev, or
|
|
1121
|
+
* `<package>/docs/` in published builds) and are overwritten on every
|
|
1122
|
+
* release — the file header documents that fact, so users know not to
|
|
1123
|
+
* edit them.
|
|
1124
|
+
*
|
|
1125
|
+
* Skip-path: a manifest file `.docs-version` on the cloud records the
|
|
1126
|
+
* app version that last deployed. We re-deploy only when the running
|
|
1127
|
+
* app's version differs, so startup doesn't pound Drive on every launch.
|
|
1128
|
+
*
|
|
1129
|
+
* This function is fire-and-forget at the call site; failures are logged
|
|
1130
|
+
* but never throw. The `.md` files are reference material, not load-
|
|
1131
|
+
* bearing — the app works without them.
|
|
1132
|
+
*/
|
|
1133
|
+
export async function deployDocs(appVersion) {
|
|
1134
|
+
if (!pendingCloudConfig?.folderId)
|
|
1135
|
+
return;
|
|
1136
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
|
|
1137
|
+
if (!provider)
|
|
1138
|
+
return;
|
|
1139
|
+
// Resolve the source docs dir. In dev the workspace has `app/docs/`;
|
|
1140
|
+
// in published packages we look for `docs/` next to this file.
|
|
1141
|
+
const candidates = [
|
|
1142
|
+
path.join(__dirname, "..", "..", "docs"), // workspace dev
|
|
1143
|
+
path.join(__dirname, "docs"), // sibling in published package
|
|
1144
|
+
path.join(__dirname, "..", "docs"), // one level up in some layouts
|
|
1145
|
+
];
|
|
1146
|
+
let docsDir = "";
|
|
1147
|
+
for (const c of candidates) {
|
|
1148
|
+
if (fs.existsSync(c) && fs.readdirSync(c).some(f => f.endsWith(".md"))) {
|
|
1149
|
+
docsDir = c;
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (!docsDir) {
|
|
1154
|
+
console.log(" [docs] no docs/ dir found in package — skipping deploy");
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const deployedVersion = (await provider.read(".docs-version") || "").trim();
|
|
1159
|
+
if (deployedVersion === appVersion)
|
|
1160
|
+
return; // already up to date
|
|
1161
|
+
const mdFiles = fs.readdirSync(docsDir).filter(f => f.endsWith(".md"));
|
|
1162
|
+
for (const f of mdFiles) {
|
|
1163
|
+
const content = fs.readFileSync(path.join(docsDir, f), "utf-8");
|
|
1164
|
+
await provider.write(f, content);
|
|
1165
|
+
}
|
|
1166
|
+
await provider.write(".docs-version", appVersion);
|
|
1167
|
+
console.log(` [docs] deployed ${mdFiles.length} .md file(s) for app v${appVersion}`);
|
|
1168
|
+
}
|
|
1169
|
+
catch (e) {
|
|
1170
|
+
console.warn(` [docs] deploy failed: ${e.message}`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
851
1173
|
//# sourceMappingURL=index.js.map
|