@bobfrankston/mailx-settings 0.1.7 → 0.1.10

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/docs/accounts.md CHANGED
@@ -21,7 +21,11 @@
21
21
  "enabled": true, // false skips this account at startup
22
22
  "identityDomains": ["alias.com"] // extra domains for Reply-From auto-detect
23
23
  }
24
- ]
24
+ ],
25
+ "keys": { // AI provider API keys (optional)
26
+ "anthropic": "", // sk-ant-api03-...
27
+ "openai": "" // sk-...
28
+ }
25
29
  }
26
30
  ```
27
31
 
@@ -33,6 +37,7 @@
33
37
  - **auth** — `"password"` for traditional IMAP/SMTP, `"oauth2"` for Gmail/Google Workspace/Outlook.
34
38
  - **enabled** — set `false` to keep the account record but skip sync at startup.
35
39
  - **identityDomains** — addresses you receive at on alternative domains. When you Reply, rmfmail picks the matching identity address as From instead of the account's primary.
40
+ - **keys.anthropic / keys.openai** — API keys for the AI features (translate / proofread / summarize / autocomplete). Lives at the file's top level alongside `accounts:` so you set it once across all your devices. Generated at console.anthropic.com or platform.openai.com. Provider selection is in `preferences.jsonc`; the key here is read based on which provider is active. Empty string means "not configured" — the AI feature silently no-ops. Local Ollama provider needs no key.
36
41
 
37
42
  ## Notes
38
43
 
package/docs/config.md CHANGED
@@ -17,7 +17,7 @@ Lives at `~/.rmfmail/config.jsonc` on the local filesystem.
17
17
  "path": ".rmfmail", // folder name in My Drive (currently My Drive/home/.rmfmail)
18
18
  "folderId": "1AbCdEf...XYZ" // resolved Drive folderId, cached after first lookup
19
19
  },
20
- "storePath": "C:/Users/Bob/.rmfmail/mailxstore", // local directory for .eml message bodies
20
+ "storePath": "~/.rmfmail/mailxstore", // local directory for .eml message bodies
21
21
  "historyDays": 30 // how far back to sync; overrides the shared default
22
22
  }
23
23
  ```
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?: {
@@ -107,8 +107,32 @@ export declare function denormalizeAccount(acct: AccountConfig, globalName?: str
107
107
  /** Save account configs */
108
108
  /** Save accounts — merges with cloud copy by email (multi-client safe).
109
109
  * Writes the lean form via denormalizeAccount so accounts.jsonc stays
110
- * 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. */
111
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>;
112
136
  /** Load preferences (shared + local overrides, with legacy fallback) */
113
137
  export declare function loadPreferences(): typeof DEFAULT_PREFERENCES;
114
138
  /** Save preferences */
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAyGpG,QAAA,MAAM,SAAS,QAA4E,CAAC;AAiE5F,qFAAqF;AACrF,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAC;AAE/G,wBAAgB,YAAY,CAAC,EAAE,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAM/D;AAOD,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAA2B;AAU7E,iBAAS,YAAY,IAAI,MAAM,CAgB9B;AAOD,sEAAsE;AACtE,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoCxE;AAED;;qCAEqC;AACrC,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BjF;AAyBD,2CAA2C;AAC3C,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED,4CAA4C;AAC5C,wBAAgB,cAAc,IAAI;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CA+B3L;AAuID;;qDAEqD;AACrD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,aAAa,CA4D9E;AAMD,QAAA,MAAM,mBAAmB;;eAEE,QAAQ,GAAG,MAAM,GAAG,OAAO;gBAC3B,OAAO,GAAG,QAAQ;;;;;;;;;;;;;;;;;;;;CAoB5C,CAAC;AAEF,QAAA,MAAM,oBAAoB,EAAE,oBAS3B,CAAC;AAEF,QAAA,MAAM,iBAAiB;aACJ,MAAM,EAAE;aACR,MAAM,EAAE;gBACL,MAAM,EAAE;oBAOJ,MAAM,EAAE;oBACR,MAAM,EAAE;CACjC,CAAC;AAIF,2BAA2B;AAC3B,wBAAgB,YAAY,IAAI,aAAa,EAAE,CA0C9C;AAoCD;;;;0CAI0C;AAC1C,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAqBlE;AAED;;;;;;;;;;;;;;;iDAeiD;AACjD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,GAAG,CA+ChF;AAED,2BAA2B;AAC3B;;kCAEkC;AAClC,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B3E;AAED,wEAAwE;AACxE,wBAAgB,eAAe,IAAI,OAAO,mBAAmB,CAoB5D;AAED,uBAAuB;AACvB,wBAAgB,eAAe,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAEhD;AAED,iCAAiC;AACjC,wBAAgB,gBAAgB,IAAI,oBAAoB,CAGvD;AAED,iCAAiC;AACjC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAIrE;AAED,qCAAqC;AACrC,wBAAgB,aAAa,IAAI,OAAO,iBAAiB,CAExD;AAED,4EAA4E;AAC5E,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBjF;AAcD,6EAA6E;AAC7E,wBAAgB,YAAY,IAAI,aAAa,CAe5C;AAED,4CAA4C;AAC5C,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzE;AAED,oCAAoC;AACpC,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,qDAAqD;AACrD,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wCAAwC;AACxC,OAAO,EAAE,YAAY,EAAE,CAAC;AAKxB,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAkB5E;AAED;;;mFAGmF;AACnF,wBAAsB,eAAe,CAAC,QAAQ,GAAE,QAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAclF;AAED,QAAA,MAAM,gBAAgB,EAAE,aAMvB,CAAC;AAEF,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAQzD;AAED,uEAAuE;AACvE,wBAAgB,WAAW,IAAI,OAAO,CAGrC;AAED,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,SAAS,EAAE,CAAC;AAErG;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmClE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
package/index.js CHANGED
@@ -572,7 +572,7 @@ const DEFAULT_PREFERENCES = {
572
572
  ollamaUrl: "http://localhost:11434",
573
573
  ollamaModel: "qwen2.5-coder:1.5b",
574
574
  cloudApiKey: "",
575
- cloudModel: "claude-sonnet-4-20250514",
575
+ cloudModel: "claude-sonnet-4-5",
576
576
  debounceMs: 600,
577
577
  maxTokens: 60,
578
578
  },
@@ -583,7 +583,7 @@ const DEFAULT_AUTOCOMPLETE = {
583
583
  ollamaUrl: "http://localhost:11434",
584
584
  ollamaModel: "qwen2.5-coder:1.5b",
585
585
  cloudApiKey: "",
586
- cloudModel: "claude-sonnet-4-20250514",
586
+ cloudModel: "claude-sonnet-4-5",
587
587
  debounceMs: 600,
588
588
  maxTokens: 60,
589
589
  };
@@ -800,28 +800,44 @@ export function denormalizeAccount(acct, globalName) {
800
800
  /** Save account configs */
801
801
  /** Save accounts — merges with cloud copy by email (multi-client safe).
802
802
  * Writes the lean form via denormalizeAccount so accounts.jsonc stays
803
- * 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. */
804
805
  export async function saveAccounts(accounts) {
805
- // Merge with cloud: keep all accounts, deduplicate by normalized email
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;
806
810
  try {
807
811
  const cloudContent = await cloudRead("accounts.jsonc");
808
- if (cloudContent) {
809
- const cloud = parseJsonc(cloudContent);
810
- const cloudAccts = cloud?.accounts || (Array.isArray(cloud) ? cloud : []);
811
- if (cloudAccts.length > 0) {
812
- const seen = new Set(accounts.map(a => normalizeEmail(a.email)));
813
- for (const ca of cloudAccts) {
814
- if (ca.email && !seen.has(normalizeEmail(ca.email))) {
815
- // Cloud entries are already lean — feed through
816
- // normalizeAccount to coerce to AccountConfig shape.
817
- accounts.push(normalizeAccount(ca));
818
- seen.add(normalizeEmail(ca.email));
819
- }
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));
820
837
  }
821
838
  }
822
839
  }
823
840
  }
824
- catch { /* cloud read failed — save local version */ }
825
841
  // Promote a shared "name" to file level when every account has the same
826
842
  // name — keeps the JSONC tidy ({ "name": "Bob Frankston", "accounts": [...] }
827
843
  // instead of repeating "name" on each entry).
@@ -829,8 +845,106 @@ export async function saveAccounts(accounts) {
829
845
  const globalName = names.size === 1 ? [...names][0] : undefined;
830
846
  const lean = accounts.map(a => denormalizeAccount(a, globalName));
831
847
  const payload = globalName ? { name: globalName, accounts: lean } : { accounts: lean };
848
+ if (preservedKeys)
849
+ payload.keys = preservedKeys;
832
850
  saveFile("accounts.jsonc", payload);
833
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
+ }
834
948
  /** Load preferences (shared + local overrides, with legacy fallback) */
835
949
  export function loadPreferences() {
836
950
  let shared = loadFile("preferences.jsonc", DEFAULT_PREFERENCES);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-settings",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
- "@bobfrankston/mailx-types": "^0.1.6",
20
+ "@bobfrankston/mailx-types": "^0.1.8",
21
21
  "jsonc-parser": "^3.3.1"
22
22
  },
23
23
  "repository": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  ".transformedSnapshot": {
35
35
  "dependencies": {
36
- "@bobfrankston/mailx-types": "^0.1.6",
36
+ "@bobfrankston/mailx-types": "^0.1.8",
37
37
  "jsonc-parser": "^3.3.1"
38
38
  }
39
39
  }