openmates 0.11.1 → 0.12.0-alpha.1
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 +31 -22
- package/dist/chunk-AXNRPVLE.js +242 -0
- package/dist/chunk-GJPN6PKK.js +44073 -0
- package/dist/cli.js +2 -1
- package/dist/index.d.ts +240 -10
- package/dist/index.js +4 -1
- package/dist/uploadService-S464XJRA.js +10 -0
- package/package.json +13 -2
- package/dist/chunk-N6QY7K6L.js +0 -9894
package/README.md
CHANGED
|
@@ -1,46 +1,52 @@
|
|
|
1
1
|
# openmates
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
30
|
-
openmates apps ai ask "What is Docker?"
|
|
31
|
-
openmates
|
|
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
|
-
|
|
32
|
+
Run `openmates --help` or `openmates <command> --help` for the full command surface.
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
## Environments
|
|
38
35
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
+
};
|