openmates 0.11.0-alpha.7 → 0.11.0-alpha.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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ uploadFile
3
+ } from "./chunk-SFTCIVE2.js";
4
+
1
5
  // src/client.ts
2
6
  import { randomUUID } from "crypto";
3
7
  import { platform as platform2, release } from "os";
@@ -87,6 +91,16 @@ async function decryptWithAesGcmCombined(encryptedWithIvB64, rawKeyBytes) {
87
91
  return null;
88
92
  }
89
93
  }
94
+ async function deriveEmailEncryptionKeyB64(email, emailSaltB64) {
95
+ const encoder = new TextEncoder();
96
+ const emailBytes = encoder.encode(email);
97
+ const saltBytes = base64ToBytes(emailSaltB64);
98
+ const combined = new Uint8Array(emailBytes.length + saltBytes.length);
99
+ combined.set(emailBytes);
100
+ combined.set(saltBytes, emailBytes.length);
101
+ const hashBuffer = await cryptoApi.subtle.digest("SHA-256", toArrayBuffer(combined));
102
+ return bytesToBase64(new Uint8Array(hashBuffer));
103
+ }
90
104
  async function decryptBytesWithAesGcm(encryptedWithIvB64, rawKeyBytes) {
91
105
  try {
92
106
  const combined = base64ToBytes(encryptedWithIvB64);
@@ -186,6 +200,23 @@ var OpenMatesHttpClient = class {
186
200
  async patch(path, body, headers = {}) {
187
201
  return this.request("PATCH", path, body, headers);
188
202
  }
203
+ async getBinary(path, headers = {}) {
204
+ const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
205
+ const requestHeaders = {
206
+ Accept: "application/pdf,application/octet-stream",
207
+ ...headers
208
+ };
209
+ const cookieHeader = this.formatCookieHeader();
210
+ if (cookieHeader) requestHeaders.Cookie = cookieHeader;
211
+ const response = await fetch(url, { method: "GET", headers: requestHeaders });
212
+ this.captureCookies(response);
213
+ return {
214
+ ok: response.ok,
215
+ status: response.status,
216
+ data: new Uint8Array(await response.arrayBuffer()),
217
+ headers: response.headers
218
+ };
219
+ }
189
220
  async request(method, path, body, headers = {}) {
190
221
  const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
191
222
  const requestHeaders = {
@@ -533,6 +564,18 @@ function saveSession(session) {
533
564
  } else if (result.type === "plaintext") {
534
565
  onDisk.masterKeyExportedB64 = session.masterKeyExportedB64;
535
566
  }
567
+ if (session.emailEncryptionKeyB64) {
568
+ const emailKeyResult = storeMasterKey(
569
+ session.emailEncryptionKeyB64,
570
+ `${session.hashedEmail}:email`
571
+ );
572
+ onDisk.emailEncryptionKeyStorage = emailKeyResult.type;
573
+ if (emailKeyResult.type === "encrypted") {
574
+ onDisk.emailEncryptionKeyEncrypted = emailKeyResult.encryptedData;
575
+ } else if (emailKeyResult.type === "plaintext") {
576
+ onDisk.emailEncryptionKeyB64 = session.emailEncryptionKeyB64;
577
+ }
578
+ }
536
579
  writeJsonFile(filePath, onDisk);
537
580
  if (result.type !== "plaintext") {
538
581
  process.stderr.write("Decrypting data...\n");
@@ -543,17 +586,19 @@ function loadSession() {
543
586
  const onDisk = readJsonFile(filePath);
544
587
  if (!onDisk) return null;
545
588
  let masterKey = null;
589
+ let emailEncryptionKey = null;
546
590
  if (!onDisk.masterKeyStorage) {
547
591
  masterKey = onDisk.masterKeyExportedB64 ?? null;
548
592
  if (masterKey) {
549
- const session = buildSession(onDisk, masterKey);
593
+ emailEncryptionKey = getEmailEncryptionKeyFromDisk(onDisk);
594
+ const session = buildSession(onDisk, masterKey, emailEncryptionKey);
550
595
  try {
551
596
  saveSession(session);
552
597
  process.stderr.write("Decrypting data...\n");
553
598
  } catch {
554
599
  }
555
600
  }
556
- return masterKey ? buildSession(onDisk, masterKey) : null;
601
+ return masterKey ? buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk)) : null;
557
602
  }
558
603
  switch (onDisk.masterKeyStorage) {
559
604
  case "keychain":
@@ -577,7 +622,7 @@ function loadSession() {
577
622
  );
578
623
  return null;
579
624
  }
580
- return buildSession(onDisk, masterKey);
625
+ return buildSession(onDisk, masterKey, getEmailEncryptionKeyFromDisk(onDisk));
581
626
  }
582
627
  function clearSession() {
583
628
  const filePath = join(ensureStateDir(), "session.json");
@@ -585,17 +630,36 @@ function clearSession() {
585
630
  if (onDisk?.masterKeyStorage) {
586
631
  deleteMasterKey(onDisk.masterKeyStorage, onDisk.hashedEmail);
587
632
  }
633
+ if (onDisk?.emailEncryptionKeyStorage) {
634
+ deleteMasterKey(onDisk.emailEncryptionKeyStorage, `${onDisk.hashedEmail}:email`);
635
+ }
588
636
  if (existsSync2(filePath)) {
589
637
  rmSync(filePath);
590
638
  }
591
639
  }
592
- function buildSession(onDisk, masterKey) {
640
+ function getEmailEncryptionKeyFromDisk(onDisk) {
641
+ if (!onDisk.emailEncryptionKeyStorage) return onDisk.emailEncryptionKeyB64 ?? null;
642
+ switch (onDisk.emailEncryptionKeyStorage) {
643
+ case "keychain":
644
+ return retrieveMasterKey("keychain", `${onDisk.hashedEmail}:email`);
645
+ case "encrypted":
646
+ return retrieveMasterKey(
647
+ "encrypted",
648
+ `${onDisk.hashedEmail}:email`,
649
+ onDisk.emailEncryptionKeyEncrypted
650
+ );
651
+ case "plaintext":
652
+ return onDisk.emailEncryptionKeyB64 ?? null;
653
+ }
654
+ }
655
+ function buildSession(onDisk, masterKey, emailEncryptionKey) {
593
656
  return {
594
657
  apiUrl: onDisk.apiUrl,
595
658
  sessionId: onDisk.sessionId,
596
659
  wsToken: onDisk.wsToken,
597
660
  cookies: onDisk.cookies,
598
661
  masterKeyExportedB64: masterKey,
662
+ emailEncryptionKeyB64: emailEncryptionKey,
599
663
  hashedEmail: onDisk.hashedEmail,
600
664
  userEmailSalt: onDisk.userEmailSalt,
601
665
  createdAt: onDisk.createdAt,
@@ -1807,6 +1871,7 @@ var OpenMatesClient = class _OpenMatesClient {
1807
1871
  authorizerDeviceName: complete.data.authorizer_device_name ?? null,
1808
1872
  autoLogoutMinutes: complete.data.auto_logout_minutes ?? null
1809
1873
  };
1874
+ await this.hydrateEmailEncryptionKey(session);
1810
1875
  saveSession(session);
1811
1876
  }
1812
1877
  async whoAmI() {
@@ -2767,6 +2832,124 @@ var OpenMatesClient = class _OpenMatesClient {
2767
2832
  }
2768
2833
  return response.data;
2769
2834
  }
2835
+ async listInvoices() {
2836
+ this.requireSession();
2837
+ const response = await this.http.get(
2838
+ "/v1/payments/invoices",
2839
+ this.getCliRequestHeaders()
2840
+ );
2841
+ if (!response.ok) {
2842
+ throw new Error(`Failed to fetch invoices (HTTP ${response.status})`);
2843
+ }
2844
+ return { invoices: response.data.invoices ?? [] };
2845
+ }
2846
+ async downloadInvoice(invoiceId) {
2847
+ return this.downloadPaymentPdf(
2848
+ `/v1/payments/invoices/${encodeURIComponent(invoiceId)}/download`,
2849
+ `Invoice_${invoiceId}.pdf`
2850
+ );
2851
+ }
2852
+ async downloadCreditNote(invoiceId) {
2853
+ return this.downloadPaymentPdf(
2854
+ `/v1/payments/invoices/${encodeURIComponent(invoiceId)}/credit-note/download`,
2855
+ `CreditNote_${invoiceId}.pdf`
2856
+ );
2857
+ }
2858
+ async requestRefund(invoiceId) {
2859
+ const session = this.requireSession();
2860
+ const emailEncryptionKey = await this.ensureEmailEncryptionKey(session);
2861
+ const response = await this.http.post(
2862
+ "/v1/payments/refund",
2863
+ { invoice_id: invoiceId, email_encryption_key: emailEncryptionKey },
2864
+ this.getCliRequestHeaders()
2865
+ );
2866
+ if (!response.ok) {
2867
+ throw new Error(`Refund request failed (HTTP ${response.status})`);
2868
+ }
2869
+ return response.data;
2870
+ }
2871
+ async updateUsername(username) {
2872
+ return this.settingsPost("user/username", { username });
2873
+ }
2874
+ async updateProfileImage(filePath) {
2875
+ const { uploadProfileImage } = await import("./uploadService-3CAJXU4L.js");
2876
+ const result = await uploadProfileImage(filePath, this.requireSession());
2877
+ if (result.status === "rejected") {
2878
+ throw new Error(result.detail ?? "Profile image rejected by content safety checks.");
2879
+ }
2880
+ if (result.status === "account_deleted") {
2881
+ throw new Error("Account deleted due to repeated profile image policy violations.");
2882
+ }
2883
+ if (result.status !== "ok") {
2884
+ throw new Error(result.detail ?? `Profile image upload failed with status '${result.status}'.`);
2885
+ }
2886
+ return result;
2887
+ }
2888
+ async getNewsletterCategories() {
2889
+ this.requireSession();
2890
+ const response = await this.http.get(
2891
+ "/v1/newsletter/categories",
2892
+ this.getCliRequestHeaders()
2893
+ );
2894
+ if (!response.ok) {
2895
+ throw new Error(`Failed to fetch newsletter categories (HTTP ${response.status})`);
2896
+ }
2897
+ return response.data;
2898
+ }
2899
+ async updateNewsletterCategories(categories) {
2900
+ this.requireSession();
2901
+ const response = await this.http.patch(
2902
+ "/v1/newsletter/categories",
2903
+ { categories },
2904
+ this.getCliRequestHeaders()
2905
+ );
2906
+ if (!response.ok) {
2907
+ throw new Error(`Failed to update newsletter categories (HTTP ${response.status})`);
2908
+ }
2909
+ return response.data;
2910
+ }
2911
+ async subscribeNewsletter(email, language = "en", darkmode = false) {
2912
+ const response = await this.http.post(
2913
+ "/v1/newsletter/subscribe",
2914
+ { email, language, darkmode },
2915
+ this.getCliRequestHeaders()
2916
+ );
2917
+ if (!response.ok) {
2918
+ throw new Error(`Newsletter subscribe failed (HTTP ${response.status})`);
2919
+ }
2920
+ return response.data;
2921
+ }
2922
+ async confirmNewsletter(token) {
2923
+ const response = await this.http.get(
2924
+ `/v1/newsletter/confirm/${encodeURIComponent(token)}`,
2925
+ this.getCliRequestHeaders()
2926
+ );
2927
+ if (!response.ok) {
2928
+ throw new Error(`Newsletter confirmation failed (HTTP ${response.status})`);
2929
+ }
2930
+ return response.data;
2931
+ }
2932
+ async unsubscribeNewsletter(token) {
2933
+ const response = await this.http.get(
2934
+ `/v1/newsletter/unsubscribe/${encodeURIComponent(token)}`,
2935
+ this.getCliRequestHeaders()
2936
+ );
2937
+ if (!response.ok) {
2938
+ throw new Error(`Newsletter unsubscribe failed (HTTP ${response.status})`);
2939
+ }
2940
+ return response.data;
2941
+ }
2942
+ async updateEmailNotificationSettings(payload) {
2943
+ const { ws } = await this.openWsClient();
2944
+ try {
2945
+ const ackPromise = ws.waitForMessage("email_notification_settings_ack");
2946
+ ws.send("email_notification_settings", payload);
2947
+ const ack = await ackPromise;
2948
+ return ack.payload;
2949
+ } finally {
2950
+ ws.close();
2951
+ }
2952
+ }
2770
2953
  // -------------------------------------------------------------------------
2771
2954
  // Daily Inspirations
2772
2955
  // -------------------------------------------------------------------------
@@ -3231,6 +3414,45 @@ Required: ${schema.required.join(", ")}`
3231
3414
  if (path.startsWith("/")) return path;
3232
3415
  return `/v1/settings/${path}`;
3233
3416
  }
3417
+ async downloadPaymentPdf(path, fallbackFilename) {
3418
+ this.requireSession();
3419
+ const response = await this.http.getBinary(path, this.getCliRequestHeaders());
3420
+ if (!response.ok) {
3421
+ throw new Error(`Download failed (HTTP ${response.status})`);
3422
+ }
3423
+ return {
3424
+ filename: filenameFromContentDisposition(response.headers.get("content-disposition")) ?? fallbackFilename,
3425
+ data: response.data
3426
+ };
3427
+ }
3428
+ async ensureEmailEncryptionKey(session) {
3429
+ if (session.emailEncryptionKeyB64) return session.emailEncryptionKeyB64;
3430
+ await this.hydrateEmailEncryptionKey(session);
3431
+ if (session.emailEncryptionKeyB64) return session.emailEncryptionKeyB64;
3432
+ throw new Error(
3433
+ "Email encryption key is missing. Run `openmates login` again to refresh your local encryption keys."
3434
+ );
3435
+ }
3436
+ async hydrateEmailEncryptionKey(session) {
3437
+ const response = await this.http.post(
3438
+ "/v1/auth/session",
3439
+ { session_id: session.sessionId },
3440
+ this.getCliRequestHeaders()
3441
+ );
3442
+ const encryptedEmail = response.data.user?.encrypted_email_with_master_key;
3443
+ if (!response.ok || !response.data.success || typeof encryptedEmail !== "string") {
3444
+ return;
3445
+ }
3446
+ const email = await decryptWithAesGcmCombined(
3447
+ encryptedEmail,
3448
+ base64ToBytes(session.masterKeyExportedB64)
3449
+ );
3450
+ if (!email) return;
3451
+ session.emailEncryptionKeyB64 = await deriveEmailEncryptionKeyB64(
3452
+ email,
3453
+ session.userEmailSalt
3454
+ );
3455
+ }
3234
3456
  requireSession() {
3235
3457
  if (!this.session) {
3236
3458
  throw new Error("Not logged in. Run `openmates login`.");
@@ -3606,6 +3828,15 @@ function parseNewChatSuggestionText(text) {
3606
3828
  const skillId = raw.slice(dashIdx + 1);
3607
3829
  return { body, appId, skillId };
3608
3830
  }
3831
+ function filenameFromContentDisposition(header2) {
3832
+ if (!header2) return null;
3833
+ const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header2)?.[1];
3834
+ if (encoded) return decodeURIComponent(encoded);
3835
+ const quoted = /filename="([^"]+)"/i.exec(header2)?.[1];
3836
+ if (quoted) return quoted;
3837
+ const plain = /filename=([^;]+)/i.exec(header2)?.[1];
3838
+ return plain?.trim() ?? null;
3839
+ }
3609
3840
  function sleep(ms) {
3610
3841
  return new Promise((resolve4) => setTimeout(resolve4, ms));
3611
3842
  }
@@ -4819,97 +5050,6 @@ function formatEmbedsForMessage(embeds) {
4819
5050
  return "\n" + embeds.map((e) => e.referenceBlock).join("\n");
4820
5051
  }
4821
5052
 
4822
- // src/uploadService.ts
4823
- import { readFileSync as readFileSync4 } from "fs";
4824
- import { basename as basename2 } from "path";
4825
- var UPLOAD_MAX_ATTEMPTS = 3;
4826
- var UPLOAD_RETRY_DELAY_MS = 2e3;
4827
- function getUploadUrl(apiUrl) {
4828
- try {
4829
- const url = new URL(apiUrl);
4830
- if (url.hostname === "localhost") return "http://localhost:8001";
4831
- } catch {
4832
- }
4833
- return "https://upload.openmates.org";
4834
- }
4835
- function getUploadOrigin(apiUrl) {
4836
- try {
4837
- const url = new URL(apiUrl);
4838
- if (url.hostname === "localhost") return "http://localhost:5173";
4839
- if (url.hostname.startsWith("api.")) {
4840
- return `${url.protocol}//app.${url.hostname.slice(4)}`;
4841
- }
4842
- } catch {
4843
- }
4844
- return "https://app.openmates.org";
4845
- }
4846
- async function uploadFile(filePath, session) {
4847
- const filename = basename2(filePath);
4848
- const fileBytes = readFileSync4(filePath);
4849
- const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/file`;
4850
- const origin = getUploadOrigin(session.apiUrl);
4851
- const cookies = [];
4852
- if (session.cookies?.auth_refresh_token) {
4853
- cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
4854
- }
4855
- let response;
4856
- let lastError;
4857
- for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
4858
- try {
4859
- const blob = new Blob([fileBytes]);
4860
- const formData = new FormData();
4861
- formData.append("file", blob, filename);
4862
- response = await fetch(uploadUrl, {
4863
- method: "POST",
4864
- body: formData,
4865
- headers: {
4866
- Origin: origin,
4867
- ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
4868
- },
4869
- signal: AbortSignal.timeout(10 * 60 * 1e3)
4870
- // 10-minute timeout
4871
- });
4872
- break;
4873
- } catch (error) {
4874
- lastError = error;
4875
- if (attempt === UPLOAD_MAX_ATTEMPTS) break;
4876
- await new Promise((resolve4) => setTimeout(resolve4, UPLOAD_RETRY_DELAY_MS));
4877
- }
4878
- }
4879
- if (!response) {
4880
- const message = lastError instanceof Error ? lastError.message : String(lastError);
4881
- throw new Error(message || "Upload request failed.");
4882
- }
4883
- if (!response.ok) {
4884
- const status = response.status;
4885
- let errorMessage;
4886
- switch (status) {
4887
- case 401:
4888
- errorMessage = "Authentication failed. Run `openmates login` to re-authenticate.";
4889
- break;
4890
- case 413:
4891
- errorMessage = "File too large (maximum 100 MB).";
4892
- break;
4893
- case 415:
4894
- errorMessage = "Unsupported file type.";
4895
- break;
4896
- case 422: {
4897
- const body = await response.text().catch(() => "");
4898
- errorMessage = body.includes("malware") ? "File rejected: malware detected." : body.includes("content_safety") ? "File rejected: content safety violation." : `Upload validation failed: ${body}`;
4899
- break;
4900
- }
4901
- case 429:
4902
- errorMessage = "Upload rate limit exceeded. Try again in a minute.";
4903
- break;
4904
- default:
4905
- errorMessage = `Upload failed (HTTP ${status}).`;
4906
- }
4907
- throw new Error(errorMessage);
4908
- }
4909
- const data = await response.json();
4910
- return data;
4911
- }
4912
-
4913
5053
  // src/embedRenderers.ts
4914
5054
  var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
4915
5055
  var DIRECT_TYPES = /* @__PURE__ */ new Set([
@@ -5959,13 +6099,13 @@ function formatTs(ts) {
5959
6099
 
5960
6100
  // src/server.ts
5961
6101
  import { execSync, spawn as nodeSpawn } from "child_process";
5962
- import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync6, rmSync as rmSync3 } from "fs";
6102
+ import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync3 } from "fs";
5963
6103
  import { createInterface as createInterface2 } from "readline";
5964
6104
  import { homedir as homedir5 } from "os";
5965
6105
  import { join as join3, resolve as resolve3 } from "path";
5966
6106
 
5967
6107
  // src/serverConfig.ts
5968
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
6108
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5969
6109
  import { homedir as homedir4 } from "os";
5970
6110
  import { join as join2, resolve as resolve2 } from "path";
5971
6111
  var STATE_DIR = join2(homedir4(), ".openmates");
@@ -5986,7 +6126,7 @@ function loadServerConfig() {
5986
6126
  const filePath = join2(STATE_DIR, CONFIG_FILE);
5987
6127
  if (!existsSync4(filePath)) return null;
5988
6128
  try {
5989
- return JSON.parse(readFileSync5(filePath, "utf-8"));
6129
+ return JSON.parse(readFileSync4(filePath, "utf-8"));
5990
6130
  } catch {
5991
6131
  return null;
5992
6132
  }
@@ -6064,7 +6204,7 @@ function requireGit() {
6064
6204
  }
6065
6205
  function hasLlmCredentials(envPath) {
6066
6206
  if (!existsSync5(envPath)) return false;
6067
- const content = readFileSync6(envPath, "utf-8");
6207
+ const content = readFileSync5(envPath, "utf-8");
6068
6208
  for (const line of content.split("\n")) {
6069
6209
  const trimmed = line.trim();
6070
6210
  if (trimmed.startsWith("#") || !trimmed) continue;
@@ -7673,6 +7813,8 @@ var SETTINGS_EXECUTABLE_COMMANDS = [
7673
7813
  { path: ["account", "export", "manifest"], description: "Show account export manifest", examples: ["openmates settings account export manifest --json"] },
7674
7814
  { path: ["account", "export", "data"], description: "Fetch account export data", examples: ["openmates settings account export data --json"] },
7675
7815
  { path: ["account", "import-chat"], description: "Import a CLI chat export file", examples: ["openmates settings account import-chat ./chat.yml", "openmates settings account import-chat ./payload.json"] },
7816
+ { path: ["account", "username", "set"], description: "Change account username", examples: ["openmates settings account username set alice_123"] },
7817
+ { path: ["account", "profile-picture", "set"], description: "Upload a profile picture", examples: ["openmates settings account profile-picture set ./avatar.jpg"] },
7676
7818
  { path: ["account", "chats", "stats"], description: "Show chat statistics", examples: ["openmates settings account chats stats"] },
7677
7819
  { path: ["account", "delete", "preview"], description: "Preview account deletion impact", examples: ["openmates settings account delete preview"] },
7678
7820
  { path: ["account", "storage", "overview"], description: "Show storage overview", examples: ["openmates settings account storage overview"] },
@@ -7689,9 +7831,16 @@ var SETTINGS_EXECUTABLE_COMMANDS = [
7689
7831
  { path: ["billing", "usage", "summaries"], description: "Show usage summaries", examples: ["openmates settings billing usage summaries"] },
7690
7832
  { path: ["billing", "usage", "daily"], description: "Show daily usage overview", examples: ["openmates settings billing usage daily"] },
7691
7833
  { path: ["billing", "usage", "export"], description: "Export usage data", examples: ["openmates settings billing usage export --json"] },
7834
+ { path: ["billing", "invoices", "list"], description: "List invoices", examples: ["openmates settings billing invoices list --json"] },
7835
+ { path: ["billing", "invoices", "download"], description: "Download an invoice PDF", examples: ["openmates settings billing invoices download <invoice-id> --output ./invoices"] },
7836
+ { path: ["billing", "invoices", "credit-note"], description: "Download a credit note PDF", examples: ["openmates settings billing invoices credit-note <invoice-id> --output ./invoices"] },
7837
+ { path: ["billing", "invoices", "refund"], description: "Request a refund for an invoice", examples: ["openmates settings billing invoices refund <invoice-id> --yes"] },
7692
7838
  { path: ["billing", "gift-card", "redeem"], description: "Redeem a gift card", examples: ["openmates settings billing gift-card redeem ABCD-1234"] },
7693
7839
  { path: ["billing", "gift-card", "list"], description: "List redeemed gift cards", examples: ["openmates settings billing gift-card list"] },
7694
7840
  { path: ["billing", "auto-topup", "low-balance", "set"], description: "Configure low-balance auto top-up", examples: ["openmates settings billing auto-topup low-balance set --enabled true --amount 1000 --currency eur --email you@example.com"] },
7841
+ { path: ["notifications", "status"], description: "Show notification settings", examples: ["openmates settings notifications status --json"] },
7842
+ { path: ["notifications", "email", "set"], description: "Configure email notifications", examples: ["openmates settings notifications email set --enabled true --email you@example.com --ai-responses true --backup-reminder true --webhook-chats true"] },
7843
+ { path: ["notifications", "backup", "set"], description: "Configure backup reminder emails", examples: ["openmates settings notifications backup set --enabled true --interval 30 --email you@example.com"] },
7695
7844
  { path: ["reminders", "list"], description: "List active reminders", examples: ["openmates settings reminders list"] },
7696
7845
  { path: ["reminders", "update"], description: "Update a reminder", examples: ["openmates settings reminders update <id> --enabled false"] },
7697
7846
  { path: ["reminders", "delete"], description: "Delete a reminder", examples: ["openmates settings reminders delete <id> --yes"] },
@@ -7699,12 +7848,18 @@ var SETTINGS_EXECUTABLE_COMMANDS = [
7699
7848
  { path: ["developers", "api-keys", "revoke"], description: "Revoke an API key", examples: ["openmates settings developers api-keys revoke <key-id> --yes"] },
7700
7849
  { path: ["report-issue", "create"], description: "Report an issue", examples: ['openmates settings report-issue create --title "Bug" --body "What happened"'] },
7701
7850
  { path: ["report-issue", "status"], description: "Show issue status", examples: ["openmates settings report-issue status <issue-id>"] },
7851
+ { path: ["mates", "list"], description: "List available mates", examples: ["openmates settings mates list"] },
7852
+ { path: ["mates", "info"], description: "Show mate details", examples: ["openmates settings mates info software_development"] },
7853
+ { path: ["mates", "consent"], description: "Record mate settings consent", examples: ["openmates settings mates consent --yes"] },
7854
+ { path: ["newsletter", "categories"], description: "Show newsletter category preferences", examples: ["openmates settings newsletter categories"] },
7855
+ { path: ["newsletter", "categories", "set"], description: "Set newsletter category preferences", examples: ["openmates settings newsletter categories set --updates true --tips true --daily false"] },
7856
+ { path: ["newsletter", "subscribe"], description: "Subscribe an email to the newsletter", examples: ["openmates settings newsletter subscribe you@example.com --language en"] },
7857
+ { path: ["newsletter", "confirm"], description: "Confirm newsletter subscription token", examples: ["openmates settings newsletter confirm <token>"] },
7858
+ { path: ["newsletter", "unsubscribe"], description: "Unsubscribe with newsletter token", examples: ["openmates settings newsletter unsubscribe <token>"] },
7702
7859
  { path: ["memories"], description: "Manage encrypted memories", examples: ["openmates settings memories list", `openmates settings memories create --app-id code --item-type projects --data '{"name":"OpenMates"}'`] }
7703
7860
  ];
7704
7861
  var SETTINGS_INFO_COMMANDS = [
7705
- { path: ["account", "username"], description: "Username management is not CLI-ready yet", webPath: "account/username", reason: "The current web flow writes encrypted profile fields; CLI support needs a dedicated encryption UX first.", examples: ["openmates settings account username --help"] },
7706
7862
  { path: ["account", "email"], description: "Email changes are web-only", webPath: "account/email", reason: "Email changes require a guided identity verification flow.", examples: ["openmates settings account email"] },
7707
- { path: ["account", "profile-picture"], description: "Profile picture changes are not CLI-ready yet", webPath: "account/profile-picture", reason: "The upload/update flow needs image validation and parity with the browser uploader.", examples: ["openmates settings account profile-picture"] },
7708
7863
  { path: ["account", "delete"], description: "Account deletion is web-only", webPath: "account/delete", reason: "Account deletion requires browser-based reauthentication and explicit confirmation.", examples: ["openmates settings account delete"] },
7709
7864
  { path: ["security"], description: "Security settings are web-only", webPath: "account/security", reason: "Security settings require browser APIs or high-risk reauthentication.", examples: ["openmates settings security"] },
7710
7865
  { path: ["security", "passkeys"], description: "Passkeys are web-only", webPath: "account/security/passkeys", reason: "Passkeys require WebAuthn browser APIs.", examples: ["openmates settings security passkeys"] },
@@ -7715,16 +7870,12 @@ var SETTINGS_INFO_COMMANDS = [
7715
7870
  { path: ["billing", "buy-credits"], description: "Credit purchase is web-only", webPath: "billing/buy-credits", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings billing buy-credits"] },
7716
7871
  { path: ["billing", "gift-card", "buy"], description: "Gift card purchase is web-only", webPath: "billing/gift-cards/buy", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings billing gift-card buy"] },
7717
7872
  { path: ["billing", "auto-topup", "monthly"], description: "Monthly auto top-up is web-only for now", webPath: "billing/auto-topup/monthly", reason: "Recurring payment setup needs a payment-flow audit before CLI support.", examples: ["openmates settings billing auto-topup monthly"] },
7718
- { path: ["billing", "invoices"], description: "Invoice management is web-only for now", webPath: "billing/invoices", reason: "Invoice download support needs auth-gated file streaming parity first.", examples: ["openmates settings billing invoices"] },
7719
7873
  { path: ["privacy", "personal-data"], description: "Personal data management is not CLI-ready yet", webPath: "privacy/hide-personal-data", reason: "The CLI needs a dedicated encrypted personal-data UX before exposing writes.", examples: ["openmates settings privacy personal-data"] },
7720
- { path: ["notifications"], description: "Notification preferences are not CLI-ready yet", webPath: "notifications", reason: "Backend preference endpoints need an audit before terminal writes are exposed.", examples: ["openmates settings notifications"] },
7721
7874
  { path: ["shared", "tip"], description: "Tips are web-only", webPath: "shared/tip", reason: "Payment checkout must use the browser/payment provider UI.", examples: ["openmates settings shared tip"] },
7722
- { path: ["mates"], description: "Mate browsing is available through mentions for now", webPath: "mates", reason: "Use @mate mentions in chat; rich mate settings remain browser-first.", examples: ["openmates mentions list --type mate"] },
7723
7875
  { path: ["developers", "api-keys", "create"], description: "API key creation is web-only", webPath: "developers/api-keys", reason: "API key secrets are shown once and need the browser approval flow.", examples: ["openmates settings developers api-keys create"] },
7724
7876
  { path: ["developers", "devices"], description: "Developer devices are web-only", webPath: "developers/devices", reason: "Device approvals and revocations are sensitive.", examples: ["openmates settings developers devices"] },
7725
7877
  { path: ["developers", "webhooks"], description: "Developer webhooks are not CLI-ready yet", webPath: "developers/webhooks", reason: "Webhook CRUD needs a backend/API audit before CLI support.", examples: ["openmates settings developers webhooks"] },
7726
7878
  { path: ["support"], description: "Support payments are web-only", webPath: "support", reason: "Payment flows must use the browser/payment provider UI.", examples: ["openmates settings support"] },
7727
- { path: ["newsletter"], description: "Newsletter settings are not CLI-ready yet", webPath: "newsletter", reason: "Newsletter API flow needs an audit before CLI support.", examples: ["openmates settings newsletter"] },
7728
7879
  { path: ["incognito", "info"], description: "Explain incognito mode", reason: "Incognito chats are sent without saving chat history. The CLI stores no incognito transcript.", examples: ['openmates chats incognito "Private question"'] },
7729
7880
  { path: ["server"], description: "Server admin settings are web/admin-only", webPath: "server", reason: "Use `openmates server --help` for self-hosted terminal server management.", examples: ["openmates server status"] }
7730
7881
  ];
@@ -7762,6 +7913,11 @@ function parseRequiredNumber(value, flag) {
7762
7913
  if (!Number.isFinite(parsed)) throw new Error(`Invalid ${flag}: ${value}`);
7763
7914
  return parsed;
7764
7915
  }
7916
+ function parseOptionalBoolean(value, fallback, label) {
7917
+ if (value === void 0) return fallback;
7918
+ if (typeof value === "boolean") return value;
7919
+ return parseOnOff(value, label);
7920
+ }
7765
7921
  function parseDataOrFlags(flags, booleanFlags) {
7766
7922
  if (typeof flags.data === "string") return JSON.parse(flags.data);
7767
7923
  const body = {};
@@ -7873,6 +8029,37 @@ function parseYamlScalar(value) {
7873
8029
  }
7874
8030
  return trimmed;
7875
8031
  }
8032
+ async function saveDownloadedDocument(document, output) {
8033
+ const { mkdir, writeFile } = await import("fs/promises");
8034
+ const { join: join4, basename: basename2, dirname } = await import("path");
8035
+ const target = typeof output === "string" ? output : ".";
8036
+ const filename = basename2(document.filename || "document.pdf");
8037
+ const filePath = target.endsWith(".pdf") ? target : join4(target, filename);
8038
+ await mkdir(dirname(filePath), { recursive: true });
8039
+ await writeFile(filePath, document.data);
8040
+ return filePath;
8041
+ }
8042
+ function printMates(json) {
8043
+ const mates = Object.entries(MATE_NAMES).map(([id, name]) => ({ id, name, mention: `@mate:${id}` }));
8044
+ if (json) {
8045
+ printJson2(mates);
8046
+ return;
8047
+ }
8048
+ header("Mates");
8049
+ for (const mate of mates) console.log(`${mate.id.padEnd(28)} ${mate.name.padEnd(10)} ${mate.mention}`);
8050
+ }
8051
+ function printMateInfo(mateId, json) {
8052
+ const name = MATE_NAMES[mateId];
8053
+ if (!name) throw new Error(`Unknown mate '${mateId}'. Run 'openmates settings mates list'.`);
8054
+ const data = { id: mateId, name, mention: `@mate:${mateId}` };
8055
+ if (json) {
8056
+ printJson2(data);
8057
+ return;
8058
+ }
8059
+ header(`${name} (${mateId})`);
8060
+ console.log(`Mention: ${data.mention}`);
8061
+ console.log(`Use: openmates chats send "${data.mention} <message>"`);
8062
+ }
7876
8063
  async function confirmOrExit(question) {
7877
8064
  const rl = await import("readline");
7878
8065
  const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
@@ -7957,6 +8144,18 @@ async function handleSettings(client, subcommand, rest, flags) {
7957
8144
  );
7958
8145
  return;
7959
8146
  }
8147
+ if (matches(tokens, ["account", "username", "set"])) {
8148
+ const username = rest[2];
8149
+ if (!username) throw new Error("Missing username. Example: openmates settings account username set alice_123");
8150
+ await printSettingsMutationResult(client.updateUsername(username), flags);
8151
+ return;
8152
+ }
8153
+ if (matches(tokens, ["account", "profile-picture", "set"])) {
8154
+ const file = rest[2];
8155
+ if (!file) throw new Error("Missing image file. Example: openmates settings account profile-picture set ./avatar.jpg");
8156
+ await printSettingsMutationResult(client.updateProfileImage(file), flags);
8157
+ return;
8158
+ }
7960
8159
  if (matches(tokens, ["account", "chats", "stats"])) {
7961
8160
  await printSettingsResult(client.settingsGet("chats"), flags);
7962
8161
  return;
@@ -8046,6 +8245,35 @@ async function handleSettings(client, subcommand, rest, flags) {
8046
8245
  await printSettingsResult(client.settingsGet("usage"), flags);
8047
8246
  return;
8048
8247
  }
8248
+ if (matches(tokens, ["billing", "invoices", "list"])) {
8249
+ await printSettingsResult(client.listInvoices(), flags);
8250
+ return;
8251
+ }
8252
+ if (matches(tokens, ["billing", "invoices", "download"])) {
8253
+ const invoiceId = rest[2];
8254
+ if (!invoiceId) throw new Error("Missing invoice ID.");
8255
+ const document = await client.downloadInvoice(invoiceId);
8256
+ const filePath = await saveDownloadedDocument(document, flags.output);
8257
+ if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
8258
+ else console.log(`\x1B[32m\u2713\x1B[0m Invoice saved to ${filePath}`);
8259
+ return;
8260
+ }
8261
+ if (matches(tokens, ["billing", "invoices", "credit-note"])) {
8262
+ const invoiceId = rest[2];
8263
+ if (!invoiceId) throw new Error("Missing invoice ID.");
8264
+ const document = await client.downloadCreditNote(invoiceId);
8265
+ const filePath = await saveDownloadedDocument(document, flags.output);
8266
+ if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
8267
+ else console.log(`\x1B[32m\u2713\x1B[0m Credit note saved to ${filePath}`);
8268
+ return;
8269
+ }
8270
+ if (matches(tokens, ["billing", "invoices", "refund"])) {
8271
+ const invoiceId = rest[2];
8272
+ if (!invoiceId) throw new Error("Missing invoice ID.");
8273
+ if (flags.yes !== true) await confirmOrExit(`Request refund for invoice ${invoiceId}? [y/N] `);
8274
+ await printSettingsMutationResult(client.requestRefund(invoiceId), flags);
8275
+ return;
8276
+ }
8049
8277
  if (matches(tokens, ["billing", "gift-card", "redeem"]) || subcommand === "gift-card" && rest[0] === "redeem") {
8050
8278
  const code = matches(tokens, ["billing", "gift-card", "redeem"]) ? rest[2] : rest[1];
8051
8279
  if (!code) throw new Error("Missing gift card code.");
@@ -8079,6 +8307,48 @@ async function handleSettings(client, subcommand, rest, flags) {
8079
8307
  );
8080
8308
  return;
8081
8309
  }
8310
+ if (matches(tokens, ["notifications", "status"])) {
8311
+ const user = await client.whoAmI();
8312
+ const status = {
8313
+ enabled: user.email_notifications_enabled ?? false,
8314
+ preferences: user.email_notification_preferences ?? {},
8315
+ backup_reminder_interval_days: user.backup_reminder_interval_days ?? null,
8316
+ encrypted_notification_email_configured: Boolean(user.encrypted_notification_email)
8317
+ };
8318
+ flags.json === true ? printJson2(status) : printGenericObject(status);
8319
+ return;
8320
+ }
8321
+ if (matches(tokens, ["notifications", "email", "set"])) {
8322
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "email notifications");
8323
+ const email = typeof flags.email === "string" ? flags.email : null;
8324
+ if (enabled && !email) throw new Error("Provide --email when enabling email notifications.");
8325
+ const preferences = {
8326
+ aiResponses: parseOptionalBoolean(flags["ai-responses"], true, "AI response notifications"),
8327
+ backupReminder: parseOptionalBoolean(flags["backup-reminder"], false, "backup reminder notifications"),
8328
+ webhookChats: parseOptionalBoolean(flags["webhook-chats"], false, "webhook chat notifications")
8329
+ };
8330
+ await printSettingsMutationResult(
8331
+ client.updateEmailNotificationSettings({ enabled, email, preferences }),
8332
+ flags
8333
+ );
8334
+ return;
8335
+ }
8336
+ if (matches(tokens, ["notifications", "backup", "set"])) {
8337
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "backup reminders");
8338
+ const email = typeof flags.email === "string" ? flags.email : null;
8339
+ const interval = parseRequiredNumber(flags.interval, "--interval");
8340
+ if (enabled && !email) throw new Error("Provide --email when enabling backup reminders.");
8341
+ await printSettingsMutationResult(
8342
+ client.updateEmailNotificationSettings({
8343
+ enabled,
8344
+ email,
8345
+ preferences: { aiResponses: false, backupReminder: enabled },
8346
+ backup_reminder_interval_days: interval
8347
+ }),
8348
+ flags
8349
+ );
8350
+ return;
8351
+ }
8082
8352
  if (matches(tokens, ["reminders", "list"])) {
8083
8353
  await printSettingsResult(client.settingsGet("reminders"), flags);
8084
8354
  return;
@@ -8121,6 +8391,54 @@ async function handleSettings(client, subcommand, rest, flags) {
8121
8391
  await printSettingsResult(client.settingsGet(`issues/${id}/status`), flags);
8122
8392
  return;
8123
8393
  }
8394
+ if (matches(tokens, ["mates", "list"])) {
8395
+ printMates(flags.json === true);
8396
+ return;
8397
+ }
8398
+ if (matches(tokens, ["mates", "info"])) {
8399
+ const mateId = rest[1];
8400
+ if (!mateId) throw new Error("Missing mate ID. Example: openmates settings mates info software_development");
8401
+ printMateInfo(mateId, flags.json === true);
8402
+ return;
8403
+ }
8404
+ if (matches(tokens, ["mates", "consent"])) {
8405
+ if (flags.yes !== true) await confirmOrExit("Record consent for mate settings? [y/N] ");
8406
+ await printSettingsMutationResult(client.settingsPost("user/consent/mates", { consent: true }), flags);
8407
+ return;
8408
+ }
8409
+ if (matches(tokens, ["newsletter", "categories"]) && tokens.length === 2) {
8410
+ await printSettingsResult(client.getNewsletterCategories(), flags);
8411
+ return;
8412
+ }
8413
+ if (matches(tokens, ["newsletter", "categories", "set"])) {
8414
+ const categories = {
8415
+ updates_and_announcements: parseOptionalBoolean(flags.updates, true, "updates newsletter"),
8416
+ tips_and_tricks: parseOptionalBoolean(flags.tips, true, "tips newsletter"),
8417
+ daily_inspirations: parseOptionalBoolean(flags.daily, true, "daily inspirations newsletter")
8418
+ };
8419
+ await printSettingsMutationResult(client.updateNewsletterCategories(categories), flags);
8420
+ return;
8421
+ }
8422
+ if (matches(tokens, ["newsletter", "subscribe"])) {
8423
+ const email = rest[1];
8424
+ if (!email) throw new Error("Missing email. Example: openmates settings newsletter subscribe you@example.com");
8425
+ const language = typeof flags.language === "string" ? flags.language : "en";
8426
+ const darkmode = parseOptionalBoolean(flags.darkmode, false, "newsletter dark mode");
8427
+ await printSettingsMutationResult(client.subscribeNewsletter(email, language, darkmode), flags);
8428
+ return;
8429
+ }
8430
+ if (matches(tokens, ["newsletter", "confirm"])) {
8431
+ const token = rest[1];
8432
+ if (!token) throw new Error("Missing confirmation token.");
8433
+ await printSettingsMutationResult(client.confirmNewsletter(token), flags);
8434
+ return;
8435
+ }
8436
+ if (matches(tokens, ["newsletter", "unsubscribe"])) {
8437
+ const token = rest[1];
8438
+ if (!token) throw new Error("Missing unsubscribe token.");
8439
+ await printSettingsMutationResult(client.unsubscribeNewsletter(token), flags);
8440
+ return;
8441
+ }
8124
8442
  if (subcommand === "memories") {
8125
8443
  await handleMemories(client, rest, flags);
8126
8444
  return;
@@ -0,0 +1,131 @@
1
+ // src/uploadService.ts
2
+ import { readFileSync } from "fs";
3
+ import { basename, extname } from "path";
4
+ var UPLOAD_MAX_ATTEMPTS = 3;
5
+ var UPLOAD_RETRY_DELAY_MS = 2e3;
6
+ var PROFILE_IMAGE_MAX_SIZE_BYTES = 300 * 1024;
7
+ function getUploadUrl(apiUrl) {
8
+ try {
9
+ const url = new URL(apiUrl);
10
+ if (url.hostname === "localhost") return "http://localhost:8001";
11
+ } catch {
12
+ }
13
+ return "https://upload.openmates.org";
14
+ }
15
+ function getUploadOrigin(apiUrl) {
16
+ try {
17
+ const url = new URL(apiUrl);
18
+ if (url.hostname === "localhost") return "http://localhost:5173";
19
+ if (url.hostname.startsWith("api.")) {
20
+ return `${url.protocol}//app.${url.hostname.slice(4)}`;
21
+ }
22
+ } catch {
23
+ }
24
+ return "https://app.openmates.org";
25
+ }
26
+ async function uploadFile(filePath, session) {
27
+ const filename = basename(filePath);
28
+ const fileBytes = readFileSync(filePath);
29
+ const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/file`;
30
+ const origin = getUploadOrigin(session.apiUrl);
31
+ const cookies = [];
32
+ if (session.cookies?.auth_refresh_token) {
33
+ cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
34
+ }
35
+ let response;
36
+ let lastError;
37
+ for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
38
+ try {
39
+ const blob = new Blob([fileBytes]);
40
+ const formData = new FormData();
41
+ formData.append("file", blob, filename);
42
+ response = await fetch(uploadUrl, {
43
+ method: "POST",
44
+ body: formData,
45
+ headers: {
46
+ Origin: origin,
47
+ ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
48
+ },
49
+ signal: AbortSignal.timeout(10 * 60 * 1e3)
50
+ // 10-minute timeout
51
+ });
52
+ break;
53
+ } catch (error) {
54
+ lastError = error;
55
+ if (attempt === UPLOAD_MAX_ATTEMPTS) break;
56
+ await new Promise((resolve) => setTimeout(resolve, UPLOAD_RETRY_DELAY_MS));
57
+ }
58
+ }
59
+ if (!response) {
60
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
61
+ throw new Error(message || "Upload request failed.");
62
+ }
63
+ if (!response.ok) {
64
+ const status = response.status;
65
+ let errorMessage;
66
+ switch (status) {
67
+ case 401:
68
+ errorMessage = "Authentication failed. Run `openmates login` to re-authenticate.";
69
+ break;
70
+ case 413:
71
+ errorMessage = "File too large (maximum 100 MB).";
72
+ break;
73
+ case 415:
74
+ errorMessage = "Unsupported file type.";
75
+ break;
76
+ case 422: {
77
+ const body = await response.text().catch(() => "");
78
+ errorMessage = body.includes("malware") ? "File rejected: malware detected." : body.includes("content_safety") ? "File rejected: content safety violation." : `Upload validation failed: ${body}`;
79
+ break;
80
+ }
81
+ case 429:
82
+ errorMessage = "Upload rate limit exceeded. Try again in a minute.";
83
+ break;
84
+ default:
85
+ errorMessage = `Upload failed (HTTP ${status}).`;
86
+ }
87
+ throw new Error(errorMessage);
88
+ }
89
+ const data = await response.json();
90
+ return data;
91
+ }
92
+ function getProfileImageMime(filename) {
93
+ const ext = extname(filename).toLowerCase();
94
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
95
+ if (ext === ".png") return "image/png";
96
+ throw new Error("Profile images must be JPEG or PNG files.");
97
+ }
98
+ async function uploadProfileImage(filePath, session) {
99
+ const filename = basename(filePath);
100
+ const fileBytes = readFileSync(filePath);
101
+ const contentType = getProfileImageMime(filename);
102
+ if (fileBytes.byteLength > PROFILE_IMAGE_MAX_SIZE_BYTES) {
103
+ throw new Error("Profile image must be 300 KB or smaller. Resize/compress the image and try again.");
104
+ }
105
+ const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/profile-image`;
106
+ const origin = getUploadOrigin(session.apiUrl);
107
+ const cookies = [];
108
+ if (session.cookies?.auth_refresh_token) {
109
+ cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
110
+ }
111
+ const formData = new FormData();
112
+ formData.append("file", new Blob([fileBytes], { type: contentType }), filename);
113
+ const response = await fetch(uploadUrl, {
114
+ method: "POST",
115
+ body: formData,
116
+ headers: {
117
+ Origin: origin,
118
+ ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
119
+ }
120
+ });
121
+ const data = await response.json().catch(() => ({}));
122
+ if (!response.ok && !data.status) {
123
+ throw new Error(data.detail ?? `Profile image upload failed (HTTP ${response.status}).`);
124
+ }
125
+ return data;
126
+ }
127
+
128
+ export {
129
+ uploadFile,
130
+ uploadProfileImage
131
+ };
package/dist/cli.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import {
3
3
  getExtForLang,
4
4
  serializeToYaml
5
- } from "./chunk-T77MVTFX.js";
5
+ } from "./chunk-QJE5DL63.js";
6
+ import "./chunk-SFTCIVE2.js";
6
7
  export {
7
8
  getExtForLang,
8
9
  serializeToYaml
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ interface OpenMatesSession {
7
7
  wsToken: string | null;
8
8
  cookies: Record<string, string>;
9
9
  masterKeyExportedB64: string;
10
+ emailEncryptionKeyB64?: string | null;
10
11
  hashedEmail: string;
11
12
  userEmailSalt: string;
12
13
  createdAt: number;
@@ -293,6 +294,23 @@ interface OpenMatesClientOptions {
293
294
  apiUrl?: string;
294
295
  session?: OpenMatesSession;
295
296
  }
297
+ interface InvoiceListItem {
298
+ id: string;
299
+ order_id?: string | null;
300
+ date: string;
301
+ amount: string;
302
+ credits_purchased: number;
303
+ filename: string;
304
+ is_gift_card?: boolean;
305
+ refunded_at?: string | null;
306
+ refund_status?: string | null;
307
+ currency?: string | null;
308
+ provider?: string | null;
309
+ }
310
+ interface DownloadedDocument {
311
+ filename: string;
312
+ data: Uint8Array;
313
+ }
296
314
  /**
297
315
  * Derive the web app URL from the API URL so the pair token is always looked
298
316
  * up on the same backend the CLI created it on.
@@ -467,6 +485,25 @@ declare class OpenMatesClient {
467
485
  message: string;
468
486
  }>;
469
487
  listRedeemedGiftCards(): Promise<unknown>;
488
+ listInvoices(): Promise<{
489
+ invoices: InvoiceListItem[];
490
+ }>;
491
+ downloadInvoice(invoiceId: string): Promise<DownloadedDocument>;
492
+ downloadCreditNote(invoiceId: string): Promise<DownloadedDocument>;
493
+ requestRefund(invoiceId: string): Promise<unknown>;
494
+ updateUsername(username: string): Promise<unknown>;
495
+ updateProfileImage(filePath: string): Promise<unknown>;
496
+ getNewsletterCategories(): Promise<unknown>;
497
+ updateNewsletterCategories(categories: Record<string, boolean>): Promise<unknown>;
498
+ subscribeNewsletter(email: string, language?: string, darkmode?: boolean): Promise<unknown>;
499
+ confirmNewsletter(token: string): Promise<unknown>;
500
+ unsubscribeNewsletter(token: string): Promise<unknown>;
501
+ updateEmailNotificationSettings(payload: {
502
+ enabled: boolean;
503
+ email?: string | null;
504
+ preferences: Record<string, boolean>;
505
+ backup_reminder_interval_days?: number;
506
+ }): Promise<unknown>;
470
507
  /**
471
508
  * Fetch today's daily inspirations.
472
509
  *
@@ -600,6 +637,9 @@ declare class OpenMatesClient {
600
637
  */
601
638
  buildMentionContext(): Promise<MentionContext>;
602
639
  private normalizePath;
640
+ private downloadPaymentPdf;
641
+ private ensureEmailEncryptionKey;
642
+ private hydrateEmailEncryptionKey;
603
643
  private requireSession;
604
644
  private getMasterKeyBytes;
605
645
  private getValidSessionFromDisk;
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@ import {
6
6
  getExtForLang,
7
7
  parseNewChatSuggestionText,
8
8
  serializeToYaml
9
- } from "./chunk-T77MVTFX.js";
9
+ } from "./chunk-QJE5DL63.js";
10
+ import "./chunk-SFTCIVE2.js";
10
11
  export {
11
12
  MATE_NAMES,
12
13
  MEMORY_TYPE_REGISTRY,
@@ -0,0 +1,8 @@
1
+ import {
2
+ uploadFile,
3
+ uploadProfileImage
4
+ } from "./chunk-SFTCIVE2.js";
5
+ export {
6
+ uploadFile,
7
+ uploadProfileImage
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.11.0-alpha.7",
3
+ "version": "0.11.0-alpha.9",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",