openmates 0.11.0-alpha.6 → 0.11.0-alpha.8

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";
@@ -180,12 +184,29 @@ var OpenMatesHttpClient = class {
180
184
  async post(path, body, headers = {}) {
181
185
  return this.request("POST", path, body, headers);
182
186
  }
183
- async delete(path, headers = {}) {
184
- return this.request("DELETE", path, void 0, headers);
187
+ async delete(path, body, headers = {}) {
188
+ return this.request("DELETE", path, body, headers);
185
189
  }
186
190
  async patch(path, body, headers = {}) {
187
191
  return this.request("PATCH", path, body, headers);
188
192
  }
193
+ async getBinary(path, headers = {}) {
194
+ const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
195
+ const requestHeaders = {
196
+ Accept: "application/pdf,application/octet-stream",
197
+ ...headers
198
+ };
199
+ const cookieHeader = this.formatCookieHeader();
200
+ if (cookieHeader) requestHeaders.Cookie = cookieHeader;
201
+ const response = await fetch(url, { method: "GET", headers: requestHeaders });
202
+ this.captureCookies(response);
203
+ return {
204
+ ok: response.ok,
205
+ status: response.status,
206
+ data: new Uint8Array(await response.arrayBuffer()),
207
+ headers: response.headers
208
+ };
209
+ }
189
210
  async request(method, path, body, headers = {}) {
190
211
  const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
191
212
  const requestHeaders = {
@@ -603,18 +624,6 @@ function buildSession(onDisk, masterKey) {
603
624
  autoLogoutMinutes: onDisk.autoLogoutMinutes
604
625
  };
605
626
  }
606
- function loadIncognitoHistory() {
607
- const filePath = join(ensureStateDir(), "incognito.json");
608
- return readJsonFile(filePath) ?? [];
609
- }
610
- function saveIncognitoHistory(items) {
611
- const filePath = join(ensureStateDir(), "incognito.json");
612
- writeJsonFile(filePath, items);
613
- }
614
- function clearIncognitoHistory() {
615
- const filePath = join(ensureStateDir(), "incognito.json");
616
- writeJsonFile(filePath, []);
617
- }
618
627
  var SYNC_CACHE_FILE = "sync_cache.json";
619
628
  function saveSyncCache(cache) {
620
629
  const filePath = join(ensureStateDir(), SYNC_CACHE_FILE);
@@ -1718,7 +1727,11 @@ var BLOCKED_SETTINGS_MUTATE_PATHS = /* @__PURE__ */ new Set([
1718
1727
  "/v1/auth/setup_password",
1719
1728
  "/v1/auth/2fa/setup/initiate",
1720
1729
  "/v1/auth/2fa/setup/provider",
1721
- "/v1/auth/2fa/setup/verify-signup"
1730
+ "/v1/auth/2fa/setup/verify-signup",
1731
+ "/v1/settings/delete-account",
1732
+ "/v1/settings/request-action-verification",
1733
+ "/v1/settings/verify-action-code",
1734
+ "/v1/settings/user/disable-2fa"
1722
1735
  ]);
1723
1736
  var OpenMatesClient = class _OpenMatesClient {
1724
1737
  apiUrl;
@@ -1834,7 +1847,6 @@ var OpenMatesClient = class _OpenMatesClient {
1834
1847
  await this.http.post("/v1/auth/logout", {}, this.getCliRequestHeaders()).catch(() => void 0);
1835
1848
  }
1836
1849
  clearSession();
1837
- clearIncognitoHistory();
1838
1850
  }
1839
1851
  // -------------------------------------------------------------------------
1840
1852
  // Chats
@@ -2375,12 +2387,6 @@ var OpenMatesClient = class _OpenMatesClient {
2375
2387
  let followUpSuggestions = [];
2376
2388
  const streamOpts = { onStream: params.onStream };
2377
2389
  if (params.incognito) {
2378
- const history = loadIncognitoHistory();
2379
- history.push({
2380
- role: "user",
2381
- content: params.message,
2382
- createdAt: Date.now()
2383
- });
2384
2390
  try {
2385
2391
  const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
2386
2392
  assistant = resp.content;
@@ -2389,12 +2395,6 @@ var OpenMatesClient = class _OpenMatesClient {
2389
2395
  } finally {
2390
2396
  ws.close();
2391
2397
  }
2392
- history.push({
2393
- role: "assistant",
2394
- content: assistant,
2395
- createdAt: Date.now()
2396
- });
2397
- saveIncognitoHistory(history);
2398
2398
  } else {
2399
2399
  try {
2400
2400
  const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
@@ -2416,12 +2416,6 @@ var OpenMatesClient = class _OpenMatesClient {
2416
2416
  followUpSuggestions
2417
2417
  };
2418
2418
  }
2419
- getIncognitoHistory() {
2420
- return loadIncognitoHistory();
2421
- }
2422
- clearIncognitoHistory() {
2423
- clearIncognitoHistory();
2424
- }
2425
2419
  /**
2426
2420
  * Delete a chat by ID.
2427
2421
  *
@@ -2734,7 +2728,7 @@ var OpenMatesClient = class _OpenMatesClient {
2734
2728
  }
2735
2729
  return response.data;
2736
2730
  }
2737
- async settingsDelete(path) {
2731
+ async settingsDelete(path, body) {
2738
2732
  this.requireSession();
2739
2733
  const normalizedPath = this.normalizePath(path);
2740
2734
  if (BLOCKED_SETTINGS_MUTATE_PATHS.has(normalizedPath)) {
@@ -2742,6 +2736,7 @@ var OpenMatesClient = class _OpenMatesClient {
2742
2736
  }
2743
2737
  const response = await this.http.delete(
2744
2738
  normalizedPath,
2739
+ body,
2745
2740
  this.getCliRequestHeaders()
2746
2741
  );
2747
2742
  if (!response.ok) {
@@ -2793,6 +2788,123 @@ var OpenMatesClient = class _OpenMatesClient {
2793
2788
  }
2794
2789
  return response.data;
2795
2790
  }
2791
+ async listInvoices() {
2792
+ this.requireSession();
2793
+ const response = await this.http.get(
2794
+ "/v1/payments/invoices",
2795
+ this.getCliRequestHeaders()
2796
+ );
2797
+ if (!response.ok) {
2798
+ throw new Error(`Failed to fetch invoices (HTTP ${response.status})`);
2799
+ }
2800
+ return { invoices: response.data.invoices ?? [] };
2801
+ }
2802
+ async downloadInvoice(invoiceId) {
2803
+ return this.downloadPaymentPdf(
2804
+ `/v1/payments/invoices/${encodeURIComponent(invoiceId)}/download`,
2805
+ `Invoice_${invoiceId}.pdf`
2806
+ );
2807
+ }
2808
+ async downloadCreditNote(invoiceId) {
2809
+ return this.downloadPaymentPdf(
2810
+ `/v1/payments/invoices/${encodeURIComponent(invoiceId)}/credit-note/download`,
2811
+ `CreditNote_${invoiceId}.pdf`
2812
+ );
2813
+ }
2814
+ async requestRefund(invoiceId, emailEncryptionKey) {
2815
+ this.requireSession();
2816
+ const response = await this.http.post(
2817
+ "/v1/payments/refund",
2818
+ { invoice_id: invoiceId, email_encryption_key: emailEncryptionKey },
2819
+ this.getCliRequestHeaders()
2820
+ );
2821
+ if (!response.ok) {
2822
+ throw new Error(`Refund request failed (HTTP ${response.status})`);
2823
+ }
2824
+ return response.data;
2825
+ }
2826
+ async updateUsername(username) {
2827
+ return this.settingsPost("user/username", { username });
2828
+ }
2829
+ async updateProfileImage(filePath) {
2830
+ const { uploadProfileImage } = await import("./uploadService-3CAJXU4L.js");
2831
+ const result = await uploadProfileImage(filePath, this.requireSession());
2832
+ if (result.status === "rejected") {
2833
+ throw new Error(result.detail ?? "Profile image rejected by content safety checks.");
2834
+ }
2835
+ if (result.status === "account_deleted") {
2836
+ throw new Error("Account deleted due to repeated profile image policy violations.");
2837
+ }
2838
+ if (result.status !== "ok") {
2839
+ throw new Error(result.detail ?? `Profile image upload failed with status '${result.status}'.`);
2840
+ }
2841
+ return result;
2842
+ }
2843
+ async getNewsletterCategories() {
2844
+ this.requireSession();
2845
+ const response = await this.http.get(
2846
+ "/v1/newsletter/categories",
2847
+ this.getCliRequestHeaders()
2848
+ );
2849
+ if (!response.ok) {
2850
+ throw new Error(`Failed to fetch newsletter categories (HTTP ${response.status})`);
2851
+ }
2852
+ return response.data;
2853
+ }
2854
+ async updateNewsletterCategories(categories) {
2855
+ this.requireSession();
2856
+ const response = await this.http.patch(
2857
+ "/v1/newsletter/categories",
2858
+ { categories },
2859
+ this.getCliRequestHeaders()
2860
+ );
2861
+ if (!response.ok) {
2862
+ throw new Error(`Failed to update newsletter categories (HTTP ${response.status})`);
2863
+ }
2864
+ return response.data;
2865
+ }
2866
+ async subscribeNewsletter(email, language = "en", darkmode = false) {
2867
+ const response = await this.http.post(
2868
+ "/v1/newsletter/subscribe",
2869
+ { email, language, darkmode },
2870
+ this.getCliRequestHeaders()
2871
+ );
2872
+ if (!response.ok) {
2873
+ throw new Error(`Newsletter subscribe failed (HTTP ${response.status})`);
2874
+ }
2875
+ return response.data;
2876
+ }
2877
+ async confirmNewsletter(token) {
2878
+ const response = await this.http.get(
2879
+ `/v1/newsletter/confirm/${encodeURIComponent(token)}`,
2880
+ this.getCliRequestHeaders()
2881
+ );
2882
+ if (!response.ok) {
2883
+ throw new Error(`Newsletter confirmation failed (HTTP ${response.status})`);
2884
+ }
2885
+ return response.data;
2886
+ }
2887
+ async unsubscribeNewsletter(token) {
2888
+ const response = await this.http.get(
2889
+ `/v1/newsletter/unsubscribe/${encodeURIComponent(token)}`,
2890
+ this.getCliRequestHeaders()
2891
+ );
2892
+ if (!response.ok) {
2893
+ throw new Error(`Newsletter unsubscribe failed (HTTP ${response.status})`);
2894
+ }
2895
+ return response.data;
2896
+ }
2897
+ async updateEmailNotificationSettings(payload) {
2898
+ const { ws } = await this.openWsClient();
2899
+ try {
2900
+ const ackPromise = ws.waitForMessage("email_notification_settings_ack");
2901
+ ws.send("email_notification_settings", payload);
2902
+ const ack = await ackPromise;
2903
+ return ack.payload;
2904
+ } finally {
2905
+ ws.close();
2906
+ }
2907
+ }
2796
2908
  // -------------------------------------------------------------------------
2797
2909
  // Daily Inspirations
2798
2910
  // -------------------------------------------------------------------------
@@ -3257,6 +3369,17 @@ Required: ${schema.required.join(", ")}`
3257
3369
  if (path.startsWith("/")) return path;
3258
3370
  return `/v1/settings/${path}`;
3259
3371
  }
3372
+ async downloadPaymentPdf(path, fallbackFilename) {
3373
+ this.requireSession();
3374
+ const response = await this.http.getBinary(path, this.getCliRequestHeaders());
3375
+ if (!response.ok) {
3376
+ throw new Error(`Download failed (HTTP ${response.status})`);
3377
+ }
3378
+ return {
3379
+ filename: filenameFromContentDisposition(response.headers.get("content-disposition")) ?? fallbackFilename,
3380
+ data: response.data
3381
+ };
3382
+ }
3260
3383
  requireSession() {
3261
3384
  if (!this.session) {
3262
3385
  throw new Error("Not logged in. Run `openmates login`.");
@@ -3632,6 +3755,15 @@ function parseNewChatSuggestionText(text) {
3632
3755
  const skillId = raw.slice(dashIdx + 1);
3633
3756
  return { body, appId, skillId };
3634
3757
  }
3758
+ function filenameFromContentDisposition(header2) {
3759
+ if (!header2) return null;
3760
+ const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header2)?.[1];
3761
+ if (encoded) return decodeURIComponent(encoded);
3762
+ const quoted = /filename="([^"]+)"/i.exec(header2)?.[1];
3763
+ if (quoted) return quoted;
3764
+ const plain = /filename=([^;]+)/i.exec(header2)?.[1];
3765
+ return plain?.trim() ?? null;
3766
+ }
3635
3767
  function sleep(ms) {
3636
3768
  return new Promise((resolve4) => setTimeout(resolve4, ms));
3637
3769
  }
@@ -3921,8 +4053,8 @@ var SecretRegistry = class {
3921
4053
  const sortedResults = [];
3922
4054
  for (const result of results) {
3923
4055
  const endIdx = result[0];
3924
- const matches = result[1];
3925
- for (const match of matches) {
4056
+ const matches2 = result[1];
4057
+ for (const match of matches2) {
3926
4058
  sortedResults.push({ endIndex: endIdx, match });
3927
4059
  }
3928
4060
  }
@@ -4845,97 +4977,6 @@ function formatEmbedsForMessage(embeds) {
4845
4977
  return "\n" + embeds.map((e) => e.referenceBlock).join("\n");
4846
4978
  }
4847
4979
 
4848
- // src/uploadService.ts
4849
- import { readFileSync as readFileSync4 } from "fs";
4850
- import { basename as basename2 } from "path";
4851
- var UPLOAD_MAX_ATTEMPTS = 3;
4852
- var UPLOAD_RETRY_DELAY_MS = 2e3;
4853
- function getUploadUrl(apiUrl) {
4854
- try {
4855
- const url = new URL(apiUrl);
4856
- if (url.hostname === "localhost") return "http://localhost:8001";
4857
- } catch {
4858
- }
4859
- return "https://upload.openmates.org";
4860
- }
4861
- function getUploadOrigin(apiUrl) {
4862
- try {
4863
- const url = new URL(apiUrl);
4864
- if (url.hostname === "localhost") return "http://localhost:5173";
4865
- if (url.hostname.startsWith("api.")) {
4866
- return `${url.protocol}//app.${url.hostname.slice(4)}`;
4867
- }
4868
- } catch {
4869
- }
4870
- return "https://app.openmates.org";
4871
- }
4872
- async function uploadFile(filePath, session) {
4873
- const filename = basename2(filePath);
4874
- const fileBytes = readFileSync4(filePath);
4875
- const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/file`;
4876
- const origin = getUploadOrigin(session.apiUrl);
4877
- const cookies = [];
4878
- if (session.cookies?.auth_refresh_token) {
4879
- cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
4880
- }
4881
- let response;
4882
- let lastError;
4883
- for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
4884
- try {
4885
- const blob = new Blob([fileBytes]);
4886
- const formData = new FormData();
4887
- formData.append("file", blob, filename);
4888
- response = await fetch(uploadUrl, {
4889
- method: "POST",
4890
- body: formData,
4891
- headers: {
4892
- Origin: origin,
4893
- ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
4894
- },
4895
- signal: AbortSignal.timeout(10 * 60 * 1e3)
4896
- // 10-minute timeout
4897
- });
4898
- break;
4899
- } catch (error) {
4900
- lastError = error;
4901
- if (attempt === UPLOAD_MAX_ATTEMPTS) break;
4902
- await new Promise((resolve4) => setTimeout(resolve4, UPLOAD_RETRY_DELAY_MS));
4903
- }
4904
- }
4905
- if (!response) {
4906
- const message = lastError instanceof Error ? lastError.message : String(lastError);
4907
- throw new Error(message || "Upload request failed.");
4908
- }
4909
- if (!response.ok) {
4910
- const status = response.status;
4911
- let errorMessage;
4912
- switch (status) {
4913
- case 401:
4914
- errorMessage = "Authentication failed. Run `openmates login` to re-authenticate.";
4915
- break;
4916
- case 413:
4917
- errorMessage = "File too large (maximum 100 MB).";
4918
- break;
4919
- case 415:
4920
- errorMessage = "Unsupported file type.";
4921
- break;
4922
- case 422: {
4923
- const body = await response.text().catch(() => "");
4924
- errorMessage = body.includes("malware") ? "File rejected: malware detected." : body.includes("content_safety") ? "File rejected: content safety violation." : `Upload validation failed: ${body}`;
4925
- break;
4926
- }
4927
- case 429:
4928
- errorMessage = "Upload rate limit exceeded. Try again in a minute.";
4929
- break;
4930
- default:
4931
- errorMessage = `Upload failed (HTTP ${status}).`;
4932
- }
4933
- throw new Error(errorMessage);
4934
- }
4935
- const data = await response.json();
4936
- return data;
4937
- }
4938
-
4939
4980
  // src/embedRenderers.ts
4940
4981
  var str = (v) => typeof v === "string" && v.length > 0 ? v : null;
4941
4982
  var DIRECT_TYPES = /* @__PURE__ */ new Set([
@@ -5985,13 +6026,13 @@ function formatTs(ts) {
5985
6026
 
5986
6027
  // src/server.ts
5987
6028
  import { execSync, spawn as nodeSpawn } from "child_process";
5988
- import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync6, rmSync as rmSync3 } from "fs";
6029
+ import { copyFileSync, existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync3 } from "fs";
5989
6030
  import { createInterface as createInterface2 } from "readline";
5990
6031
  import { homedir as homedir5 } from "os";
5991
6032
  import { join as join3, resolve as resolve3 } from "path";
5992
6033
 
5993
6034
  // src/serverConfig.ts
5994
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
6035
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5995
6036
  import { homedir as homedir4 } from "os";
5996
6037
  import { join as join2, resolve as resolve2 } from "path";
5997
6038
  var STATE_DIR = join2(homedir4(), ".openmates");
@@ -6012,7 +6053,7 @@ function loadServerConfig() {
6012
6053
  const filePath = join2(STATE_DIR, CONFIG_FILE);
6013
6054
  if (!existsSync4(filePath)) return null;
6014
6055
  try {
6015
- return JSON.parse(readFileSync5(filePath, "utf-8"));
6056
+ return JSON.parse(readFileSync4(filePath, "utf-8"));
6016
6057
  } catch {
6017
6058
  return null;
6018
6059
  }
@@ -6090,7 +6131,7 @@ function requireGit() {
6090
6131
  }
6091
6132
  function hasLlmCredentials(envPath) {
6092
6133
  if (!existsSync5(envPath)) return false;
6093
- const content = readFileSync6(envPath, "utf-8");
6134
+ const content = readFileSync5(envPath, "utf-8");
6094
6135
  for (const line of content.split("\n")) {
6095
6136
  const trimmed = line.trim();
6096
6137
  if (trimmed.startsWith("#") || !trimmed) continue;
@@ -6821,17 +6862,11 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
6821
6862
  return;
6822
6863
  }
6823
6864
  if (subcommand === "incognito-history") {
6824
- const history = client.getIncognitoHistory();
6825
- if (flags.json === true) {
6826
- printJson2(history);
6827
- } else {
6828
- printIncognitoHistory(history);
6829
- }
6865
+ printIncognitoNoHistoryNotice(flags.json === true);
6830
6866
  return;
6831
6867
  }
6832
6868
  if (subcommand === "incognito-clear") {
6833
- client.clearIncognitoHistory();
6834
- console.log("Incognito history cleared.");
6869
+ printIncognitoNoHistoryNotice(flags.json === true);
6835
6870
  return;
6836
6871
  }
6837
6872
  if (subcommand === "show") {
@@ -7699,125 +7734,652 @@ async function handleEmbeds(client, subcommand, rest, flags) {
7699
7734
  printEmbedsHelp();
7700
7735
  process.exit(1);
7701
7736
  }
7702
- async function handleSettings(client, subcommand, rest, flags) {
7703
- if (!subcommand || subcommand === "help" || flags.help === true) {
7704
- printSettingsHelp(client);
7737
+ var SETTINGS_EXECUTABLE_COMMANDS = [
7738
+ { path: ["account", "info"], description: "Show account info", examples: ["openmates settings account info --json"] },
7739
+ { path: ["account", "timezone", "set"], description: "Set account timezone", examples: ["openmates settings account timezone set Europe/Berlin"] },
7740
+ { path: ["account", "export", "manifest"], description: "Show account export manifest", examples: ["openmates settings account export manifest --json"] },
7741
+ { path: ["account", "export", "data"], description: "Fetch account export data", examples: ["openmates settings account export data --json"] },
7742
+ { 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"] },
7743
+ { path: ["account", "username", "set"], description: "Change account username", examples: ["openmates settings account username set alice_123"] },
7744
+ { path: ["account", "profile-picture", "set"], description: "Upload a profile picture", examples: ["openmates settings account profile-picture set ./avatar.jpg"] },
7745
+ { path: ["account", "chats", "stats"], description: "Show chat statistics", examples: ["openmates settings account chats stats"] },
7746
+ { path: ["account", "delete", "preview"], description: "Preview account deletion impact", examples: ["openmates settings account delete preview"] },
7747
+ { path: ["account", "storage", "overview"], description: "Show storage overview", examples: ["openmates settings account storage overview"] },
7748
+ { path: ["account", "storage", "files"], description: "List stored files", examples: ["openmates settings account storage files --category images"] },
7749
+ { path: ["account", "storage", "delete"], description: "Delete one stored file by file ID", examples: ["openmates settings account storage delete <file-id> --yes"] },
7750
+ { path: ["interface", "language", "set"], description: "Set interface language", examples: ["openmates settings interface language set en"] },
7751
+ { path: ["interface", "dark-mode", "set"], description: "Set dark mode on or off", examples: ["openmates settings interface dark-mode set on"] },
7752
+ { path: ["interface", "font", "set"], description: "Set interface font", examples: ["openmates settings interface font set lexend"] },
7753
+ { path: ["ai", "models", "set-defaults"], description: "Set default AI models", examples: ["openmates settings ai models set-defaults --simple gpt-5.4 --complex claude-opus-4-7"] },
7754
+ { path: ["privacy", "auto-delete", "chats", "set"], description: "Set chat auto-deletion period", examples: ["openmates settings privacy auto-delete chats set 90d"] },
7755
+ { path: ["privacy", "debug-logs", "share"], description: "Create a debug log sharing session", examples: ["openmates settings privacy debug-logs share --duration 1h --confirm"] },
7756
+ { path: ["billing", "overview"], description: "Show billing overview", examples: ["openmates settings billing overview"] },
7757
+ { path: ["billing", "usage"], description: "Show usage history", examples: ["openmates settings billing usage --json"] },
7758
+ { path: ["billing", "usage", "summaries"], description: "Show usage summaries", examples: ["openmates settings billing usage summaries"] },
7759
+ { path: ["billing", "usage", "daily"], description: "Show daily usage overview", examples: ["openmates settings billing usage daily"] },
7760
+ { path: ["billing", "usage", "export"], description: "Export usage data", examples: ["openmates settings billing usage export --json"] },
7761
+ { path: ["billing", "invoices", "list"], description: "List invoices", examples: ["openmates settings billing invoices list --json"] },
7762
+ { path: ["billing", "invoices", "download"], description: "Download an invoice PDF", examples: ["openmates settings billing invoices download <invoice-id> --output ./invoices"] },
7763
+ { path: ["billing", "invoices", "credit-note"], description: "Download a credit note PDF", examples: ["openmates settings billing invoices credit-note <invoice-id> --output ./invoices"] },
7764
+ { path: ["billing", "invoices", "refund"], description: "Request a refund for an invoice", examples: ["openmates settings billing invoices refund <invoice-id> --email-encryption-key <base64>"] },
7765
+ { path: ["billing", "gift-card", "redeem"], description: "Redeem a gift card", examples: ["openmates settings billing gift-card redeem ABCD-1234"] },
7766
+ { path: ["billing", "gift-card", "list"], description: "List redeemed gift cards", examples: ["openmates settings billing gift-card list"] },
7767
+ { 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"] },
7768
+ { path: ["notifications", "status"], description: "Show notification settings", examples: ["openmates settings notifications status --json"] },
7769
+ { 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"] },
7770
+ { path: ["notifications", "backup", "set"], description: "Configure backup reminder emails", examples: ["openmates settings notifications backup set --enabled true --interval 30 --email you@example.com"] },
7771
+ { path: ["reminders", "list"], description: "List active reminders", examples: ["openmates settings reminders list"] },
7772
+ { path: ["reminders", "update"], description: "Update a reminder", examples: ["openmates settings reminders update <id> --enabled false"] },
7773
+ { path: ["reminders", "delete"], description: "Delete a reminder", examples: ["openmates settings reminders delete <id> --yes"] },
7774
+ { path: ["developers", "api-keys", "list"], description: "List API keys", examples: ["openmates settings developers api-keys list"] },
7775
+ { path: ["developers", "api-keys", "revoke"], description: "Revoke an API key", examples: ["openmates settings developers api-keys revoke <key-id> --yes"] },
7776
+ { path: ["report-issue", "create"], description: "Report an issue", examples: ['openmates settings report-issue create --title "Bug" --body "What happened"'] },
7777
+ { path: ["report-issue", "status"], description: "Show issue status", examples: ["openmates settings report-issue status <issue-id>"] },
7778
+ { path: ["mates", "list"], description: "List available mates", examples: ["openmates settings mates list"] },
7779
+ { path: ["mates", "info"], description: "Show mate details", examples: ["openmates settings mates info software_development"] },
7780
+ { path: ["mates", "consent"], description: "Record mate settings consent", examples: ["openmates settings mates consent --yes"] },
7781
+ { path: ["newsletter", "categories"], description: "Show newsletter category preferences", examples: ["openmates settings newsletter categories"] },
7782
+ { path: ["newsletter", "categories", "set"], description: "Set newsletter category preferences", examples: ["openmates settings newsletter categories set --updates true --tips true --daily false"] },
7783
+ { path: ["newsletter", "subscribe"], description: "Subscribe an email to the newsletter", examples: ["openmates settings newsletter subscribe you@example.com --language en"] },
7784
+ { path: ["newsletter", "confirm"], description: "Confirm newsletter subscription token", examples: ["openmates settings newsletter confirm <token>"] },
7785
+ { path: ["newsletter", "unsubscribe"], description: "Unsubscribe with newsletter token", examples: ["openmates settings newsletter unsubscribe <token>"] },
7786
+ { path: ["memories"], description: "Manage encrypted memories", examples: ["openmates settings memories list", `openmates settings memories create --app-id code --item-type projects --data '{"name":"OpenMates"}'`] }
7787
+ ];
7788
+ var SETTINGS_INFO_COMMANDS = [
7789
+ { 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"] },
7790
+ { 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"] },
7791
+ { 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"] },
7792
+ { path: ["security", "passkeys"], description: "Passkeys are web-only", webPath: "account/security/passkeys", reason: "Passkeys require WebAuthn browser APIs.", examples: ["openmates settings security passkeys"] },
7793
+ { path: ["security", "password"], description: "Password changes are web-only", webPath: "account/security/password", reason: "The CLI never asks for account credentials.", examples: ["openmates settings security password"] },
7794
+ { path: ["security", "2fa"], description: "2FA setup and changes are web-only", webPath: "account/security/2fa", reason: "2FA setup requires a guided browser verification flow.", examples: ["openmates settings security 2fa"] },
7795
+ { path: ["security", "recovery-key"], description: "Recovery key settings are web-only", webPath: "account/security/recovery-key", reason: "Recovery keys are a high-risk account recovery surface.", examples: ["openmates settings security recovery-key"] },
7796
+ { path: ["security", "sessions"], description: "Session management is web-only", webPath: "account/security/sessions", reason: "The CLI is a paired restricted session; approval and revocation stay in the browser.", examples: ["openmates settings security sessions"] },
7797
+ { 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"] },
7798
+ { 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"] },
7799
+ { 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"] },
7800
+ { 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"] },
7801
+ { 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"] },
7802
+ { 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"] },
7803
+ { path: ["developers", "devices"], description: "Developer devices are web-only", webPath: "developers/devices", reason: "Device approvals and revocations are sensitive.", examples: ["openmates settings developers devices"] },
7804
+ { 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"] },
7805
+ { path: ["support"], description: "Support payments are web-only", webPath: "support", reason: "Payment flows must use the browser/payment provider UI.", examples: ["openmates settings support"] },
7806
+ { 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"'] },
7807
+ { 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"] }
7808
+ ];
7809
+ function matches(actual, expected) {
7810
+ return expected.every((part, index) => actual[index] === part);
7811
+ }
7812
+ function findSettingsInfoCommand(tokens) {
7813
+ const all = [...SETTINGS_INFO_COMMANDS, ...SETTINGS_EXECUTABLE_COMMANDS];
7814
+ return all.sort((a, b) => b.path.length - a.path.length).find((command) => matches(tokens, command.path)) ?? null;
7815
+ }
7816
+ async function printSettingsResult(resultPromise, flags) {
7817
+ const result = await resultPromise;
7818
+ flags.json === true ? printJson2(result) : printGenericObject(result);
7819
+ }
7820
+ async function printSettingsMutationResult(resultPromise, flags) {
7821
+ const result = await resultPromise;
7822
+ if (flags.json === true) {
7823
+ printJson2(result);
7705
7824
  return;
7706
7825
  }
7707
- if (subcommand === "get") {
7708
- const path = rest[0];
7709
- if (!path) {
7710
- console.error("Missing path.\n");
7711
- printSettingsHelp();
7712
- process.exit(1);
7826
+ process.stdout.write("\x1B[32m\u2713\x1B[0m Settings updated\n");
7827
+ if (result && typeof result === "object") printGenericObject(result);
7828
+ }
7829
+ function addQueryParam(params, key, value) {
7830
+ if (typeof value === "string" && value.length > 0) params.set(key, value);
7831
+ }
7832
+ function parseOnOff(value, label) {
7833
+ if (value === "on" || value === "true" || value === "1") return true;
7834
+ if (value === "off" || value === "false" || value === "0") return false;
7835
+ throw new Error(`Invalid ${label} value '${value ?? ""}'. Use on/off or true/false.`);
7836
+ }
7837
+ function parseRequiredNumber(value, flag) {
7838
+ if (typeof value !== "string") throw new Error(`Missing ${flag}.`);
7839
+ const parsed = Number(value);
7840
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid ${flag}: ${value}`);
7841
+ return parsed;
7842
+ }
7843
+ function parseOptionalBoolean(value, fallback, label) {
7844
+ if (value === void 0) return fallback;
7845
+ if (typeof value === "boolean") return value;
7846
+ return parseOnOff(value, label);
7847
+ }
7848
+ function parseDataOrFlags(flags, booleanFlags) {
7849
+ if (typeof flags.data === "string") return JSON.parse(flags.data);
7850
+ const body = {};
7851
+ for (const key of booleanFlags) {
7852
+ if (flags[key] !== void 0) body[key] = parseOnOff(String(flags[key]), key);
7853
+ }
7854
+ if (Object.keys(body).length === 0) throw new Error("Provide --data '<json>' or a supported flag.");
7855
+ return body;
7856
+ }
7857
+ function parseChatImportPayload(raw) {
7858
+ const trimmed = raw.trim();
7859
+ if (!trimmed) throw new Error("Import file is empty.");
7860
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
7861
+ const parsed = JSON.parse(trimmed);
7862
+ if (Array.isArray(parsed)) return { chats: parsed };
7863
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.chats)) {
7864
+ return parsed;
7865
+ }
7866
+ if (parsed && typeof parsed === "object") {
7867
+ const object = parsed;
7868
+ if (object.chat || object.messages) return { chats: [normalizeImportedChat(object)] };
7869
+ }
7870
+ throw new Error("JSON import must contain a chats array, a chat object, or messages.");
7871
+ }
7872
+ return { chats: [parseCliExportYaml(trimmed)] };
7873
+ }
7874
+ function normalizeImportedChat(source) {
7875
+ const chat = source.chat && typeof source.chat === "object" ? source.chat : source;
7876
+ const messages = Array.isArray(source.messages) ? source.messages : [];
7877
+ return {
7878
+ title: chat.title ?? null,
7879
+ draft: chat.draft ?? null,
7880
+ summary: chat.summary ?? null,
7881
+ messages: messages.map((message) => normalizeImportedMessage(message))
7882
+ };
7883
+ }
7884
+ function normalizeImportedMessage(message) {
7885
+ return {
7886
+ role: message.role,
7887
+ content: message.content,
7888
+ completed_at: message.completed_at ?? message.timestamp ?? null,
7889
+ assistant_category: message.assistant_category ?? null,
7890
+ thinking: message.thinking ?? null,
7891
+ has_thinking: message.has_thinking ?? null,
7892
+ thinking_tokens: message.thinking_tokens ?? null
7893
+ };
7894
+ }
7895
+ function parseCliExportYaml(raw) {
7896
+ const chat = {};
7897
+ const messages = [];
7898
+ const lines = raw.split("\n");
7899
+ let section = null;
7900
+ let currentMessage = null;
7901
+ const multilineState = { current: null };
7902
+ const setValue = (target, key, value, indent) => {
7903
+ if (value === "|") {
7904
+ target[key] = "";
7905
+ multilineState.current = { target, key, indent: indent + 2 };
7906
+ return;
7713
7907
  }
7714
- const result = await client.settingsGet(path);
7715
- if (flags.json === true) {
7716
- printJson2(result);
7717
- } else {
7718
- printGenericObject(result);
7908
+ target[key] = parseYamlScalar(value);
7909
+ };
7910
+ for (const line of lines) {
7911
+ const indent = line.match(/^ */)?.[0].length ?? 0;
7912
+ const trimmed = line.trimEnd();
7913
+ if (!trimmed.trim()) continue;
7914
+ if (multilineState.current) {
7915
+ if (indent >= multilineState.current.indent) {
7916
+ const previous = String(multilineState.current.target[multilineState.current.key] ?? "");
7917
+ const nextLine = line.slice(multilineState.current.indent);
7918
+ multilineState.current.target[multilineState.current.key] = previous ? `${previous}
7919
+ ${nextLine}` : nextLine;
7920
+ continue;
7921
+ }
7922
+ multilineState.current = null;
7719
7923
  }
7720
- return;
7721
- }
7722
- if (subcommand === "post") {
7723
- const path = rest[0];
7724
- if (!path) {
7725
- console.error("Missing path.\n");
7726
- printSettingsHelp();
7727
- process.exit(1);
7924
+ if (trimmed === "chat:") {
7925
+ section = "chat";
7926
+ currentMessage = null;
7927
+ continue;
7728
7928
  }
7729
- const dataRaw = typeof flags.data === "string" ? flags.data : "{}";
7730
- const data = JSON.parse(dataRaw);
7731
- const result = await client.settingsPost(path, data);
7732
- if (flags.json === true) {
7733
- printJson2(result);
7734
- } else {
7735
- printGenericObject(result);
7929
+ if (trimmed === "messages:") {
7930
+ section = "messages";
7931
+ currentMessage = null;
7932
+ continue;
7933
+ }
7934
+ if (section === "messages" && trimmed.trim() === "-") {
7935
+ currentMessage = {};
7936
+ messages.push(currentMessage);
7937
+ continue;
7736
7938
  }
7939
+ const match = /^\s*([\w-]+):\s*(.*)$/.exec(line);
7940
+ if (!match) continue;
7941
+ const [, key, value] = match;
7942
+ if (section === "chat") setValue(chat, key, value, indent);
7943
+ if (section === "messages" && currentMessage) setValue(currentMessage, key, value, indent);
7944
+ }
7945
+ if (messages.length === 0) throw new Error("Import YAML did not contain any messages.");
7946
+ return normalizeImportedChat({ chat, messages });
7947
+ }
7948
+ function parseYamlScalar(value) {
7949
+ const trimmed = value.trim();
7950
+ if (trimmed === "null") return null;
7951
+ if (trimmed === "true") return true;
7952
+ if (trimmed === "false") return false;
7953
+ if (trimmed !== "" && Number.isFinite(Number(trimmed))) return Number(trimmed);
7954
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
7955
+ return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
7956
+ }
7957
+ return trimmed;
7958
+ }
7959
+ async function saveDownloadedDocument(document, output) {
7960
+ const { mkdir, writeFile } = await import("fs/promises");
7961
+ const { join: join4, basename: basename2, dirname } = await import("path");
7962
+ const target = typeof output === "string" ? output : ".";
7963
+ const filename = basename2(document.filename || "document.pdf");
7964
+ const filePath = target.endsWith(".pdf") ? target : join4(target, filename);
7965
+ await mkdir(dirname(filePath), { recursive: true });
7966
+ await writeFile(filePath, document.data);
7967
+ return filePath;
7968
+ }
7969
+ function printMates(json) {
7970
+ const mates = Object.entries(MATE_NAMES).map(([id, name]) => ({ id, name, mention: `@mate:${id}` }));
7971
+ if (json) {
7972
+ printJson2(mates);
7737
7973
  return;
7738
7974
  }
7739
- if (subcommand === "delete") {
7740
- const path = rest[0];
7741
- if (!path) {
7742
- console.error("Missing path.\n");
7743
- printSettingsHelp();
7744
- process.exit(1);
7745
- }
7746
- const result = await client.settingsDelete(path);
7747
- if (flags.json === true) {
7748
- printJson2(result);
7749
- } else {
7750
- printGenericObject(result);
7751
- }
7975
+ header("Mates");
7976
+ for (const mate of mates) console.log(`${mate.id.padEnd(28)} ${mate.name.padEnd(10)} ${mate.mention}`);
7977
+ }
7978
+ function printMateInfo(mateId, json) {
7979
+ const name = MATE_NAMES[mateId];
7980
+ if (!name) throw new Error(`Unknown mate '${mateId}'. Run 'openmates settings mates list'.`);
7981
+ const data = { id: mateId, name, mention: `@mate:${mateId}` };
7982
+ if (json) {
7983
+ printJson2(data);
7752
7984
  return;
7753
7985
  }
7754
- if (subcommand === "patch") {
7755
- const path = rest[0];
7756
- if (!path) {
7757
- console.error("Missing path.\n");
7758
- printSettingsHelp();
7759
- process.exit(1);
7986
+ header(`${name} (${mateId})`);
7987
+ console.log(`Mention: ${data.mention}`);
7988
+ console.log(`Use: openmates chats send "${data.mention} <message>"`);
7989
+ }
7990
+ async function confirmOrExit(question) {
7991
+ const rl = await import("readline");
7992
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
7993
+ const answer = await new Promise((resolve4) => iface.question(question, resolve4));
7994
+ iface.close();
7995
+ if (answer.trim().toLowerCase() !== "y") {
7996
+ console.log("Aborted.");
7997
+ process.exit(0);
7998
+ }
7999
+ }
8000
+ function printSettingsInfoCommand(client, command, json) {
8001
+ const appUrl = deriveAppUrl(client.apiUrl);
8002
+ const webUrl = command.webPath ? `${appUrl}/#settings/${command.webPath}` : null;
8003
+ if (json) {
8004
+ printJson2({
8005
+ command: `openmates settings ${command.path.join(" ")}`,
8006
+ supported_in_cli: false,
8007
+ description: command.description,
8008
+ reason: command.reason ?? null,
8009
+ web_url: webUrl,
8010
+ examples: command.examples
8011
+ });
8012
+ return;
8013
+ }
8014
+ header(command.description);
8015
+ if (command.reason) console.log(command.reason);
8016
+ if (webUrl) console.log(`
8017
+ Open in web app:
8018
+ ${webUrl}`);
8019
+ if (command.examples.length > 0) {
8020
+ console.log("\nExamples:");
8021
+ for (const example of command.examples) console.log(` ${example}`);
8022
+ }
8023
+ }
8024
+ async function handleSettings(client, subcommand, rest, flags) {
8025
+ if (!subcommand || subcommand === "help") {
8026
+ printSettingsHelp(client, subcommand ? [] : void 0);
8027
+ return;
8028
+ }
8029
+ const tokens = [subcommand, ...rest].filter((token) => token !== "help");
8030
+ if (rest.includes("help") || Boolean(flags.help)) {
8031
+ printSettingsHelp(client, tokens);
8032
+ return;
8033
+ }
8034
+ if (["get", "post", "patch", "delete"].includes(subcommand)) {
8035
+ console.error(
8036
+ "Raw settings passthrough is no longer supported. Use a predefined settings command.\n"
8037
+ );
8038
+ printSettingsHelp(client);
8039
+ process.exit(1);
8040
+ }
8041
+ if (matches(tokens, ["account", "info"])) {
8042
+ const user = await client.whoAmI();
8043
+ flags.json === true ? printJson2(user) : printWhoAmI(user);
8044
+ return;
8045
+ }
8046
+ if (matches(tokens, ["account", "timezone", "set"])) {
8047
+ const timezone = rest[2];
8048
+ if (!timezone) throw new Error("Missing timezone. Example: openmates settings account timezone set Europe/Berlin");
8049
+ await printSettingsMutationResult(
8050
+ client.settingsPost("user/timezone", { timezone }),
8051
+ flags
8052
+ );
8053
+ return;
8054
+ }
8055
+ if (matches(tokens, ["account", "export", "manifest"])) {
8056
+ await printSettingsResult(client.settingsGet("export-account-manifest"), flags);
8057
+ return;
8058
+ }
8059
+ if (matches(tokens, ["account", "export", "data"])) {
8060
+ await printSettingsResult(client.settingsGet("export-account-data"), flags);
8061
+ return;
8062
+ }
8063
+ if (matches(tokens, ["account", "import-chat"])) {
8064
+ const file = rest[1];
8065
+ if (!file) throw new Error("Missing import file. Example: openmates settings account import-chat ./chat.yml");
8066
+ const { readFile } = await import("fs/promises");
8067
+ const content = await readFile(file, "utf-8");
8068
+ await printSettingsMutationResult(
8069
+ client.settingsPost("import-chat", parseChatImportPayload(content)),
8070
+ flags
8071
+ );
8072
+ return;
8073
+ }
8074
+ if (matches(tokens, ["account", "username", "set"])) {
8075
+ const username = rest[2];
8076
+ if (!username) throw new Error("Missing username. Example: openmates settings account username set alice_123");
8077
+ await printSettingsMutationResult(client.updateUsername(username), flags);
8078
+ return;
8079
+ }
8080
+ if (matches(tokens, ["account", "profile-picture", "set"])) {
8081
+ const file = rest[2];
8082
+ if (!file) throw new Error("Missing image file. Example: openmates settings account profile-picture set ./avatar.jpg");
8083
+ await printSettingsMutationResult(client.updateProfileImage(file), flags);
8084
+ return;
8085
+ }
8086
+ if (matches(tokens, ["account", "chats", "stats"])) {
8087
+ await printSettingsResult(client.settingsGet("chats"), flags);
8088
+ return;
8089
+ }
8090
+ if (matches(tokens, ["account", "delete", "preview"])) {
8091
+ await printSettingsResult(client.settingsGet("delete-account-preview"), flags);
8092
+ return;
8093
+ }
8094
+ if (matches(tokens, ["account", "storage", "overview"])) {
8095
+ await printSettingsResult(client.settingsGet("storage"), flags);
8096
+ return;
8097
+ }
8098
+ if (matches(tokens, ["account", "storage", "files"])) {
8099
+ const params = new URLSearchParams();
8100
+ addQueryParam(params, "category", flags.category ?? flags.type);
8101
+ const query = params.toString();
8102
+ await printSettingsResult(client.settingsGet(`storage/files${query ? `?${query}` : ""}`), flags);
8103
+ return;
8104
+ }
8105
+ if (matches(tokens, ["account", "storage", "delete"])) {
8106
+ const fileId = rest[3];
8107
+ const category = typeof flags.category === "string" ? flags.category : void 0;
8108
+ const scope = flags.all === true ? "all" : category ? "category" : "single";
8109
+ if (scope === "single" && !fileId) throw new Error("Missing file ID.");
8110
+ if (flags.yes !== true) await confirmOrExit(`Delete stored file data (${scope})? This cannot be undone. [y/N] `);
8111
+ await printSettingsMutationResult(
8112
+ client.settingsDelete("storage/files", { scope, file_id: fileId, category }),
8113
+ flags
8114
+ );
8115
+ return;
8116
+ }
8117
+ if (matches(tokens, ["interface", "language", "set"])) {
8118
+ const language = rest[2];
8119
+ if (!language) throw new Error("Missing language code. Example: openmates settings interface language set en");
8120
+ await printSettingsMutationResult(client.settingsPost("user/language", { language }), flags);
8121
+ return;
8122
+ }
8123
+ if (matches(tokens, ["interface", "dark-mode", "set"])) {
8124
+ const value = parseOnOff(rest[2], "dark mode");
8125
+ await printSettingsMutationResult(client.settingsPost("user/darkmode", { dark_mode: value }), flags);
8126
+ return;
8127
+ }
8128
+ if (matches(tokens, ["interface", "font", "set"])) {
8129
+ const font = rest[2];
8130
+ if (!font) throw new Error("Missing font. Example: openmates settings interface font set lexend");
8131
+ await printSettingsMutationResult(client.settingsPost("user/ui-font", { ui_font: font }), flags);
8132
+ return;
8133
+ }
8134
+ if (matches(tokens, ["ai", "models", "set-defaults"])) {
8135
+ const simple = typeof flags.simple === "string" ? flags.simple : void 0;
8136
+ const complex = typeof flags.complex === "string" ? flags.complex : void 0;
8137
+ if (!simple && !complex) throw new Error("Provide --simple <model-id> and/or --complex <model-id>.");
8138
+ await printSettingsMutationResult(client.settingsPost("ai-model-defaults", { simple, complex }), flags);
8139
+ return;
8140
+ }
8141
+ if (matches(tokens, ["privacy", "auto-delete", "chats", "set"])) {
8142
+ const period = rest[3];
8143
+ if (!period) throw new Error("Missing period. Example: openmates settings privacy auto-delete chats set 90d");
8144
+ await printSettingsMutationResult(client.settingsPost("auto-delete-chats", { period }), flags);
8145
+ return;
8146
+ }
8147
+ if (matches(tokens, ["privacy", "debug-logs", "share"])) {
8148
+ if (flags.yes !== true && flags.confirm !== true) {
8149
+ await confirmOrExit("Share debug logs with OpenMates support? [y/N] ");
7760
8150
  }
7761
- const dataRaw = typeof flags.data === "string" ? flags.data : "{}";
7762
- const data = JSON.parse(dataRaw);
7763
- const result = await client.settingsPatch(path, data);
8151
+ const duration = typeof flags.duration === "string" ? flags.duration : "1h";
8152
+ await printSettingsMutationResult(client.settingsPost("debug-session", { duration }), flags);
8153
+ return;
8154
+ }
8155
+ if (matches(tokens, ["billing", "overview"])) {
8156
+ await printSettingsResult(client.settingsGet("billing"), flags);
8157
+ return;
8158
+ }
8159
+ if (matches(tokens, ["billing", "usage", "summaries"])) {
8160
+ await printSettingsResult(client.settingsGet("usage/summaries"), flags);
8161
+ return;
8162
+ }
8163
+ if (matches(tokens, ["billing", "usage", "daily"])) {
8164
+ await printSettingsResult(client.settingsGet("usage/daily-overview"), flags);
8165
+ return;
8166
+ }
8167
+ if (matches(tokens, ["billing", "usage", "export"])) {
8168
+ await printSettingsResult(client.settingsGet("usage/export"), flags);
8169
+ return;
8170
+ }
8171
+ if (matches(tokens, ["billing", "usage"])) {
8172
+ await printSettingsResult(client.settingsGet("usage"), flags);
8173
+ return;
8174
+ }
8175
+ if (matches(tokens, ["billing", "invoices", "list"])) {
8176
+ await printSettingsResult(client.listInvoices(), flags);
8177
+ return;
8178
+ }
8179
+ if (matches(tokens, ["billing", "invoices", "download"])) {
8180
+ const invoiceId = rest[2];
8181
+ if (!invoiceId) throw new Error("Missing invoice ID.");
8182
+ const document = await client.downloadInvoice(invoiceId);
8183
+ const filePath = await saveDownloadedDocument(document, flags.output);
8184
+ if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
8185
+ else console.log(`\x1B[32m\u2713\x1B[0m Invoice saved to ${filePath}`);
8186
+ return;
8187
+ }
8188
+ if (matches(tokens, ["billing", "invoices", "credit-note"])) {
8189
+ const invoiceId = rest[2];
8190
+ if (!invoiceId) throw new Error("Missing invoice ID.");
8191
+ const document = await client.downloadCreditNote(invoiceId);
8192
+ const filePath = await saveDownloadedDocument(document, flags.output);
8193
+ if (flags.json === true) printJson2({ path: filePath, filename: document.filename });
8194
+ else console.log(`\x1B[32m\u2713\x1B[0m Credit note saved to ${filePath}`);
8195
+ return;
8196
+ }
8197
+ if (matches(tokens, ["billing", "invoices", "refund"])) {
8198
+ const invoiceId = rest[2];
8199
+ const emailEncryptionKey = typeof flags["email-encryption-key"] === "string" ? flags["email-encryption-key"] : void 0;
8200
+ if (!invoiceId) throw new Error("Missing invoice ID.");
8201
+ if (!emailEncryptionKey) throw new Error("Missing --email-encryption-key. The backend requires the encrypted email key for refund requests.");
8202
+ if (flags.yes !== true) await confirmOrExit(`Request refund for invoice ${invoiceId}? [y/N] `);
8203
+ await printSettingsMutationResult(client.requestRefund(invoiceId, emailEncryptionKey), flags);
8204
+ return;
8205
+ }
8206
+ if (matches(tokens, ["billing", "gift-card", "redeem"]) || subcommand === "gift-card" && rest[0] === "redeem") {
8207
+ const code = matches(tokens, ["billing", "gift-card", "redeem"]) ? rest[2] : rest[1];
8208
+ if (!code) throw new Error("Missing gift card code.");
8209
+ const result = await client.redeemGiftCard(code);
7764
8210
  if (flags.json === true) {
7765
8211
  printJson2(result);
8212
+ } else if (result.success) {
8213
+ process.stdout.write(`\x1B[32m\u2713\x1B[0m Gift card redeemed! +${result.credits_added} credits
8214
+ `);
8215
+ process.stdout.write(` Balance: ${result.current_credits} credits
8216
+ `);
7766
8217
  } else {
7767
- printGenericObject(result);
8218
+ process.stdout.write(`\x1B[31m\u2717\x1B[0m ${result.message}
8219
+ `);
7768
8220
  }
7769
8221
  return;
7770
8222
  }
8223
+ if (matches(tokens, ["billing", "gift-card", "list"]) || subcommand === "gift-card" && rest[0] === "list") {
8224
+ await printSettingsResult(client.listRedeemedGiftCards(), flags);
8225
+ return;
8226
+ }
8227
+ if (matches(tokens, ["billing", "auto-topup", "low-balance", "set"])) {
8228
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "low-balance auto top-up");
8229
+ const amount = parseRequiredNumber(flags.amount, "--amount");
8230
+ const currency = typeof flags.currency === "string" ? flags.currency : "eur";
8231
+ const email = typeof flags.email === "string" ? flags.email : void 0;
8232
+ if (enabled && !email) throw new Error("Provide --email when enabling low-balance auto top-up.");
8233
+ await printSettingsMutationResult(
8234
+ client.settingsPost("auto-topup/low-balance", { enabled, threshold: 100, amount, currency, email }),
8235
+ flags
8236
+ );
8237
+ return;
8238
+ }
8239
+ if (matches(tokens, ["notifications", "status"])) {
8240
+ const user = await client.whoAmI();
8241
+ const status = {
8242
+ enabled: user.email_notifications_enabled ?? false,
8243
+ preferences: user.email_notification_preferences ?? {},
8244
+ backup_reminder_interval_days: user.backup_reminder_interval_days ?? null,
8245
+ encrypted_notification_email_configured: Boolean(user.encrypted_notification_email)
8246
+ };
8247
+ flags.json === true ? printJson2(status) : printGenericObject(status);
8248
+ return;
8249
+ }
8250
+ if (matches(tokens, ["notifications", "email", "set"])) {
8251
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "email notifications");
8252
+ const email = typeof flags.email === "string" ? flags.email : null;
8253
+ if (enabled && !email) throw new Error("Provide --email when enabling email notifications.");
8254
+ const preferences = {
8255
+ aiResponses: parseOptionalBoolean(flags["ai-responses"], true, "AI response notifications"),
8256
+ backupReminder: parseOptionalBoolean(flags["backup-reminder"], false, "backup reminder notifications"),
8257
+ webhookChats: parseOptionalBoolean(flags["webhook-chats"], false, "webhook chat notifications")
8258
+ };
8259
+ await printSettingsMutationResult(
8260
+ client.updateEmailNotificationSettings({ enabled, email, preferences }),
8261
+ flags
8262
+ );
8263
+ return;
8264
+ }
8265
+ if (matches(tokens, ["notifications", "backup", "set"])) {
8266
+ const enabled = parseOnOff(String(flags.enabled ?? ""), "backup reminders");
8267
+ const email = typeof flags.email === "string" ? flags.email : null;
8268
+ const interval = parseRequiredNumber(flags.interval, "--interval");
8269
+ if (enabled && !email) throw new Error("Provide --email when enabling backup reminders.");
8270
+ await printSettingsMutationResult(
8271
+ client.updateEmailNotificationSettings({
8272
+ enabled,
8273
+ email,
8274
+ preferences: { aiResponses: false, backupReminder: enabled },
8275
+ backup_reminder_interval_days: interval
8276
+ }),
8277
+ flags
8278
+ );
8279
+ return;
8280
+ }
8281
+ if (matches(tokens, ["reminders", "list"])) {
8282
+ await printSettingsResult(client.settingsGet("reminders"), flags);
8283
+ return;
8284
+ }
8285
+ if (matches(tokens, ["reminders", "update"])) {
8286
+ const id = rest[1];
8287
+ if (!id) throw new Error("Missing reminder ID.");
8288
+ const body = parseDataOrFlags(flags, ["enabled"]);
8289
+ await printSettingsMutationResult(client.settingsPatch(`reminders/${id}`, body), flags);
8290
+ return;
8291
+ }
8292
+ if (matches(tokens, ["reminders", "delete"])) {
8293
+ const id = rest[1];
8294
+ if (!id) throw new Error("Missing reminder ID.");
8295
+ if (flags.yes !== true) await confirmOrExit(`Delete reminder ${id}? [y/N] `);
8296
+ await printSettingsMutationResult(client.settingsDelete(`reminders/${id}`), flags);
8297
+ return;
8298
+ }
8299
+ if (matches(tokens, ["developers", "api-keys", "list"])) {
8300
+ await printSettingsResult(client.settingsGet("api-keys"), flags);
8301
+ return;
8302
+ }
8303
+ if (matches(tokens, ["developers", "api-keys", "revoke"])) {
8304
+ const id = rest[2];
8305
+ if (!id) throw new Error("Missing API key ID.");
8306
+ if (flags.yes !== true) await confirmOrExit(`Revoke API key ${id}? [y/N] `);
8307
+ await printSettingsMutationResult(client.settingsDelete(`api-keys/${id}`), flags);
8308
+ return;
8309
+ }
8310
+ if (matches(tokens, ["report-issue", "create"])) {
8311
+ const title = typeof flags.title === "string" ? flags.title : void 0;
8312
+ const body = typeof flags.body === "string" ? flags.body : void 0;
8313
+ if (!title || !body) throw new Error("Provide --title and --body.");
8314
+ await printSettingsMutationResult(client.settingsPost("issues", { title, description: body }), flags);
8315
+ return;
8316
+ }
8317
+ if (matches(tokens, ["report-issue", "status"])) {
8318
+ const id = rest[1];
8319
+ if (!id) throw new Error("Missing issue ID.");
8320
+ await printSettingsResult(client.settingsGet(`issues/${id}/status`), flags);
8321
+ return;
8322
+ }
8323
+ if (matches(tokens, ["mates", "list"])) {
8324
+ printMates(flags.json === true);
8325
+ return;
8326
+ }
8327
+ if (matches(tokens, ["mates", "info"])) {
8328
+ const mateId = rest[1];
8329
+ if (!mateId) throw new Error("Missing mate ID. Example: openmates settings mates info software_development");
8330
+ printMateInfo(mateId, flags.json === true);
8331
+ return;
8332
+ }
8333
+ if (matches(tokens, ["mates", "consent"])) {
8334
+ if (flags.yes !== true) await confirmOrExit("Record consent for mate settings? [y/N] ");
8335
+ await printSettingsMutationResult(client.settingsPost("user/consent/mates", { consent: true }), flags);
8336
+ return;
8337
+ }
8338
+ if (matches(tokens, ["newsletter", "categories"]) && tokens.length === 2) {
8339
+ await printSettingsResult(client.getNewsletterCategories(), flags);
8340
+ return;
8341
+ }
8342
+ if (matches(tokens, ["newsletter", "categories", "set"])) {
8343
+ const categories = {
8344
+ updates_and_announcements: parseOptionalBoolean(flags.updates, true, "updates newsletter"),
8345
+ tips_and_tricks: parseOptionalBoolean(flags.tips, true, "tips newsletter"),
8346
+ daily_inspirations: parseOptionalBoolean(flags.daily, true, "daily inspirations newsletter")
8347
+ };
8348
+ await printSettingsMutationResult(client.updateNewsletterCategories(categories), flags);
8349
+ return;
8350
+ }
8351
+ if (matches(tokens, ["newsletter", "subscribe"])) {
8352
+ const email = rest[1];
8353
+ if (!email) throw new Error("Missing email. Example: openmates settings newsletter subscribe you@example.com");
8354
+ const language = typeof flags.language === "string" ? flags.language : "en";
8355
+ const darkmode = parseOptionalBoolean(flags.darkmode, false, "newsletter dark mode");
8356
+ await printSettingsMutationResult(client.subscribeNewsletter(email, language, darkmode), flags);
8357
+ return;
8358
+ }
8359
+ if (matches(tokens, ["newsletter", "confirm"])) {
8360
+ const token = rest[1];
8361
+ if (!token) throw new Error("Missing confirmation token.");
8362
+ await printSettingsMutationResult(client.confirmNewsletter(token), flags);
8363
+ return;
8364
+ }
8365
+ if (matches(tokens, ["newsletter", "unsubscribe"])) {
8366
+ const token = rest[1];
8367
+ if (!token) throw new Error("Missing unsubscribe token.");
8368
+ await printSettingsMutationResult(client.unsubscribeNewsletter(token), flags);
8369
+ return;
8370
+ }
7771
8371
  if (subcommand === "memories") {
7772
8372
  await handleMemories(client, rest, flags);
7773
8373
  return;
7774
8374
  }
7775
- if (subcommand === "gift-card") {
7776
- const action = rest[0];
7777
- if (action === "redeem") {
7778
- const code = rest[1];
7779
- if (!code) {
7780
- console.error("Missing gift card code.\n");
7781
- console.log("Usage: openmates settings gift-card redeem <CODE>");
7782
- process.exit(1);
7783
- }
7784
- const result = await client.redeemGiftCard(code);
7785
- if (flags.json === true) {
7786
- printJson2(result);
7787
- } else {
7788
- if (result.success) {
7789
- process.stdout.write(
7790
- `\x1B[32m\u2713\x1B[0m Gift card redeemed! +${result.credits_added} credits
7791
- `
7792
- );
7793
- process.stdout.write(
7794
- ` Balance: ${result.current_credits} credits
7795
- `
7796
- );
7797
- } else {
7798
- process.stdout.write(`\x1B[31m\u2717\x1B[0m ${result.message}
7799
- `);
7800
- }
7801
- }
7802
- return;
7803
- }
7804
- if (action === "list") {
7805
- const result = await client.listRedeemedGiftCards();
7806
- if (flags.json === true) {
7807
- printJson2(result);
7808
- } else {
7809
- printGenericObject(result);
7810
- }
7811
- return;
7812
- }
7813
- console.log(`Gift card commands:
7814
- openmates settings gift-card redeem <CODE> Redeem a gift card
7815
- openmates settings gift-card list List redeemed gift cards`);
8375
+ const webOnly = findSettingsInfoCommand(tokens);
8376
+ if (webOnly) {
8377
+ printSettingsInfoCommand(client, webOnly, flags.json === true);
7816
8378
  return;
7817
8379
  }
7818
- console.error(`Unknown settings subcommand '${subcommand}'.
8380
+ console.error(`Unknown settings command '${tokens.join(" ")}'.
7819
8381
  `);
7820
- printSettingsHelp();
8382
+ printSettingsHelp(client, [subcommand]);
7821
8383
  process.exit(1);
7822
8384
  }
7823
8385
  async function handleMemories(client, rest, flags) {
@@ -8370,21 +8932,13 @@ async function sendMessageStreaming(client, params, redactor) {
8370
8932
  }
8371
8933
  return result;
8372
8934
  }
8373
- function printIncognitoHistory(history) {
8374
- if (history.length === 0) {
8375
- console.log("No incognito history.");
8935
+ function printIncognitoNoHistoryNotice(json) {
8936
+ const message = "Incognito chats are not stored. There is no incognito history to show or clear.";
8937
+ if (json) {
8938
+ printJson2({ history: [], stored: false, message });
8376
8939
  return;
8377
8940
  }
8378
- header(`Incognito history (${history.length} messages)`);
8379
- console.log();
8380
- for (const msg of history) {
8381
- const ts = formatTimestamp(Math.floor(msg.createdAt / 1e3));
8382
- const roleLabel = msg.role === "user" ? "\x1B[1mYou\x1B[0m" : "\x1B[36mAssistant\x1B[0m";
8383
- process.stdout.write(`${roleLabel} \x1B[2m${ts}\x1B[0m
8384
- `);
8385
- console.log(msg.content);
8386
- console.log();
8387
- }
8941
+ console.log(message);
8388
8942
  }
8389
8943
  var SEP = `\x1B[2m${"\u2500".repeat(60)}\x1B[0m`;
8390
8944
  function parseMessageSegments(content) {
@@ -9351,23 +9905,23 @@ async function handleMentions(client, subcommand, _rest, flags) {
9351
9905
  const context = await client.buildMentionContext();
9352
9906
  const allOptions = listMentionOptions(context);
9353
9907
  const normalizedQuery = query.toLowerCase().replace(/[\s_-]+/g, "");
9354
- const matches = allOptions.filter((opt) => {
9908
+ const matches2 = allOptions.filter((opt) => {
9355
9909
  const normalizedName = opt.displayName.toLowerCase().replace(/[@\s_-]+/g, "");
9356
9910
  const normalizedDesc = opt.description.toLowerCase().replace(/[\s_-]+/g, "");
9357
9911
  return normalizedName.includes(normalizedQuery) || normalizedDesc.includes(normalizedQuery);
9358
9912
  }).slice(0, 15);
9359
9913
  if (flags.json === true) {
9360
- console.log(JSON.stringify(matches, null, 2));
9914
+ console.log(JSON.stringify(matches2, null, 2));
9361
9915
  return;
9362
9916
  }
9363
- if (matches.length === 0) {
9917
+ if (matches2.length === 0) {
9364
9918
  console.log(`No mentions matching '${query}'.`);
9365
9919
  return;
9366
9920
  }
9367
9921
  process.stdout.write(`
9368
9922
  \x1B[1mMatches for '${query}':\x1B[0m
9369
9923
  `);
9370
- for (const m of matches) {
9924
+ for (const m of matches2) {
9371
9925
  const typeLabel = m.type.replace("_", " ");
9372
9926
  process.stdout.write(
9373
9927
  ` \x1B[36m${m.displayName.padEnd(35)}\x1B[0m \x1B[2m${m.description} (${typeLabel})\x1B[0m
@@ -9512,7 +10066,7 @@ Commands:
9512
10066
  openmates apps [--help] App skill commands (list, run, ...)
9513
10067
  openmates mentions [--help] List available @mentions
9514
10068
  openmates embeds [--help] Embed commands (show)
9515
- openmates settings [--help] Memories
10069
+ openmates settings [--help] Predefined settings commands
9516
10070
  openmates inspirations [--lang <code>] [--json] Daily inspirations
9517
10071
  openmates newchatsuggestions [--limit <n>] [--json] Personalized new chat suggestions
9518
10072
  openmates server [--help] Server management (install, start, stop, ...)
@@ -9537,8 +10091,8 @@ function printChatsHelp() {
9537
10091
  openmates chats delete <id1> [id2] [id3] ... [--yes]
9538
10092
  openmates chats share [<chat-id>] [--expires <seconds>] [--password <pwd>] [--json]
9539
10093
  openmates chats incognito <message> [--json]
9540
- openmates chats incognito-history [--json]
9541
- openmates chats incognito-clear
10094
+ openmates chats incognito-history [--json] Deprecated: incognito stores no history
10095
+ openmates chats incognito-clear Deprecated: incognito stores no history
9542
10096
 
9543
10097
  Options for 'list':
9544
10098
  --limit <n> Number of chats per page (default: 10)
@@ -9662,72 +10216,33 @@ Examples:
9662
10216
  openmates inspirations --lang de
9663
10217
  openmates inspirations --json`);
9664
10218
  }
9665
- function printSettingsHelp(client) {
10219
+ function printSettingsHelp(client, filter) {
10220
+ const commands = [...SETTINGS_EXECUTABLE_COMMANDS, ...SETTINGS_INFO_COMMANDS].filter((command) => {
10221
+ if (!filter || filter.length === 0) return true;
10222
+ return filter.every((part, index) => command.path[index] === part);
10223
+ }).sort((a, b) => a.path.join(" ").localeCompare(b.path.join(" ")));
9666
10224
  const appUrl = client ? deriveAppUrl(client.apiUrl) : "https://openmates.org";
9667
- const s = (path) => `${appUrl}/#settings/${path}`;
9668
- const h = (title) => `
9669
- \x1B[1m${title}\x1B[0m`;
9670
- console.log(`\x1B[1mSettings\x1B[0m
9671
- ${h("Account")}
9672
- openmates settings post user/username --data '{"encrypted_username":"..."}'
9673
- openmates settings post user/timezone --data '{"timezone":"Europe/Berlin"}'
9674
- openmates settings get export-account-manifest GDPR data export manifest
9675
- openmates settings get export-account-data GDPR data export
9676
- openmates settings post import-chat --data '<json>' Import a chat
9677
- openmates settings get storage [--json] Storage overview
9678
- openmates settings get chats [--json] Chat statistics
9679
- openmates settings get delete-account-preview Preview account deletion
9680
- \x1B[2mSecurity (passkeys, password, 2FA, sessions): ${s("account/security")}\x1B[0m
9681
- \x1B[2mDelete account: ${s("account/delete")}\x1B[0m
9682
- ${h("Billing")}
9683
- openmates settings get billing [--json] Balance & billing overview
9684
- openmates settings post auto-topup/low-balance --data '{"enabled":true,"amount":1000,"currency":"eur"}'
9685
- openmates settings get usage [--json] Full usage history
9686
- openmates settings get usage/summaries [--json] Usage summaries by type
9687
- openmates settings get usage/daily-overview [--json]
9688
- openmates settings get usage/export [--json] Export usage as CSV
9689
- openmates settings gift-card redeem <CODE> Redeem a gift card
9690
- openmates settings gift-card list List redeemed gift cards
9691
- \x1B[2mBuy credits: ${s("billing/buy-credits")}\x1B[0m
9692
- \x1B[2mMonthly auto top-up: ${s("billing/auto-topup/monthly")}\x1B[0m
9693
- \x1B[2mInvoices: ${s("billing/invoices")}\x1B[0m
9694
- \x1B[2mGift cards (buy/manage): ${s("billing/gift-cards")}\x1B[0m
9695
- ${h("Privacy")}
9696
- openmates settings post auto-delete-chats --data '{"period":"90d"}'
9697
- \x1B[2mHide personal data / anonymization: ${s("privacy/hide-personal-data")}\x1B[0m
9698
- ${h("Notifications")}
9699
- openmates settings get reminders [--json] Active reminders
9700
- \x1B[2mChat notifications: ${s("notifications/chat")}\x1B[0m
9701
- \x1B[2mBackup reminders: ${s("notifications/backup")}\x1B[0m
9702
- ${h("Interface")}
9703
- openmates settings post user/language --data '{"language":"en"}'
9704
- openmates settings post user/darkmode --data '{"dark_mode":true}'
9705
- openmates settings post ai-model-defaults --data '{"simple":"...","complex":"..."}'
9706
- ${h("Apps")}
9707
- openmates apps list Same as Apps
9708
- openmates apps <app-id> App details
9709
- \x1B[2mWeb: ${s("app_store")}\x1B[0m
9710
- ${h("Mates")}
9711
- \x1B[2m${s("mates")}\x1B[0m
9712
- ${h("Memories & app settings")}
9713
- openmates settings memories list [--app-id <id>] [--item-type <type>] [--json]
9714
- openmates settings memories types [--app-id <id>] [--json]
9715
- openmates settings memories create --app-id <id> --item-type <type> --data '<json>'
9716
- openmates settings memories update --id <id> --app-id <id> --item-type <type> --data '<json>'
9717
- openmates settings memories delete --id <entry-id>
9718
- ${h("Developers")}
9719
- openmates settings get api-keys [--json] List API keys
9720
- openmates settings delete api-keys/<key-id> Revoke API key
9721
- \x1B[2mCreate API key (shows secret once): ${s("developers/api-keys")}\x1B[0m
9722
- \x1B[2mManage devices: ${s("developers/devices")}\x1B[0m
9723
- ${h("Support")}
9724
- openmates settings post issues --data '<json>' Report an issue
9725
-
9726
- \x1B[2mWeb app only (security \u2014 manage in browser):\x1B[0m
9727
- \x1B[2mPasskeys: ${s("account/security/passkeys")}\x1B[0m
9728
- \x1B[2mPassword: ${s("account/security/password")}\x1B[0m
9729
- \x1B[2m2FA: ${s("account/security/2fa")}\x1B[0m
9730
- \x1B[2mSessions: ${s("account/security/sessions")}\x1B[0m`);
10225
+ const title = filter && filter.length > 0 ? `Settings: ${filter.join(" ")}` : "Settings";
10226
+ header(title);
10227
+ console.log("Predefined commands only. Raw settings get/post/patch/delete is not supported.\n");
10228
+ if (commands.length === 0) {
10229
+ console.log("No matching settings commands.");
10230
+ return;
10231
+ }
10232
+ for (const command of commands) {
10233
+ const isInfoOnly = SETTINGS_INFO_COMMANDS.includes(command);
10234
+ const label = `openmates settings ${command.path.join(" ")}`;
10235
+ process.stdout.write(` ${label.padEnd(58)} ${command.description}`);
10236
+ if (isInfoOnly) process.stdout.write(" \x1B[2m(info/web-only)\x1B[0m");
10237
+ process.stdout.write("\n");
10238
+ if (filter && filter.length > 0) {
10239
+ for (const example of command.examples) process.stdout.write(` e.g. ${example}
10240
+ `);
10241
+ if (command.webPath) process.stdout.write(` web: ${appUrl}/#settings/${command.webPath}
10242
+ `);
10243
+ }
10244
+ }
10245
+ console.log("\nUse --help after a group for examples, e.g. openmates settings billing --help");
9731
10246
  }
9732
10247
  function printNewChatSuggestionsHelp() {
9733
10248
  console.log(`New chat suggestions command: