@eng-ai/sdk 2.0.2 → 2.2.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,39 @@
2
2
 
3
3
  All notable changes to `@eng-ai/sdk` will be documented in this file.
4
4
 
5
+ ## 2.2.0 - 2026-04-05
6
+
7
+ ### Added
8
+
9
+ - New project management methods in JS SDK:
10
+ - `createProject(payload)` -> `POST /api/v2/projects/`
11
+ - `listProjects({ limit, offset })` -> `GET /api/v2/projects/`
12
+ - Enables external CLI/project onboarding flow with API key only.
13
+ - Structured error class `EngAIError` with metadata:
14
+ - `status`, `code`, `detail`, `requestId`, `retryAfter`, `idempotencyKey`, `method`, `url`
15
+ - Optional client diagnostics with secure redaction:
16
+ - constructor options: `{ debug, logger, timeoutMs }`
17
+ - debug logs redact sensitive keys (`api_key`, `authorization`, `token`, `secret`, `password`, `cookie`)
18
+
19
+ ### Changed
20
+
21
+ - HTTP handling hardened across all SDK calls:
22
+ - safe JSON/text response parsing
23
+ - timeout control with `AbortController`
24
+ - normalized HTTP/network/timeout errors via `EngAIError`
25
+ - Added client-side validation for required inputs:
26
+ - `invokeAgent`: requires non-empty `message`
27
+ - `createProject`: requires non-empty `name`
28
+
29
+ ## 2.1.0 - 2026-04-05
30
+
31
+ ### Added
32
+
33
+ - `invokeAgent` now supports autonomous fallback when `agent_id` is omitted:
34
+ - `client.invokeAgent({ message: "..." })` -> `POST /api/v2/sdk/invoke`
35
+ - Explicit specialist invocation remains supported:
36
+ - `client.invokeAgent("general", { ... })` -> `POST /api/v2/sdk/agents/{agent_id}/invoke`
37
+
5
38
  ## 2.0.2 - 2026-04-05
6
39
 
7
40
  ### Changed
package/README.md CHANGED
@@ -11,18 +11,66 @@ npm install @eng-ai/sdk
11
11
  ## Usage
12
12
 
13
13
  ```js
14
- import { EngAIClient } from "@eng-ai/sdk";
14
+ import { EngAIClient, EngAIError } from "@eng-ai/sdk";
15
15
 
16
- const client = new EngAIClient(process.env.ENG_AI_API_KEY, "https://your-domain/api/v2");
17
-
18
- const response = await client.invokeAgent("general", {
16
+ const client = new EngAIClient(
17
+ process.env.ENG_AI_API_KEY,
18
+ "https://your-domain/api/v2",
19
+ {
20
+ debug: false, // set true only in trusted environments
21
+ timeoutMs: 60000
22
+ }
23
+ );
24
+ const agentMode = (process.env.ENG_AI_AGENT_MODE || "autonomous").toLowerCase();
25
+ const specialistAgentId = process.env.ENG_AI_AGENT_ID || "general";
26
+ const payload = {
19
27
  message: "Explique o conceito de talude",
20
28
  normativeMode: true,
21
29
  normId: "auto"
22
- });
30
+ };
31
+
32
+ const response =
33
+ agentMode === "specialist"
34
+ ? await client.invokeAgent(specialistAgentId, payload)
35
+ : await client.invokeAgent(payload);
23
36
 
24
37
  console.log(response.result.content);
25
38
  console.log(response.normative?.citations ?? []);
39
+
40
+ // Recommended error handling
41
+ try {
42
+ await client.invokeAgent(payload);
43
+ } catch (error) {
44
+ if (error instanceof EngAIError) {
45
+ console.error("ENG-AI request failed", {
46
+ status: error.status,
47
+ code: error.code,
48
+ detail: error.detail,
49
+ requestId: error.requestId,
50
+ retryAfter: error.retryAfter
51
+ });
52
+ } else {
53
+ console.error("Unexpected error", error);
54
+ }
55
+ }
56
+ ```
57
+
58
+ Agent mode options:
59
+
60
+ - `ENG_AI_AGENT_MODE=autonomous`: omite `agent_id` e deixa o orquestrador escolher.
61
+ - `ENG_AI_AGENT_MODE=specialist`: fixa `agent_id` via `ENG_AI_AGENT_ID` (ex.: `general`, `contract_manager`).
62
+
63
+ Project creation (API key only):
64
+
65
+ ```js
66
+ const project = await client.createProject({
67
+ name: "Projeto Piloto",
68
+ description: "Projeto criado via SDK CLI",
69
+ projectLocation: "São Paulo - SP"
70
+ });
71
+
72
+ const projects = await client.listProjects({ limit: 10, offset: 0 });
73
+ console.log(project.id, projects.length);
26
74
  ```
27
75
 
28
76
  Default base URL: `https://api.eng-ai.com/api/v2`.
@@ -32,6 +80,7 @@ Default base URL: `https://api.eng-ai.com/api/v2`.
32
80
  - Use your ENG-AI API key only in trusted server environments.
33
81
  - Do not ship secrets in frontend bundles.
34
82
  - Keep idempotency enabled for mutation-heavy integrations.
83
+ - Keep `debug` disabled in production logs unless strictly needed.
35
84
 
36
85
  ## License
37
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eng-ai/sdk",
3
- "version": "2.0.2",
3
+ "version": "2.2.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,14 +1,287 @@
1
+ const DEFAULT_BASE_URL = "https://api.eng-ai.com/api/v2";
2
+ const DEFAULT_TIMEOUT_MS = 60000;
3
+ const REDACT_KEY_PATTERN = /api[-_]?key|authorization|token|secret|password|cookie|set-cookie/i;
4
+
5
+ function isPlainObject(value) {
6
+ return !!value && Object.prototype.toString.call(value) === "[object Object]";
7
+ }
8
+
9
+ function truncateString(value, max = 300) {
10
+ const text = String(value);
11
+ if (text.length <= max) return text;
12
+ return `${text.slice(0, max)}...(${text.length} chars)`;
13
+ }
14
+
15
+ function sanitizeForLogs(value, parentKey = "") {
16
+ if (value === null || value === undefined) return value;
17
+
18
+ if (REDACT_KEY_PATTERN.test(parentKey)) {
19
+ return "[REDACTED]";
20
+ }
21
+
22
+ if (Array.isArray(value)) {
23
+ return value.map((item) => sanitizeForLogs(item, parentKey));
24
+ }
25
+
26
+ if (isPlainObject(value)) {
27
+ const output = {};
28
+ for (const [key, nested] of Object.entries(value)) {
29
+ output[key] = sanitizeForLogs(nested, key);
30
+ }
31
+ return output;
32
+ }
33
+
34
+ if (typeof value === "string") {
35
+ return truncateString(value);
36
+ }
37
+
38
+ return value;
39
+ }
40
+
41
+ function describeBody(value) {
42
+ if (value === null || value === undefined) return null;
43
+ if (typeof value === "string") return { type: "string", length: value.length };
44
+ if (Array.isArray(value)) return { type: "array", length: value.length };
45
+ if (isPlainObject(value)) return { type: "object", keys: Object.keys(value) };
46
+ return { type: typeof value };
47
+ }
48
+
49
+ function normalizeErrorDetail(parsedBody, fallback = null) {
50
+ if (isPlainObject(parsedBody)) {
51
+ if (typeof parsedBody.detail === "string" && parsedBody.detail.trim()) {
52
+ return parsedBody.detail.trim();
53
+ }
54
+ if (
55
+ isPlainObject(parsedBody.error) &&
56
+ typeof parsedBody.error.message === "string" &&
57
+ parsedBody.error.message.trim()
58
+ ) {
59
+ return parsedBody.error.message.trim();
60
+ }
61
+ if (typeof parsedBody.message === "string" && parsedBody.message.trim()) {
62
+ return parsedBody.message.trim();
63
+ }
64
+ }
65
+ if (typeof parsedBody === "string" && parsedBody.trim()) return parsedBody.trim();
66
+ return fallback;
67
+ }
68
+
69
+ function extractErrorCode(parsedBody) {
70
+ if (isPlainObject(parsedBody)) {
71
+ if (isPlainObject(parsedBody.error) && parsedBody.error.code !== undefined) {
72
+ return parsedBody.error.code;
73
+ }
74
+ if (parsedBody.code !== undefined) return parsedBody.code;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function extractRequestId(parsedBody) {
80
+ if (isPlainObject(parsedBody) && isPlainObject(parsedBody.request)) {
81
+ return parsedBody.request.request_id || null;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ export class EngAIError extends Error {
87
+ constructor(message, metadata = {}, cause = undefined) {
88
+ super(message);
89
+ this.name = "EngAIError";
90
+ this.status = metadata.status ?? null;
91
+ this.code = metadata.code ?? null;
92
+ this.detail = metadata.detail ?? null;
93
+ this.requestId = metadata.requestId ?? null;
94
+ this.retryAfter = metadata.retryAfter ?? null;
95
+ this.idempotencyKey = metadata.idempotencyKey ?? null;
96
+ this.method = metadata.method ?? null;
97
+ this.url = metadata.url ?? null;
98
+
99
+ if (cause !== undefined) {
100
+ this.cause = cause;
101
+ }
102
+ }
103
+
104
+ toJSON() {
105
+ return {
106
+ name: this.name,
107
+ message: this.message,
108
+ status: this.status,
109
+ code: this.code,
110
+ detail: this.detail,
111
+ requestId: this.requestId,
112
+ retryAfter: this.retryAfter,
113
+ idempotencyKey: this.idempotencyKey,
114
+ method: this.method,
115
+ url: this.url,
116
+ };
117
+ }
118
+ }
119
+
1
120
  export class EngAIClient {
2
- constructor(apiKey, baseUrl = "https://api.eng-ai.com/api/v2") {
3
- this.apiKey = apiKey;
4
- this.baseUrl = String(baseUrl || "").replace(/\/+$/, "");
5
- this.headers = {
121
+ constructor(apiKey, baseUrl = DEFAULT_BASE_URL, options = {}) {
122
+ if (!String(apiKey || "").trim()) {
123
+ throw new TypeError("ENG-AI API key is required");
124
+ }
125
+
126
+ if (isPlainObject(baseUrl)) {
127
+ options = baseUrl;
128
+ baseUrl = DEFAULT_BASE_URL;
129
+ }
130
+
131
+ this.apiKey = String(apiKey).trim();
132
+ this.baseUrl = String(baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
133
+ this.debug = !!options?.debug;
134
+ this.timeoutMs = Math.max(1000, Number(options?.timeoutMs || DEFAULT_TIMEOUT_MS));
135
+ this.logger = options?.logger || console;
136
+ this.defaultHeaders = {
6
137
  "X-API-Key": this.apiKey,
7
138
  "Content-Type": "application/json",
8
139
  };
9
140
  }
10
141
 
11
- async invokeAgent(agentId, payload) {
142
+ _log(level, event, data = {}) {
143
+ if (!this.debug) return;
144
+ const sink =
145
+ this.logger && typeof this.logger[level] === "function"
146
+ ? this.logger[level].bind(this.logger)
147
+ : this.logger && typeof this.logger.log === "function"
148
+ ? this.logger.log.bind(this.logger)
149
+ : null;
150
+ if (!sink) return;
151
+ sink(`[eng-ai-sdk] ${event}`, sanitizeForLogs(data));
152
+ }
153
+
154
+ _buildUrl(pathname) {
155
+ const path = pathname.startsWith("/") ? pathname : `/${pathname}`;
156
+ return `${this.baseUrl}${path}`;
157
+ }
158
+
159
+ _buildHeaders(extra = {}) {
160
+ return { ...this.defaultHeaders, ...extra };
161
+ }
162
+
163
+ async _parseResponseBody(response) {
164
+ const contentType = String(response.headers.get("content-type") || "").toLowerCase();
165
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
166
+ try {
167
+ return await response.json();
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ const text = await response.text();
174
+ if (!text) return null;
175
+ try {
176
+ return JSON.parse(text);
177
+ } catch {
178
+ return text;
179
+ }
180
+ }
181
+
182
+ async _fetchWithTimeout(url, init) {
183
+ const controller = new AbortController();
184
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
185
+ try {
186
+ return await fetch(url, { ...init, signal: controller.signal });
187
+ } finally {
188
+ clearTimeout(timer);
189
+ }
190
+ }
191
+
192
+ _buildHttpError(response, parsedBody, method, url) {
193
+ const status = Number(response.status || 0);
194
+ const detail = normalizeErrorDetail(parsedBody, response.statusText || "Request failed");
195
+ const code = extractErrorCode(parsedBody);
196
+ const requestId = extractRequestId(parsedBody);
197
+ const retryAfter = response.headers.get("Retry-After");
198
+ const idempotencyKey = response.headers.get("Idempotency-Key");
199
+ const message = `ENG-AI request failed (${status})${detail ? `: ${detail}` : ""}`;
200
+ return new EngAIError(
201
+ message,
202
+ {
203
+ status,
204
+ code,
205
+ detail,
206
+ requestId,
207
+ retryAfter,
208
+ idempotencyKey,
209
+ method,
210
+ url,
211
+ }
212
+ );
213
+ }
214
+
215
+ async _request(pathname, { method = "GET", body = undefined, headers = {} } = {}) {
216
+ const url = this._buildUrl(pathname);
217
+ const requestHeaders = this._buildHeaders(headers);
218
+ const hasBody = body !== undefined;
219
+
220
+ this._log("info", "request.start", {
221
+ method,
222
+ url,
223
+ headers: requestHeaders,
224
+ body: describeBody(body),
225
+ timeout_ms: this.timeoutMs,
226
+ });
227
+
228
+ try {
229
+ const response = await this._fetchWithTimeout(url, {
230
+ method,
231
+ headers: requestHeaders,
232
+ body: hasBody ? JSON.stringify(body) : undefined,
233
+ });
234
+
235
+ const parsedBody = await this._parseResponseBody(response);
236
+
237
+ if (!response.ok) {
238
+ const err = this._buildHttpError(response, parsedBody, method, url);
239
+ this._log("warn", "request.error", err.toJSON());
240
+ throw err;
241
+ }
242
+
243
+ this._log("info", "request.success", {
244
+ method,
245
+ url,
246
+ status: response.status,
247
+ request_id: extractRequestId(parsedBody),
248
+ });
249
+
250
+ return parsedBody;
251
+ } catch (error) {
252
+ if (error instanceof EngAIError) throw error;
253
+
254
+ if (error?.name === "AbortError") {
255
+ const timeoutErr = new EngAIError(
256
+ `ENG-AI request timeout after ${this.timeoutMs}ms`,
257
+ { status: 408, code: "request_timeout", method, url, detail: "Request timeout" },
258
+ error
259
+ );
260
+ this._log("warn", "request.timeout", timeoutErr.toJSON());
261
+ throw timeoutErr;
262
+ }
263
+
264
+ const networkErr = new EngAIError(
265
+ "ENG-AI network error",
266
+ {
267
+ status: 0,
268
+ code: "network_error",
269
+ detail: error?.message || "Unknown network error",
270
+ method,
271
+ url,
272
+ },
273
+ error
274
+ );
275
+ this._log("warn", "request.network_error", networkErr.toJSON());
276
+ throw networkErr;
277
+ }
278
+ }
279
+
280
+ async invokeAgent(agentIdOrPayload, maybePayload) {
281
+ const implicitAutonomous = arguments.length === 1;
282
+ const agentId = implicitAutonomous ? null : agentIdOrPayload;
283
+ const payload = implicitAutonomous ? agentIdOrPayload : maybePayload;
284
+
12
285
  const body =
13
286
  typeof payload === "string"
14
287
  ? { message: payload }
@@ -25,22 +298,66 @@ export class EngAIClient {
25
298
  norm_id: payload?.normId ?? payload?.norm_id,
26
299
  };
27
300
 
28
- const headers = { ...this.headers };
301
+ if (!String(body?.message || "").trim()) {
302
+ throw new EngAIError("message is required for invokeAgent", {
303
+ status: 400,
304
+ code: "validation_error",
305
+ detail: "message must be a non-empty string",
306
+ });
307
+ }
308
+
309
+ const headers = {};
29
310
  const idempotencyKey = payload?.idempotencyKey ?? payload?.idempotency_key;
30
311
  if (idempotencyKey) {
31
312
  headers["Idempotency-Key"] = idempotencyKey;
32
313
  }
33
314
 
34
- const response = await fetch(`${this.baseUrl}/sdk/agents/${agentId}/invoke`, {
315
+ const normalizedAgentId = String(agentId || "").trim();
316
+ const endpoint = normalizedAgentId
317
+ ? `/sdk/agents/${encodeURIComponent(normalizedAgentId)}/invoke`
318
+ : "/sdk/invoke";
319
+
320
+ return await this._request(endpoint, {
35
321
  method: "POST",
36
322
  headers,
37
- body: JSON.stringify(body),
323
+ body,
38
324
  });
325
+ }
39
326
 
40
- if (!response.ok) {
41
- throw new Error(`Error: ${response.statusText}`);
327
+ async createProject(payload) {
328
+ if (!String(payload?.name || "").trim()) {
329
+ throw new EngAIError("name is required for createProject", {
330
+ status: 400,
331
+ code: "validation_error",
332
+ detail: "name must be a non-empty string",
333
+ });
42
334
  }
43
335
 
44
- return await response.json();
336
+ const body = {
337
+ name: payload?.name,
338
+ description: payload?.description,
339
+ work_type: payload?.workType ?? payload?.work_type,
340
+ contractual_start: payload?.contractualStart ?? payload?.contractual_start,
341
+ contractual_deadline: payload?.contractualDeadline ?? payload?.contractual_deadline,
342
+ project_location: payload?.projectLocation ?? payload?.project_location,
343
+ telegram_chat_id: payload?.telegramChatId ?? payload?.telegram_chat_id,
344
+ vision_cameras: payload?.visionCameras ?? payload?.vision_cameras,
345
+ };
346
+
347
+ return await this._request("/projects/", {
348
+ method: "POST",
349
+ body,
350
+ });
351
+ }
352
+
353
+ async listProjects(options = {}) {
354
+ const query = new URLSearchParams();
355
+ if (options.limit !== undefined) query.set("limit", String(options.limit));
356
+ if (options.offset !== undefined) query.set("offset", String(options.offset));
357
+ const suffix = query.toString() ? `?${query.toString()}` : "";
358
+
359
+ return await this._request(`/projects/${suffix}`, {
360
+ method: "GET",
361
+ });
45
362
  }
46
363
  }