openmates 0.11.1 → 0.12.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,46 +1,52 @@
1
1
  # openmates
2
2
 
3
- OpenMates npm CLI + SDK (pair-auth login only).
3
+ Terminal CLI and Node.js SDK for OpenMates. Use it to pair-login, create or continue encrypted chats, run app skills, manage safe account settings, browse docs, and administer self-hosted installs.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
8
  npm install -g openmates
9
+ openmates chats list
10
+ openmates login
9
11
  ```
10
12
 
11
- ## Environment
12
-
13
- The CLI auto-derives the web app URL from the API URL so the pair token always lands on the right backend.
13
+ Without a session, `openmates chats list`, `show`, and `open` display clearly labeled public example chats from the web app. Login uses pair-auth: the CLI shows a QR code or PIN, and you approve it in the web app. To create a new account from the terminal, run `openmates signup`; passwords and recovery secrets are collected through hidden prompts, never command-line flags.
14
14
 
15
- | Target | Command prefix |
16
- | -------------------- | --------------------------------------------------------------------------------- |
17
- | Production (default) | _(none)_ |
18
- | Dev server | `OPENMATES_API_URL=https://api.dev.openmates.org` |
19
- | Self-hosted | `OPENMATES_API_URL=https://api.example.com OPENMATES_APP_URL=https://example.com` |
20
-
21
- ## CLI Commands
15
+ ## First Commands
22
16
 
23
17
  ```bash
24
- openmates login
25
18
  openmates whoami --json
26
19
  openmates chats list
27
- openmates chats new "Hello"
20
+ openmates chats show example-gigantic-airplanes
21
+ openmates chats new "Help me plan my day"
28
22
  openmates chats send --chat <chat-id> "continue"
29
- openmates apps list --api-key <key>
30
- openmates apps ai ask "What is Docker?" --api-key <key>
31
- openmates settings get /v1/settings/export-account-data --json
23
+ openmates apps list
24
+ openmates apps ai ask "What is Docker?"
25
+ openmates apps code run --language python --code 'print("Hello")'
26
+ openmates settings account export data --json
32
27
  openmates settings memories list --json
28
+ openmates docs list
29
+ openmates server install
33
30
  ```
34
31
 
35
- ## Safety Limits
32
+ Run `openmates --help` or `openmates <command> --help` for the full command surface.
36
33
 
37
- The CLI intentionally blocks endpoint writes for:
34
+ ## Environments
38
35
 
39
- - API key creation
40
- - Password setup/update
41
- - 2FA setup/provider changes
36
+ The CLI derives the web app URL from the API URL so pair-auth lands on the matching backend.
42
37
 
43
- See `src/client.ts` (`BLOCKED_SETTINGS_POST_PATHS`).
38
+ | Target | Command prefix |
39
+ | --- | --- |
40
+ | Production | _(none)_ |
41
+ | Dev server | `OPENMATES_API_URL=https://api.dev.openmates.org` |
42
+ | Installed self-hosted server | _(none after `openmates server install`)_ |
43
+ | Remote self-hosted server | `OPENMATES_API_URL=https://api.example.com` |
44
+
45
+ After `openmates server install`, fresh CLI commands default to that self-hosted server (`http://localhost:8000`) unless you already have a saved login session, set `OPENMATES_API_URL`, or pass `--api-url <url>`. App-skill execution can use your logged-in session, `--api-key <key>`, or `OPENMATES_API_KEY`.
46
+
47
+ ## Safety Limits
48
+
49
+ Predefined settings commands are supported; raw `settings get/post/patch/delete` passthrough is intentionally unavailable. High-risk or browser-only flows such as passkey management, password changes, API key creation, device approvals, and card checkout stay in the web app. The code-level guard is `BLOCKED_SETTINGS_MUTATE_PATHS` in `src/client.ts`.
44
50
 
45
51
  ## SDK
46
52
 
@@ -48,4 +54,7 @@ See `src/client.ts` (`BLOCKED_SETTINGS_POST_PATHS`).
48
54
  import { OpenMatesClient } from "openmates";
49
55
 
50
56
  const client = OpenMatesClient.load();
57
+ const chats = await client.listChats();
51
58
  ```
59
+
60
+ Source docs: `docs/user-guide/cli/` in the OpenMates repository.
@@ -0,0 +1,242 @@
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
+ function getUploadMimeType(filename) {
27
+ const ext = extname(filename).toLowerCase();
28
+ switch (ext) {
29
+ case ".jpg":
30
+ case ".jpeg":
31
+ return "image/jpeg";
32
+ case ".png":
33
+ return "image/png";
34
+ case ".webp":
35
+ return "image/webp";
36
+ case ".gif":
37
+ return "image/gif";
38
+ case ".heic":
39
+ return "image/heic";
40
+ case ".heif":
41
+ return "image/heif";
42
+ case ".bmp":
43
+ return "image/bmp";
44
+ case ".tif":
45
+ case ".tiff":
46
+ return "image/tiff";
47
+ case ".svg":
48
+ return "image/svg+xml";
49
+ case ".pdf":
50
+ return "application/pdf";
51
+ case ".mp3":
52
+ return "audio/mpeg";
53
+ case ".m4a":
54
+ case ".mp4":
55
+ return "audio/mp4";
56
+ case ".wav":
57
+ return "audio/wav";
58
+ case ".webm":
59
+ return "audio/webm";
60
+ case ".ogg":
61
+ case ".oga":
62
+ return "audio/ogg";
63
+ case ".aac":
64
+ return "audio/aac";
65
+ default:
66
+ return "application/octet-stream";
67
+ }
68
+ }
69
+ async function uploadFile(filePath, session) {
70
+ const filename = basename(filePath);
71
+ const fileBytes = readFileSync(filePath);
72
+ const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/file`;
73
+ const origin = getUploadOrigin(session.apiUrl);
74
+ const cookies = [];
75
+ if (session.cookies?.auth_refresh_token) {
76
+ cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
77
+ }
78
+ let response;
79
+ let lastError;
80
+ for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
81
+ try {
82
+ const blob = new Blob([fileBytes], { type: getUploadMimeType(filename) });
83
+ const formData = new FormData();
84
+ formData.append("file", blob, filename);
85
+ response = await fetch(uploadUrl, {
86
+ method: "POST",
87
+ body: formData,
88
+ headers: {
89
+ Origin: origin,
90
+ ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
91
+ },
92
+ signal: AbortSignal.timeout(10 * 60 * 1e3)
93
+ // 10-minute timeout
94
+ });
95
+ break;
96
+ } catch (error) {
97
+ lastError = error;
98
+ if (attempt === UPLOAD_MAX_ATTEMPTS) break;
99
+ await new Promise((resolve) => setTimeout(resolve, UPLOAD_RETRY_DELAY_MS));
100
+ }
101
+ }
102
+ if (!response) {
103
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
104
+ throw new Error(message || "Upload request failed.");
105
+ }
106
+ if (!response.ok) {
107
+ const status = response.status;
108
+ let errorMessage;
109
+ switch (status) {
110
+ case 401:
111
+ errorMessage = "Authentication failed. Run `openmates login` to re-authenticate.";
112
+ break;
113
+ case 413:
114
+ errorMessage = "File too large (maximum 100 MB).";
115
+ break;
116
+ case 415:
117
+ errorMessage = "Unsupported file type.";
118
+ break;
119
+ case 422: {
120
+ const body = await response.text().catch(() => "");
121
+ errorMessage = body.includes("malware") ? "File rejected: malware detected." : body.includes("content_safety") ? "File rejected: content safety violation." : `Upload validation failed: ${body}`;
122
+ break;
123
+ }
124
+ case 429:
125
+ errorMessage = "Upload rate limit exceeded. Try again in a minute.";
126
+ break;
127
+ default:
128
+ errorMessage = `Upload failed (HTTP ${status}).`;
129
+ }
130
+ throw new Error(errorMessage);
131
+ }
132
+ const data = await response.json();
133
+ return data;
134
+ }
135
+ async function transcribeUploadedAudio(uploadResult, filename, session, options = {}) {
136
+ const s3Key = uploadResult.files?.original?.s3_key ?? Object.values(uploadResult.files ?? {})[0]?.s3_key;
137
+ if (!s3Key) {
138
+ throw new Error("Upload succeeded but no audio file key was returned.");
139
+ }
140
+ const cookies = [];
141
+ if (session.cookies?.auth_refresh_token) {
142
+ cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
143
+ }
144
+ const requestItem = {
145
+ id: options.requestId ?? uploadResult.embed_id,
146
+ embed_id: uploadResult.embed_id,
147
+ s3_key: s3Key,
148
+ s3_base_url: uploadResult.s3_base_url,
149
+ aes_key: uploadResult.aes_key,
150
+ aes_nonce: uploadResult.aes_nonce,
151
+ vault_wrapped_aes_key: uploadResult.vault_wrapped_aes_key,
152
+ filename,
153
+ mime_type: uploadResult.content_type
154
+ };
155
+ if (options.chatId) {
156
+ requestItem.chat_id = options.chatId;
157
+ }
158
+ const response = await fetch(
159
+ `${session.apiUrl.replace(/\/$/, "")}/v1/apps/audio/skills/transcribe`,
160
+ {
161
+ method: "POST",
162
+ headers: {
163
+ Accept: "application/json",
164
+ "Content-Type": "application/json",
165
+ ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
166
+ },
167
+ body: JSON.stringify({ requests: [requestItem] }),
168
+ signal: AbortSignal.timeout(10 * 60 * 1e3)
169
+ }
170
+ );
171
+ if (!response.ok) {
172
+ let detail = `Transcription failed (HTTP ${response.status}).`;
173
+ try {
174
+ const data2 = await response.json();
175
+ detail = data2.detail ?? data2.error ?? detail;
176
+ } catch {
177
+ }
178
+ throw new Error(detail);
179
+ }
180
+ const data = await response.json();
181
+ if (data.success === false) {
182
+ throw new Error(data.error ?? "Transcription failed.");
183
+ }
184
+ const requestId = options.requestId ?? uploadResult.embed_id;
185
+ const group = data.data?.results?.find((item) => item.id === requestId) ?? data.data?.results?.[0];
186
+ const result = group?.results?.[0];
187
+ if (!result) {
188
+ throw new Error(group?.error ?? "Transcription response did not include a result.");
189
+ }
190
+ if (result.error) {
191
+ throw new Error(result.error);
192
+ }
193
+ return {
194
+ transcript: result.transcript ?? null,
195
+ transcript_original: result.transcript_original ?? null,
196
+ transcript_corrected: result.transcript_corrected ?? null,
197
+ use_corrected: result.use_corrected ?? null,
198
+ correction_model: result.correction_model ?? null,
199
+ model: result.model ?? null
200
+ };
201
+ }
202
+ function getProfileImageMime(filename) {
203
+ const ext = extname(filename).toLowerCase();
204
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
205
+ if (ext === ".png") return "image/png";
206
+ throw new Error("Profile images must be JPEG or PNG files.");
207
+ }
208
+ async function uploadProfileImage(filePath, session) {
209
+ const filename = basename(filePath);
210
+ const fileBytes = readFileSync(filePath);
211
+ const contentType = getProfileImageMime(filename);
212
+ if (fileBytes.byteLength > PROFILE_IMAGE_MAX_SIZE_BYTES) {
213
+ throw new Error("Profile image must be 300 KB or smaller. Resize/compress the image and try again.");
214
+ }
215
+ const uploadUrl = `${getUploadUrl(session.apiUrl)}/v1/upload/profile-image`;
216
+ const origin = getUploadOrigin(session.apiUrl);
217
+ const cookies = [];
218
+ if (session.cookies?.auth_refresh_token) {
219
+ cookies.push(`auth_refresh_token=${session.cookies.auth_refresh_token}`);
220
+ }
221
+ const formData = new FormData();
222
+ formData.append("file", new Blob([fileBytes], { type: contentType }), filename);
223
+ const response = await fetch(uploadUrl, {
224
+ method: "POST",
225
+ body: formData,
226
+ headers: {
227
+ Origin: origin,
228
+ ...cookies.length > 0 ? { Cookie: cookies.join("; ") } : {}
229
+ }
230
+ });
231
+ const data = await response.json().catch(() => ({}));
232
+ if (!response.ok && !data.status) {
233
+ throw new Error(data.detail ?? `Profile image upload failed (HTTP ${response.status}).`);
234
+ }
235
+ return data;
236
+ }
237
+
238
+ export {
239
+ uploadFile,
240
+ transcribeUploadedAudio,
241
+ uploadProfileImage
242
+ };