@eng-ai/sdk 2.5.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 CHANGED
@@ -2,6 +2,38 @@
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
+
5
37
  ## 2.5.0 - 2026-04-05
6
38
 
7
39
  ### 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
@@ -105,9 +132,12 @@ External user settings (API key owner scope):
105
132
  ```js
106
133
  const ai = await client.getAISettings();
107
134
  await client.setAISettings({
108
- provider: "google",
109
- model: "gemini-3.1-flash-lite-preview",
110
- projectModel: "gemini-3.1-flash-lite-preview",
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,
111
141
  preferredTimezone: "America/Sao_Paulo",
112
142
  });
113
143
  await client.setVoiceLiveEnabled(true);
@@ -137,6 +167,7 @@ ENG_AI_API_KEY=sk_xxx eng-ai plans apply --project-id <id> --plan-id <id>
137
167
  ENG_AI_API_KEY=sk_xxx eng-ai plans watch --interval 15 --limit 30
138
168
  ENG_AI_API_KEY=sk_xxx eng-ai settings ai get
139
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"
140
171
  ENG_AI_API_KEY=sk_xxx eng-ai settings ai voice-live --enabled true
141
172
  ENG_AI_API_KEY=sk_xxx eng-ai settings general set --preferred-timezone America/Sao_Paulo
142
173
  ENG_AI_API_KEY=sk_xxx eng-ai profile get
package/bin/eng-ai.js CHANGED
@@ -66,7 +66,7 @@ Usage:
66
66
  eng-ai plans apply --project-id <id> --plan-id <id> [--idempotency-key <key>]
67
67
  eng-ai plans watch [--interval <seconds>] [--limit <n>]
68
68
  eng-ai settings ai get
69
- eng-ai settings ai set --provider <openai|anthropic|google> --model <name> [--project-model <name>] [--api-key <key>] [--google-api-key-rag <key>] [--preferred-timezone <IANA_TZ>]
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
70
  eng-ai settings ai clear
71
71
  eng-ai settings ai voice-live --enabled <true|false>
72
72
  eng-ai settings general get
@@ -244,6 +244,9 @@ async function runSettingsCommand(client, subTokens, options) {
244
244
  apiKey: options["api-key"],
245
245
  googleApiKeyRag: options["google-api-key-rag"],
246
246
  preferredTimezone: options["preferred-timezone"],
247
+ vertexProjectId: options["vertex-project-id"],
248
+ vertexLocation: options["vertex-location"],
249
+ vertexServiceAccountJson: options["vertex-service-account-json"],
247
250
  });
248
251
  console.log("AI settings updated.");
249
252
  console.log(JSON.stringify(result, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eng-ai/sdk",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Official JavaScript SDK for ENG-AI External API v2",
5
5
  "type": "module",
6
6
  "main": "./src/client.js",
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 = /api[-_]?key|authorization|token|secret|password|cookie|set-cookie/i;
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,117 @@ 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
+
381
587
  async getAISettings() {
382
588
  return await this._request("/settings/llm", {
383
589
  method: "GET",
@@ -393,6 +599,10 @@ export class EngAIClient {
393
599
  project_model: payload?.projectModel ?? payload?.project_model,
394
600
  api_key: payload?.apiKey ?? payload?.api_key,
395
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,
396
606
  preferred_timezone: payload?.preferredTimezone ?? payload?.preferred_timezone,
397
607
  };
398
608