@eng-ai/sdk 2.3.0 → 2.7.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/CHANGELOG.md +63 -0
- package/README.md +64 -0
- package/bin/eng-ai.js +130 -0
- package/package.json +1 -1
- package/src/client.js +294 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@eng-ai/sdk` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 2.7.0 - 2026-04-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- External Documents APIs (v2) in SDK:
|
|
10
|
+
- `listDocuments({ projectId?, tags?, tagMode? })`
|
|
11
|
+
- `getDocument(documentId)`
|
|
12
|
+
- `uploadDocument({ file, fileName?, contentType?, projectId?, tags? })`
|
|
13
|
+
- `updateDocumentTags(documentId, tags)`
|
|
14
|
+
- `deleteDocument(documentId)`
|
|
15
|
+
- Tag-aware query support for external integrations (`tagMode=any|all` + repeated `tags`).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- SDK internals now support multipart requests for file uploads with consistent timeout and error handling.
|
|
20
|
+
|
|
21
|
+
## 2.6.0 - 2026-04-07
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Vertex AI BYOK fields in settings APIs:
|
|
26
|
+
- `setAISettings({ vertexProjectId?, vertexLocation?, vertexServiceAccountJson? })`
|
|
27
|
+
- CLI `eng-ai settings ai set` now supports:
|
|
28
|
+
- `--provider vertex`
|
|
29
|
+
- `--vertex-project-id`
|
|
30
|
+
- `--vertex-location`
|
|
31
|
+
- `--vertex-service-account-json`
|
|
32
|
+
|
|
33
|
+
### Security
|
|
34
|
+
|
|
35
|
+
- SDK debug redaction expanded for service-account sensitive keys (`service_account`, `private_key`, `private_key_id`, `client_email`, `client_id`).
|
|
36
|
+
|
|
37
|
+
## 2.5.0 - 2026-04-05
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- External profile support (API key owner scope):
|
|
42
|
+
- `getProfile()`
|
|
43
|
+
- `updateProfile({ preferredName?, honorific?, crea?, specialty?, avatarUrl? })`
|
|
44
|
+
- CLI `eng-ai` profile commands:
|
|
45
|
+
- `profile get`
|
|
46
|
+
- `profile set --preferred-name ... --honorific ... --crea ... --specialty ... --avatar-url ...`
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Frontend profile update flow now writes through backend API only (`/api/v1/auth/me/profile`) and no longer depends on direct profile writes in Supabase client.
|
|
51
|
+
|
|
52
|
+
## 2.4.0 - 2026-04-05
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- External user settings support (API key only):
|
|
57
|
+
- `getAISettings()`
|
|
58
|
+
- `setAISettings({ provider, model, projectModel?, apiKey?, googleApiKeyRag?, preferredTimezone? })`
|
|
59
|
+
- `clearAISettings()`
|
|
60
|
+
- `setVoiceLiveEnabled(enabled)`
|
|
61
|
+
- `getGeneralSettings()`
|
|
62
|
+
- `setGeneralSettings(preferredTimezone?)`
|
|
63
|
+
- CLI `eng-ai` commands for external settings:
|
|
64
|
+
- `settings ai get|set|clear`
|
|
65
|
+
- `settings ai voice-live --enabled true|false`
|
|
66
|
+
- `settings general get|set`
|
|
67
|
+
|
|
5
68
|
## 2.3.0 - 2026-04-05
|
|
6
69
|
|
|
7
70
|
### Added
|
package/README.md
CHANGED
|
@@ -81,6 +81,33 @@ const context = await client.getProjectContext(project.id);
|
|
|
81
81
|
console.log(context.autonomous_tasks_mode);
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
Project documents (tags, filters, upload, update):
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const uploaded = await client.uploadDocument({
|
|
88
|
+
file: new Blob(["conteudo tecnico"], { type: "text/plain" }),
|
|
89
|
+
fileName: "escopo.txt",
|
|
90
|
+
projectId: project.id,
|
|
91
|
+
tags: ["Marco Estrutural", "Planejamento"]
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await client.updateDocumentTags(uploaded.id, ["Marco Estrutural", "Revisado"]);
|
|
95
|
+
|
|
96
|
+
const docsAny = await client.listDocuments({
|
|
97
|
+
projectId: project.id,
|
|
98
|
+
tags: ["Marco Estrutural", "Revisado"],
|
|
99
|
+
tagMode: "any"
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const docsAll = await client.listDocuments({
|
|
103
|
+
projectId: project.id,
|
|
104
|
+
tags: ["Marco Estrutural", "Revisado"],
|
|
105
|
+
tagMode: "all"
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log(docsAny.length, docsAll.length);
|
|
109
|
+
```
|
|
110
|
+
|
|
84
111
|
External approval flow (pull):
|
|
85
112
|
|
|
86
113
|
```js
|
|
@@ -100,6 +127,36 @@ Supported automation plan actions:
|
|
|
100
127
|
- `rejectAutomationPlan(projectId, planId, note?, { idempotencyKey? })`
|
|
101
128
|
- `applyAutomationPlan(projectId, planId, { idempotencyKey? })`
|
|
102
129
|
|
|
130
|
+
External user settings (API key owner scope):
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
const ai = await client.getAISettings();
|
|
134
|
+
await client.setAISettings({
|
|
135
|
+
provider: "vertex",
|
|
136
|
+
model: "gemini-2.5-flash",
|
|
137
|
+
projectModel: "gemini-2.5-flash",
|
|
138
|
+
vertexProjectId: "my-gcp-project",
|
|
139
|
+
vertexLocation: "southamerica-east1",
|
|
140
|
+
vertexServiceAccountJson: process.env.VERTEX_SERVICE_ACCOUNT_JSON,
|
|
141
|
+
preferredTimezone: "America/Sao_Paulo",
|
|
142
|
+
});
|
|
143
|
+
await client.setVoiceLiveEnabled(true);
|
|
144
|
+
const general = await client.getGeneralSettings();
|
|
145
|
+
await client.setGeneralSettings(general.preferred_timezone || "America/Sao_Paulo");
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
External profile (API key owner scope):
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
const profile = await client.getProfile();
|
|
152
|
+
await client.updateProfile({
|
|
153
|
+
preferredName: "Felipe",
|
|
154
|
+
honorific: "Eng.",
|
|
155
|
+
crea: "123456/D",
|
|
156
|
+
specialty: "Geotecnia",
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
103
160
|
CLI (`eng-ai`) included in package:
|
|
104
161
|
|
|
105
162
|
```bash
|
|
@@ -108,6 +165,13 @@ ENG_AI_API_KEY=sk_xxx eng-ai plans actionable list --limit 30
|
|
|
108
165
|
ENG_AI_API_KEY=sk_xxx eng-ai plans approve --project-id <id> --plan-id <id> --note "Approved"
|
|
109
166
|
ENG_AI_API_KEY=sk_xxx eng-ai plans apply --project-id <id> --plan-id <id>
|
|
110
167
|
ENG_AI_API_KEY=sk_xxx eng-ai plans watch --interval 15 --limit 30
|
|
168
|
+
ENG_AI_API_KEY=sk_xxx eng-ai settings ai get
|
|
169
|
+
ENG_AI_API_KEY=sk_xxx eng-ai settings ai set --provider google --model gemini-3.1-flash-lite-preview --preferred-timezone America/Sao_Paulo
|
|
170
|
+
ENG_AI_API_KEY=sk_xxx eng-ai settings ai set --provider vertex --model gemini-2.5-flash --vertex-project-id my-gcp-project --vertex-location southamerica-east1 --vertex-service-account-json "$VERTEX_SERVICE_ACCOUNT_JSON"
|
|
171
|
+
ENG_AI_API_KEY=sk_xxx eng-ai settings ai voice-live --enabled true
|
|
172
|
+
ENG_AI_API_KEY=sk_xxx eng-ai settings general set --preferred-timezone America/Sao_Paulo
|
|
173
|
+
ENG_AI_API_KEY=sk_xxx eng-ai profile get
|
|
174
|
+
ENG_AI_API_KEY=sk_xxx eng-ai profile set --preferred-name Felipe --honorific "Eng." --crea "123456/D" --specialty Geotecnia
|
|
111
175
|
```
|
|
112
176
|
|
|
113
177
|
Default base URL: `https://api.eng-ai.com/api/v2`.
|
package/bin/eng-ai.js
CHANGED
|
@@ -40,6 +40,14 @@ function toPositiveInt(value, fallback) {
|
|
|
40
40
|
return Math.floor(parsed);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function toBoolean(value, fallback = undefined) {
|
|
44
|
+
if (value === undefined || value === null) return fallback;
|
|
45
|
+
const normalized = String(value).trim().toLowerCase();
|
|
46
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
|
47
|
+
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
function maskId(value) {
|
|
44
52
|
const text = String(value || "");
|
|
45
53
|
if (text.length <= 8) return text;
|
|
@@ -57,6 +65,14 @@ Usage:
|
|
|
57
65
|
eng-ai plans reject --project-id <id> --plan-id <id> [--note <text>] [--idempotency-key <key>]
|
|
58
66
|
eng-ai plans apply --project-id <id> --plan-id <id> [--idempotency-key <key>]
|
|
59
67
|
eng-ai plans watch [--interval <seconds>] [--limit <n>]
|
|
68
|
+
eng-ai settings ai get
|
|
69
|
+
eng-ai settings ai set --provider <openai|anthropic|google|vertex> --model <name> [--project-model <name>] [--api-key <key>] [--google-api-key-rag <key>] [--preferred-timezone <IANA_TZ>] [--vertex-project-id <id>] [--vertex-location <region>] [--vertex-service-account-json <json>]
|
|
70
|
+
eng-ai settings ai clear
|
|
71
|
+
eng-ai settings ai voice-live --enabled <true|false>
|
|
72
|
+
eng-ai settings general get
|
|
73
|
+
eng-ai settings general set [--preferred-timezone <IANA_TZ>]
|
|
74
|
+
eng-ai profile get
|
|
75
|
+
eng-ai profile set [--preferred-name <name>] [--honorific <text>] [--crea <text>] [--specialty <text>] [--avatar-url <url>]
|
|
60
76
|
|
|
61
77
|
Environment:
|
|
62
78
|
ENG_AI_API_KEY (required)
|
|
@@ -192,6 +208,112 @@ async function runPlansCommand(client, subTokens, options) {
|
|
|
192
208
|
console.log(JSON.stringify(result, null, 2));
|
|
193
209
|
}
|
|
194
210
|
|
|
211
|
+
async function runSettingsCommand(client, subTokens, options) {
|
|
212
|
+
const section = String(subTokens[0] || "").trim().toLowerCase();
|
|
213
|
+
const action = String(subTokens[1] || "").trim().toLowerCase();
|
|
214
|
+
|
|
215
|
+
if (!section || !action) {
|
|
216
|
+
throw new Error("Unsupported settings command. Use: settings ai|get ...");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (section === "ai") {
|
|
220
|
+
if (action === "get") {
|
|
221
|
+
const result = await client.getAISettings();
|
|
222
|
+
console.log(JSON.stringify(result, null, 2));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (action === "clear") {
|
|
227
|
+
const result = await client.clearAISettings();
|
|
228
|
+
console.log("AI settings removed.");
|
|
229
|
+
console.log(JSON.stringify(result, null, 2));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (action === "set") {
|
|
234
|
+
const provider = options.provider;
|
|
235
|
+
const model = options.model;
|
|
236
|
+
if (!provider || !model) {
|
|
237
|
+
throw new Error("Missing required options: --provider and --model");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = await client.setAISettings({
|
|
241
|
+
provider,
|
|
242
|
+
model,
|
|
243
|
+
projectModel: options["project-model"],
|
|
244
|
+
apiKey: options["api-key"],
|
|
245
|
+
googleApiKeyRag: options["google-api-key-rag"],
|
|
246
|
+
preferredTimezone: options["preferred-timezone"],
|
|
247
|
+
vertexProjectId: options["vertex-project-id"],
|
|
248
|
+
vertexLocation: options["vertex-location"],
|
|
249
|
+
vertexServiceAccountJson: options["vertex-service-account-json"],
|
|
250
|
+
});
|
|
251
|
+
console.log("AI settings updated.");
|
|
252
|
+
console.log(JSON.stringify(result, null, 2));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (action === "voice-live") {
|
|
257
|
+
const enabled = toBoolean(options.enabled, undefined);
|
|
258
|
+
if (enabled === undefined) {
|
|
259
|
+
throw new Error("Missing or invalid --enabled. Use --enabled true|false");
|
|
260
|
+
}
|
|
261
|
+
const result = await client.setVoiceLiveEnabled(enabled);
|
|
262
|
+
console.log("Voice-live toggle updated.");
|
|
263
|
+
console.log(JSON.stringify(result, null, 2));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (section === "general") {
|
|
269
|
+
if (action === "get") {
|
|
270
|
+
const result = await client.getGeneralSettings();
|
|
271
|
+
console.log(JSON.stringify(result, null, 2));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (action === "set") {
|
|
276
|
+
const result = await client.setGeneralSettings(options["preferred-timezone"] || null);
|
|
277
|
+
console.log("General settings updated.");
|
|
278
|
+
console.log(JSON.stringify(result, null, 2));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
throw new Error("Unsupported settings command. Use ai|get/set/clear/voice-live or general|get/set");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function runProfileCommand(client, subTokens, options) {
|
|
287
|
+
const action = String(subTokens[0] || "").trim().toLowerCase();
|
|
288
|
+
if (action === "get") {
|
|
289
|
+
const result = await client.getProfile();
|
|
290
|
+
console.log(JSON.stringify(result, null, 2));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (action === "set") {
|
|
295
|
+
const payload = {
|
|
296
|
+
preferredName: options["preferred-name"],
|
|
297
|
+
honorific: options.honorific,
|
|
298
|
+
crea: options.crea,
|
|
299
|
+
specialty: options.specialty,
|
|
300
|
+
avatarUrl: options["avatar-url"],
|
|
301
|
+
};
|
|
302
|
+
const hasAnyField = Object.values(payload).some((value) => value !== undefined);
|
|
303
|
+
if (!hasAnyField) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
"Missing profile fields. Use at least one: --preferred-name, --honorific, --crea, --specialty, --avatar-url"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const result = await client.updateProfile(payload);
|
|
309
|
+
console.log("Profile updated.");
|
|
310
|
+
console.log(JSON.stringify(result, null, 2));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw new Error("Unsupported profile command. Use: profile get|set");
|
|
315
|
+
}
|
|
316
|
+
|
|
195
317
|
async function main() {
|
|
196
318
|
const { tokens, options } = parseArgs(process.argv.slice(2));
|
|
197
319
|
const command = tokens[0];
|
|
@@ -209,6 +331,14 @@ async function main() {
|
|
|
209
331
|
await runPlansCommand(client, tokens.slice(1), options);
|
|
210
332
|
return;
|
|
211
333
|
}
|
|
334
|
+
if (command === "settings") {
|
|
335
|
+
await runSettingsCommand(client, tokens.slice(1), options);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (command === "profile") {
|
|
339
|
+
await runProfileCommand(client, tokens.slice(1), options);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
212
342
|
|
|
213
343
|
throw new Error(`Unknown command: ${command}`);
|
|
214
344
|
}
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const DEFAULT_BASE_URL = "https://api.eng-ai.com/api/v2";
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
3
|
-
const REDACT_KEY_PATTERN =
|
|
3
|
+
const REDACT_KEY_PATTERN =
|
|
4
|
+
/api[-_]?key|authorization|token|secret|password|cookie|set-cookie|service[-_]?account|private[-_]?key|private[-_]?key[-_]?id|client[-_]?email|client[-_]?id/i;
|
|
4
5
|
const VALID_AUTONOMOUS_TASKS_MODES = new Set([
|
|
5
6
|
"approval_required",
|
|
6
7
|
"approval_delete_only",
|
|
7
8
|
"full_autonomy",
|
|
8
9
|
]);
|
|
10
|
+
const VALID_DOCUMENT_TAG_MODES = new Set(["any", "all"]);
|
|
9
11
|
|
|
10
12
|
function isPlainObject(value) {
|
|
11
13
|
return !!value && Object.prototype.toString.call(value) === "[object Object]";
|
|
@@ -165,6 +167,28 @@ export class EngAIClient {
|
|
|
165
167
|
return { ...this.defaultHeaders, ...extra };
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
_normalizeTagArray(tags, fieldName = "tags") {
|
|
171
|
+
if (tags === undefined || tags === null) return [];
|
|
172
|
+
if (!Array.isArray(tags)) {
|
|
173
|
+
throw new EngAIError(`${fieldName} must be an array of strings`, {
|
|
174
|
+
status: 400,
|
|
175
|
+
code: "validation_error",
|
|
176
|
+
detail: `${fieldName} must be an array`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const normalized = [];
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
for (const rawTag of tags) {
|
|
182
|
+
const tag = String(rawTag || "").trim().replace(/\s+/g, " ");
|
|
183
|
+
if (!tag) continue;
|
|
184
|
+
const key = tag.toLowerCase();
|
|
185
|
+
if (seen.has(key)) continue;
|
|
186
|
+
seen.add(key);
|
|
187
|
+
normalized.push(tag);
|
|
188
|
+
}
|
|
189
|
+
return normalized;
|
|
190
|
+
}
|
|
191
|
+
|
|
168
192
|
async _parseResponseBody(response) {
|
|
169
193
|
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
170
194
|
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
@@ -282,6 +306,77 @@ export class EngAIClient {
|
|
|
282
306
|
}
|
|
283
307
|
}
|
|
284
308
|
|
|
309
|
+
async _requestMultipart(pathname, { method = "POST", formData, headers = {} } = {}) {
|
|
310
|
+
if (!(formData instanceof FormData)) {
|
|
311
|
+
throw new EngAIError("formData must be a FormData instance", {
|
|
312
|
+
status: 400,
|
|
313
|
+
code: "validation_error",
|
|
314
|
+
detail: "multipart request requires FormData",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const url = this._buildUrl(pathname);
|
|
319
|
+
const requestHeaders = {
|
|
320
|
+
...this.defaultHeaders,
|
|
321
|
+
...headers,
|
|
322
|
+
};
|
|
323
|
+
delete requestHeaders["Content-Type"];
|
|
324
|
+
|
|
325
|
+
this._log("info", "request.start", {
|
|
326
|
+
method,
|
|
327
|
+
url,
|
|
328
|
+
headers: requestHeaders,
|
|
329
|
+
body: { type: "form-data" },
|
|
330
|
+
timeout_ms: this.timeoutMs,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const response = await this._fetchWithTimeout(url, {
|
|
335
|
+
method,
|
|
336
|
+
headers: requestHeaders,
|
|
337
|
+
body: formData,
|
|
338
|
+
});
|
|
339
|
+
const parsedBody = await this._parseResponseBody(response);
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
const err = this._buildHttpError(response, parsedBody, method, url);
|
|
342
|
+
this._log("warn", "request.error", err.toJSON());
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
this._log("info", "request.success", {
|
|
346
|
+
method,
|
|
347
|
+
url,
|
|
348
|
+
status: response.status,
|
|
349
|
+
request_id: extractRequestId(parsedBody),
|
|
350
|
+
});
|
|
351
|
+
return parsedBody;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error instanceof EngAIError) throw error;
|
|
354
|
+
if (error?.name === "AbortError") {
|
|
355
|
+
const timeoutErr = new EngAIError(
|
|
356
|
+
`ENG-AI request timeout after ${this.timeoutMs}ms`,
|
|
357
|
+
{ status: 408, code: "request_timeout", method, url, detail: "Request timeout" },
|
|
358
|
+
error
|
|
359
|
+
);
|
|
360
|
+
this._log("warn", "request.timeout", timeoutErr.toJSON());
|
|
361
|
+
throw timeoutErr;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const networkErr = new EngAIError(
|
|
365
|
+
"ENG-AI network error",
|
|
366
|
+
{
|
|
367
|
+
status: 0,
|
|
368
|
+
code: "network_error",
|
|
369
|
+
detail: error?.message || "Unknown network error",
|
|
370
|
+
method,
|
|
371
|
+
url,
|
|
372
|
+
},
|
|
373
|
+
error
|
|
374
|
+
);
|
|
375
|
+
this._log("warn", "request.network_error", networkErr.toJSON());
|
|
376
|
+
throw networkErr;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
285
380
|
_requireNonEmptyString(value, fieldName) {
|
|
286
381
|
const normalized = String(value || "").trim();
|
|
287
382
|
if (!normalized) {
|
|
@@ -378,6 +473,204 @@ export class EngAIClient {
|
|
|
378
473
|
});
|
|
379
474
|
}
|
|
380
475
|
|
|
476
|
+
async listDocuments(options = {}) {
|
|
477
|
+
const query = new URLSearchParams();
|
|
478
|
+
if (options.projectId ?? options.project_id) {
|
|
479
|
+
query.set("project_id", String(options.projectId ?? options.project_id));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const tagMode = options.tagMode ?? options.tag_mode;
|
|
483
|
+
if (tagMode !== undefined && tagMode !== null) {
|
|
484
|
+
const normalizedTagMode = String(tagMode).trim().toLowerCase();
|
|
485
|
+
if (!VALID_DOCUMENT_TAG_MODES.has(normalizedTagMode)) {
|
|
486
|
+
throw new EngAIError("invalid tagMode for listDocuments", {
|
|
487
|
+
status: 400,
|
|
488
|
+
code: "validation_error",
|
|
489
|
+
detail: "tagMode must be one of: any, all",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
query.set("tag_mode", normalizedTagMode);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const normalizedTags = this._normalizeTagArray(options.tags, "tags");
|
|
496
|
+
for (const tag of normalizedTags) {
|
|
497
|
+
query.append("tags", tag);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
501
|
+
return await this._request(`/documents/${suffix}`, {
|
|
502
|
+
method: "GET",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async getDocument(documentId) {
|
|
507
|
+
const normalizedDocumentId = this._requireNonEmptyString(documentId, "documentId");
|
|
508
|
+
return await this._request(`/documents/${encodeURIComponent(normalizedDocumentId)}`, {
|
|
509
|
+
method: "GET",
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async uploadDocument(payload = {}) {
|
|
514
|
+
const file = payload.file ?? payload.content;
|
|
515
|
+
if (file === undefined || file === null) {
|
|
516
|
+
throw new EngAIError("file is required for uploadDocument", {
|
|
517
|
+
status: 400,
|
|
518
|
+
code: "validation_error",
|
|
519
|
+
detail: "payload.file must be provided",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const formData = new FormData();
|
|
524
|
+
const filename = String(payload.fileName ?? payload.filename ?? "document.bin");
|
|
525
|
+
const contentType = payload.contentType ?? payload.content_type;
|
|
526
|
+
const normalizedProjectId = payload.projectId ?? payload.project_id;
|
|
527
|
+
const normalizedTags = this._normalizeTagArray(payload.tags, "tags");
|
|
528
|
+
|
|
529
|
+
if (
|
|
530
|
+
file instanceof Blob ||
|
|
531
|
+
(typeof File !== "undefined" && file instanceof File)
|
|
532
|
+
) {
|
|
533
|
+
formData.append("file", file, filename);
|
|
534
|
+
} else if (file instanceof ArrayBuffer) {
|
|
535
|
+
formData.append(
|
|
536
|
+
"file",
|
|
537
|
+
new Blob([file], {
|
|
538
|
+
type: contentType ? String(contentType) : "application/octet-stream",
|
|
539
|
+
}),
|
|
540
|
+
filename
|
|
541
|
+
);
|
|
542
|
+
} else if (ArrayBuffer.isView(file)) {
|
|
543
|
+
formData.append(
|
|
544
|
+
"file",
|
|
545
|
+
new Blob([file], {
|
|
546
|
+
type: contentType ? String(contentType) : "application/octet-stream",
|
|
547
|
+
}),
|
|
548
|
+
filename
|
|
549
|
+
);
|
|
550
|
+
} else {
|
|
551
|
+
throw new EngAIError("unsupported file type for uploadDocument", {
|
|
552
|
+
status: 400,
|
|
553
|
+
code: "validation_error",
|
|
554
|
+
detail: "file must be Blob, File, ArrayBuffer, Buffer, or Uint8Array",
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (normalizedProjectId) {
|
|
559
|
+
formData.append("project_id", String(normalizedProjectId));
|
|
560
|
+
}
|
|
561
|
+
if (normalizedTags.length > 0) {
|
|
562
|
+
formData.append("tags", JSON.stringify(normalizedTags));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return await this._requestMultipart("/documents/upload", {
|
|
566
|
+
method: "POST",
|
|
567
|
+
formData,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async updateDocumentTags(documentId, tags = []) {
|
|
572
|
+
const normalizedDocumentId = this._requireNonEmptyString(documentId, "documentId");
|
|
573
|
+
const normalizedTags = this._normalizeTagArray(tags, "tags");
|
|
574
|
+
return await this._request(`/documents/${encodeURIComponent(normalizedDocumentId)}/tags`, {
|
|
575
|
+
method: "PATCH",
|
|
576
|
+
body: { tags: normalizedTags },
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async deleteDocument(documentId) {
|
|
581
|
+
const normalizedDocumentId = this._requireNonEmptyString(documentId, "documentId");
|
|
582
|
+
return await this._request(`/documents/${encodeURIComponent(normalizedDocumentId)}`, {
|
|
583
|
+
method: "DELETE",
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async getAISettings() {
|
|
588
|
+
return await this._request("/settings/llm", {
|
|
589
|
+
method: "GET",
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async setAISettings(payload) {
|
|
594
|
+
const provider = this._requireNonEmptyString(payload?.provider, "provider").toLowerCase();
|
|
595
|
+
const model = this._requireNonEmptyString(payload?.model, "model");
|
|
596
|
+
const body = {
|
|
597
|
+
provider,
|
|
598
|
+
model,
|
|
599
|
+
project_model: payload?.projectModel ?? payload?.project_model,
|
|
600
|
+
api_key: payload?.apiKey ?? payload?.api_key,
|
|
601
|
+
google_api_key_rag: payload?.googleApiKeyRag ?? payload?.google_api_key_rag,
|
|
602
|
+
vertex_project_id: payload?.vertexProjectId ?? payload?.vertex_project_id,
|
|
603
|
+
vertex_location: payload?.vertexLocation ?? payload?.vertex_location,
|
|
604
|
+
vertex_service_account_json:
|
|
605
|
+
payload?.vertexServiceAccountJson ?? payload?.vertex_service_account_json,
|
|
606
|
+
preferred_timezone: payload?.preferredTimezone ?? payload?.preferred_timezone,
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
return await this._request("/settings/llm", {
|
|
610
|
+
method: "PUT",
|
|
611
|
+
body,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async clearAISettings() {
|
|
616
|
+
return await this._request("/settings/llm", {
|
|
617
|
+
method: "DELETE",
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async setVoiceLiveEnabled(enabled) {
|
|
622
|
+
return await this._request("/settings/voice-live", {
|
|
623
|
+
method: "PUT",
|
|
624
|
+
body: { enabled: !!enabled },
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async getGeneralSettings() {
|
|
629
|
+
return await this._request("/settings/general", {
|
|
630
|
+
method: "GET",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async setGeneralSettings(preferredTimezone = null) {
|
|
635
|
+
return await this._request("/settings/general", {
|
|
636
|
+
method: "PUT",
|
|
637
|
+
body: {
|
|
638
|
+
preferred_timezone: preferredTimezone || null,
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async getProfile() {
|
|
644
|
+
return await this._request("/profile", {
|
|
645
|
+
method: "GET",
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async updateProfile(payload = {}) {
|
|
650
|
+
const body = {
|
|
651
|
+
preferred_name: payload?.preferredName ?? payload?.preferred_name,
|
|
652
|
+
honorific: payload?.honorific,
|
|
653
|
+
crea: payload?.crea ?? payload?.crea_cau,
|
|
654
|
+
specialty: payload?.specialty,
|
|
655
|
+
avatar_url: payload?.avatarUrl ?? payload?.avatar_url,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const hasAnyField = Object.values(body).some((value) => value !== undefined);
|
|
659
|
+
if (!hasAnyField) {
|
|
660
|
+
throw new EngAIError("at least one profile field is required for updateProfile", {
|
|
661
|
+
status: 400,
|
|
662
|
+
code: "validation_error",
|
|
663
|
+
detail:
|
|
664
|
+
"provide one of: preferred_name, honorific, crea, specialty, avatar_url",
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return await this._request("/profile", {
|
|
669
|
+
method: "PUT",
|
|
670
|
+
body,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
381
674
|
async getProjectContext(projectId) {
|
|
382
675
|
const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
|
|
383
676
|
return await this._request(`/projects/${encodeURIComponent(normalizedProjectId)}/context`, {
|