@eng-ai/sdk 2.1.0 → 2.3.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 +43 -0
- package/README.md +92 -11
- package/bin/eng-ai.js +219 -0
- package/package.json +5 -1
- package/src/client.js +451 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@eng-ai/sdk` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 2.3.0 - 2026-04-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- External project autonomy support:
|
|
10
|
+
- `setProjectAutonomyMode(projectId, mode, { idempotencyKey? })`
|
|
11
|
+
- `getProjectContext(projectId)`
|
|
12
|
+
- External automation plan approval flow support:
|
|
13
|
+
- `listActionableAutomationPlans(limit?)`
|
|
14
|
+
- `listProjectAutomationPlans(projectId, { status? })`
|
|
15
|
+
- `approveAutomationPlan(projectId, planId, note?, { idempotencyKey? })`
|
|
16
|
+
- `rejectAutomationPlan(projectId, planId, note?, { idempotencyKey? })`
|
|
17
|
+
- `applyAutomationPlan(projectId, planId, { idempotencyKey? })`
|
|
18
|
+
- Official package CLI (`eng-ai`) with commands:
|
|
19
|
+
- `projects autonomy set`
|
|
20
|
+
- `plans actionable list`
|
|
21
|
+
- `plans approve|reject|apply`
|
|
22
|
+
- `plans watch --interval`
|
|
23
|
+
|
|
24
|
+
## 2.2.0 - 2026-04-05
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- New project management methods in JS SDK:
|
|
29
|
+
- `createProject(payload)` -> `POST /api/v2/projects/`
|
|
30
|
+
- `listProjects({ limit, offset })` -> `GET /api/v2/projects/`
|
|
31
|
+
- Enables external CLI/project onboarding flow with API key only.
|
|
32
|
+
- Structured error class `EngAIError` with metadata:
|
|
33
|
+
- `status`, `code`, `detail`, `requestId`, `retryAfter`, `idempotencyKey`, `method`, `url`
|
|
34
|
+
- Optional client diagnostics with secure redaction:
|
|
35
|
+
- constructor options: `{ debug, logger, timeoutMs }`
|
|
36
|
+
- debug logs redact sensitive keys (`api_key`, `authorization`, `token`, `secret`, `password`, `cookie`)
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- HTTP handling hardened across all SDK calls:
|
|
41
|
+
- safe JSON/text response parsing
|
|
42
|
+
- timeout control with `AbortController`
|
|
43
|
+
- normalized HTTP/network/timeout errors via `EngAIError`
|
|
44
|
+
- Added client-side validation for required inputs:
|
|
45
|
+
- `invokeAgent`: requires non-empty `message`
|
|
46
|
+
- `createProject`: requires non-empty `name`
|
|
47
|
+
|
|
5
48
|
## 2.1.0 - 2026-04-05
|
|
6
49
|
|
|
7
50
|
### Added
|
package/README.md
CHANGED
|
@@ -11,23 +11,103 @@ 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(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 = {
|
|
24
27
|
message: "Explique o conceito de talude",
|
|
25
28
|
normativeMode: true,
|
|
26
29
|
normId: "auto"
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const response =
|
|
33
|
+
agentMode === "specialist"
|
|
34
|
+
? await client.invokeAgent(specialistAgentId, payload)
|
|
35
|
+
: await client.invokeAgent(payload);
|
|
36
|
+
|
|
37
|
+
console.log(response.result.content);
|
|
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 Core Orchestrator escolher.
|
|
61
|
+
- `ENG_AI_AGENT_MODE=specialist`: fixa `agent_id` via `ENG_AI_AGENT_ID` (ex.: `general`, `contract_manager` ou alias `project_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"
|
|
27
70
|
});
|
|
28
71
|
|
|
29
|
-
|
|
30
|
-
console.log(
|
|
72
|
+
const projects = await client.listProjects({ limit: 10, offset: 0 });
|
|
73
|
+
console.log(project.id, projects.length);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Task autonomy mode by project:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
await client.setProjectAutonomyMode(project.id, "approval_required");
|
|
80
|
+
const context = await client.getProjectContext(project.id);
|
|
81
|
+
console.log(context.autonomous_tasks_mode);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
External approval flow (pull):
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const actionable = await client.listActionableAutomationPlans(30);
|
|
88
|
+
const pending = actionable.find((plan) => plan.status === "pending_approval");
|
|
89
|
+
if (pending) {
|
|
90
|
+
await client.approveAutomationPlan(pending.project_id, pending.id, "Approved by integration bot");
|
|
91
|
+
await client.applyAutomationPlan(pending.project_id, pending.id);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Supported automation plan actions:
|
|
96
|
+
|
|
97
|
+
- `listActionableAutomationPlans(limit?)`
|
|
98
|
+
- `listProjectAutomationPlans(projectId, { status? })`
|
|
99
|
+
- `approveAutomationPlan(projectId, planId, note?, { idempotencyKey? })`
|
|
100
|
+
- `rejectAutomationPlan(projectId, planId, note?, { idempotencyKey? })`
|
|
101
|
+
- `applyAutomationPlan(projectId, planId, { idempotencyKey? })`
|
|
102
|
+
|
|
103
|
+
CLI (`eng-ai`) included in package:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
ENG_AI_API_KEY=sk_xxx eng-ai projects autonomy set --project-id <id> --mode approval_required
|
|
107
|
+
ENG_AI_API_KEY=sk_xxx eng-ai plans actionable list --limit 30
|
|
108
|
+
ENG_AI_API_KEY=sk_xxx eng-ai plans approve --project-id <id> --plan-id <id> --note "Approved"
|
|
109
|
+
ENG_AI_API_KEY=sk_xxx eng-ai plans apply --project-id <id> --plan-id <id>
|
|
110
|
+
ENG_AI_API_KEY=sk_xxx eng-ai plans watch --interval 15 --limit 30
|
|
31
111
|
```
|
|
32
112
|
|
|
33
113
|
Default base URL: `https://api.eng-ai.com/api/v2`.
|
|
@@ -37,6 +117,7 @@ Default base URL: `https://api.eng-ai.com/api/v2`.
|
|
|
37
117
|
- Use your ENG-AI API key only in trusted server environments.
|
|
38
118
|
- Do not ship secrets in frontend bundles.
|
|
39
119
|
- Keep idempotency enabled for mutation-heavy integrations.
|
|
120
|
+
- Keep `debug` disabled in production logs unless strictly needed.
|
|
40
121
|
|
|
41
122
|
## License
|
|
42
123
|
|
package/bin/eng-ai.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { EngAIClient, EngAIError } from "../src/client.js";
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
const tokens = [];
|
|
7
|
+
const options = {};
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const item = String(argv[i] || "");
|
|
11
|
+
if (!item.startsWith("--")) {
|
|
12
|
+
tokens.push(item);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const key = item.slice(2);
|
|
17
|
+
const next = argv[i + 1];
|
|
18
|
+
if (next !== undefined && !String(next).startsWith("--")) {
|
|
19
|
+
options[key] = next;
|
|
20
|
+
i += 1;
|
|
21
|
+
} else {
|
|
22
|
+
options[key] = "true";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { tokens, options };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function envOrDefault(name, fallback = undefined) {
|
|
30
|
+
const value = process.env[name];
|
|
31
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
return String(value).trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toPositiveInt(value, fallback) {
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
40
|
+
return Math.floor(parsed);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function maskId(value) {
|
|
44
|
+
const text = String(value || "");
|
|
45
|
+
if (text.length <= 8) return text;
|
|
46
|
+
return `${text.slice(0, 4)}...${text.slice(-4)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printHelp() {
|
|
50
|
+
console.log(`
|
|
51
|
+
ENG-AI SDK CLI
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
eng-ai projects autonomy set --project-id <id> --mode <approval_required|approval_delete_only|full_autonomy> [--idempotency-key <key>]
|
|
55
|
+
eng-ai plans actionable list [--limit <n>]
|
|
56
|
+
eng-ai plans approve --project-id <id> --plan-id <id> [--note <text>] [--idempotency-key <key>]
|
|
57
|
+
eng-ai plans reject --project-id <id> --plan-id <id> [--note <text>] [--idempotency-key <key>]
|
|
58
|
+
eng-ai plans apply --project-id <id> --plan-id <id> [--idempotency-key <key>]
|
|
59
|
+
eng-ai plans watch [--interval <seconds>] [--limit <n>]
|
|
60
|
+
|
|
61
|
+
Environment:
|
|
62
|
+
ENG_AI_API_KEY (required)
|
|
63
|
+
ENG_AI_BASE_URL (optional, default: https://api.eng-ai.com/api/v2)
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildClient() {
|
|
68
|
+
const apiKey = envOrDefault("ENG_AI_API_KEY");
|
|
69
|
+
if (!apiKey) {
|
|
70
|
+
throw new Error("ENG_AI_API_KEY is required");
|
|
71
|
+
}
|
|
72
|
+
const baseUrl = envOrDefault("ENG_AI_BASE_URL", "https://api.eng-ai.com/api/v2");
|
|
73
|
+
return new EngAIClient(apiKey, baseUrl);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printError(error) {
|
|
77
|
+
if (error instanceof EngAIError) {
|
|
78
|
+
const payload = {
|
|
79
|
+
status: error.status,
|
|
80
|
+
code: error.code,
|
|
81
|
+
detail: error.detail,
|
|
82
|
+
requestId: error.requestId,
|
|
83
|
+
retryAfter: error.retryAfter,
|
|
84
|
+
idempotencyKey: error.idempotencyKey ? maskId(error.idempotencyKey) : null,
|
|
85
|
+
};
|
|
86
|
+
console.error("ENG-AI request failed");
|
|
87
|
+
console.error(JSON.stringify(payload, null, 2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.error(error?.message || String(error));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runProjectsCommand(client, subTokens, options) {
|
|
94
|
+
if (subTokens[0] !== "autonomy" || subTokens[1] !== "set") {
|
|
95
|
+
throw new Error("Unsupported projects command. Use: projects autonomy set");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const projectId = options["project-id"];
|
|
99
|
+
const mode = options.mode;
|
|
100
|
+
const idempotencyKey = options["idempotency-key"];
|
|
101
|
+
if (!projectId || !mode) {
|
|
102
|
+
throw new Error("Missing required options: --project-id and --mode");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const context = await client.setProjectAutonomyMode(projectId, mode, {
|
|
106
|
+
idempotencyKey,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log("Project autonomy mode updated.");
|
|
110
|
+
console.log(
|
|
111
|
+
JSON.stringify(
|
|
112
|
+
{
|
|
113
|
+
project_id: context.project_id,
|
|
114
|
+
autonomous_tasks_mode: context.autonomous_tasks_mode,
|
|
115
|
+
autonomous_tasks_enabled: context.autonomous_tasks_enabled,
|
|
116
|
+
},
|
|
117
|
+
null,
|
|
118
|
+
2
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printPlansSummary(plans) {
|
|
124
|
+
const rows = Array.isArray(plans) ? plans : [];
|
|
125
|
+
const formatted = rows.map((plan) => ({
|
|
126
|
+
plan_id: plan.id,
|
|
127
|
+
project_id: plan.project_id,
|
|
128
|
+
project_name: plan.project_name || null,
|
|
129
|
+
status: plan.status,
|
|
130
|
+
risk_level: plan.risk_level,
|
|
131
|
+
created_at: plan.created_at,
|
|
132
|
+
}));
|
|
133
|
+
console.log(JSON.stringify(formatted, null, 2));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function runPlansCommand(client, subTokens, options) {
|
|
137
|
+
const action = subTokens[0];
|
|
138
|
+
|
|
139
|
+
if (action === "actionable" && subTokens[1] === "list") {
|
|
140
|
+
const limit = toPositiveInt(options.limit, 30);
|
|
141
|
+
const plans = await client.listActionableAutomationPlans(limit);
|
|
142
|
+
printPlansSummary(plans);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (action === "watch") {
|
|
147
|
+
const intervalSeconds = toPositiveInt(options.interval, 15);
|
|
148
|
+
const limit = toPositiveInt(options.limit, 30);
|
|
149
|
+
console.log(
|
|
150
|
+
`Watching actionable plans every ${intervalSeconds}s (limit=${limit}). Press Ctrl+C to stop.`
|
|
151
|
+
);
|
|
152
|
+
while (true) {
|
|
153
|
+
const plans = await client.listActionableAutomationPlans(limit);
|
|
154
|
+
const pending = (plans || []).filter((item) => item?.status === "pending_approval");
|
|
155
|
+
const stamp = new Date().toISOString();
|
|
156
|
+
console.log(`\n[${stamp}] actionable=${(plans || []).length} pending=${pending.length}`);
|
|
157
|
+
if (pending.length > 0) {
|
|
158
|
+
printPlansSummary(pending);
|
|
159
|
+
}
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!["approve", "reject", "apply"].includes(action)) {
|
|
165
|
+
throw new Error("Unsupported plans command. Use actionable list|watch|approve|reject|apply");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const projectId = options["project-id"];
|
|
169
|
+
const planId = options["plan-id"];
|
|
170
|
+
const note = options.note;
|
|
171
|
+
const idempotencyKey = options["idempotency-key"];
|
|
172
|
+
if (!projectId || !planId) {
|
|
173
|
+
throw new Error("Missing required options: --project-id and --plan-id");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let result;
|
|
177
|
+
if (action === "approve") {
|
|
178
|
+
result = await client.approveAutomationPlan(projectId, planId, note, {
|
|
179
|
+
idempotencyKey,
|
|
180
|
+
});
|
|
181
|
+
} else if (action === "reject") {
|
|
182
|
+
result = await client.rejectAutomationPlan(projectId, planId, note, {
|
|
183
|
+
idempotencyKey,
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
result = await client.applyAutomationPlan(projectId, planId, {
|
|
187
|
+
idempotencyKey,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`Plan ${action} completed.`);
|
|
192
|
+
console.log(JSON.stringify(result, null, 2));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function main() {
|
|
196
|
+
const { tokens, options } = parseArgs(process.argv.slice(2));
|
|
197
|
+
const command = tokens[0];
|
|
198
|
+
if (!command || command === "help" || command === "--help") {
|
|
199
|
+
printHelp();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const client = buildClient();
|
|
204
|
+
if (command === "projects") {
|
|
205
|
+
await runProjectsCommand(client, tokens.slice(1), options);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (command === "plans") {
|
|
209
|
+
await runPlansCommand(client, tokens.slice(1), options);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
throw new Error(`Unknown command: ${command}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
main().catch((error) => {
|
|
217
|
+
printError(error);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eng-ai/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Official JavaScript SDK for ENG-AI External API v2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/client.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"eng-ai": "./bin/eng-ai.js"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./src/client.js"
|
|
9
12
|
},
|
|
10
13
|
"files": [
|
|
14
|
+
"bin",
|
|
11
15
|
"src",
|
|
12
16
|
"README.md",
|
|
13
17
|
"CHANGELOG.md",
|
package/src/client.js
CHANGED
|
@@ -1,13 +1,299 @@
|
|
|
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
|
+
const VALID_AUTONOMOUS_TASKS_MODES = new Set([
|
|
5
|
+
"approval_required",
|
|
6
|
+
"approval_delete_only",
|
|
7
|
+
"full_autonomy",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return !!value && Object.prototype.toString.call(value) === "[object Object]";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function truncateString(value, max = 300) {
|
|
15
|
+
const text = String(value);
|
|
16
|
+
if (text.length <= max) return text;
|
|
17
|
+
return `${text.slice(0, max)}...(${text.length} chars)`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sanitizeForLogs(value, parentKey = "") {
|
|
21
|
+
if (value === null || value === undefined) return value;
|
|
22
|
+
|
|
23
|
+
if (REDACT_KEY_PATTERN.test(parentKey)) {
|
|
24
|
+
return "[REDACTED]";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => sanitizeForLogs(item, parentKey));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isPlainObject(value)) {
|
|
32
|
+
const output = {};
|
|
33
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
34
|
+
output[key] = sanitizeForLogs(nested, key);
|
|
35
|
+
}
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
return truncateString(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function describeBody(value) {
|
|
47
|
+
if (value === null || value === undefined) return null;
|
|
48
|
+
if (typeof value === "string") return { type: "string", length: value.length };
|
|
49
|
+
if (Array.isArray(value)) return { type: "array", length: value.length };
|
|
50
|
+
if (isPlainObject(value)) return { type: "object", keys: Object.keys(value) };
|
|
51
|
+
return { type: typeof value };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeErrorDetail(parsedBody, fallback = null) {
|
|
55
|
+
if (isPlainObject(parsedBody)) {
|
|
56
|
+
if (typeof parsedBody.detail === "string" && parsedBody.detail.trim()) {
|
|
57
|
+
return parsedBody.detail.trim();
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
isPlainObject(parsedBody.error) &&
|
|
61
|
+
typeof parsedBody.error.message === "string" &&
|
|
62
|
+
parsedBody.error.message.trim()
|
|
63
|
+
) {
|
|
64
|
+
return parsedBody.error.message.trim();
|
|
65
|
+
}
|
|
66
|
+
if (typeof parsedBody.message === "string" && parsedBody.message.trim()) {
|
|
67
|
+
return parsedBody.message.trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (typeof parsedBody === "string" && parsedBody.trim()) return parsedBody.trim();
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractErrorCode(parsedBody) {
|
|
75
|
+
if (isPlainObject(parsedBody)) {
|
|
76
|
+
if (isPlainObject(parsedBody.error) && parsedBody.error.code !== undefined) {
|
|
77
|
+
return parsedBody.error.code;
|
|
78
|
+
}
|
|
79
|
+
if (parsedBody.code !== undefined) return parsedBody.code;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractRequestId(parsedBody) {
|
|
85
|
+
if (isPlainObject(parsedBody) && isPlainObject(parsedBody.request)) {
|
|
86
|
+
return parsedBody.request.request_id || null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class EngAIError extends Error {
|
|
92
|
+
constructor(message, metadata = {}, cause = undefined) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "EngAIError";
|
|
95
|
+
this.status = metadata.status ?? null;
|
|
96
|
+
this.code = metadata.code ?? null;
|
|
97
|
+
this.detail = metadata.detail ?? null;
|
|
98
|
+
this.requestId = metadata.requestId ?? null;
|
|
99
|
+
this.retryAfter = metadata.retryAfter ?? null;
|
|
100
|
+
this.idempotencyKey = metadata.idempotencyKey ?? null;
|
|
101
|
+
this.method = metadata.method ?? null;
|
|
102
|
+
this.url = metadata.url ?? null;
|
|
103
|
+
|
|
104
|
+
if (cause !== undefined) {
|
|
105
|
+
this.cause = cause;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
toJSON() {
|
|
110
|
+
return {
|
|
111
|
+
name: this.name,
|
|
112
|
+
message: this.message,
|
|
113
|
+
status: this.status,
|
|
114
|
+
code: this.code,
|
|
115
|
+
detail: this.detail,
|
|
116
|
+
requestId: this.requestId,
|
|
117
|
+
retryAfter: this.retryAfter,
|
|
118
|
+
idempotencyKey: this.idempotencyKey,
|
|
119
|
+
method: this.method,
|
|
120
|
+
url: this.url,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
1
125
|
export class EngAIClient {
|
|
2
|
-
constructor(apiKey, baseUrl =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
126
|
+
constructor(apiKey, baseUrl = DEFAULT_BASE_URL, options = {}) {
|
|
127
|
+
if (!String(apiKey || "").trim()) {
|
|
128
|
+
throw new TypeError("ENG-AI API key is required");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isPlainObject(baseUrl)) {
|
|
132
|
+
options = baseUrl;
|
|
133
|
+
baseUrl = DEFAULT_BASE_URL;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.apiKey = String(apiKey).trim();
|
|
137
|
+
this.baseUrl = String(baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
138
|
+
this.debug = !!options?.debug;
|
|
139
|
+
this.timeoutMs = Math.max(1000, Number(options?.timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
140
|
+
this.logger = options?.logger || console;
|
|
141
|
+
this.defaultHeaders = {
|
|
6
142
|
"X-API-Key": this.apiKey,
|
|
7
143
|
"Content-Type": "application/json",
|
|
8
144
|
};
|
|
9
145
|
}
|
|
10
146
|
|
|
147
|
+
_log(level, event, data = {}) {
|
|
148
|
+
if (!this.debug) return;
|
|
149
|
+
const sink =
|
|
150
|
+
this.logger && typeof this.logger[level] === "function"
|
|
151
|
+
? this.logger[level].bind(this.logger)
|
|
152
|
+
: this.logger && typeof this.logger.log === "function"
|
|
153
|
+
? this.logger.log.bind(this.logger)
|
|
154
|
+
: null;
|
|
155
|
+
if (!sink) return;
|
|
156
|
+
sink(`[eng-ai-sdk] ${event}`, sanitizeForLogs(data));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_buildUrl(pathname) {
|
|
160
|
+
const path = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
161
|
+
return `${this.baseUrl}${path}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_buildHeaders(extra = {}) {
|
|
165
|
+
return { ...this.defaultHeaders, ...extra };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async _parseResponseBody(response) {
|
|
169
|
+
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
|
|
170
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
171
|
+
try {
|
|
172
|
+
return await response.json();
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const text = await response.text();
|
|
179
|
+
if (!text) return null;
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(text);
|
|
182
|
+
} catch {
|
|
183
|
+
return text;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async _fetchWithTimeout(url, init) {
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
190
|
+
try {
|
|
191
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
192
|
+
} finally {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_buildHttpError(response, parsedBody, method, url) {
|
|
198
|
+
const status = Number(response.status || 0);
|
|
199
|
+
const detail = normalizeErrorDetail(parsedBody, response.statusText || "Request failed");
|
|
200
|
+
const code = extractErrorCode(parsedBody);
|
|
201
|
+
const requestId = extractRequestId(parsedBody);
|
|
202
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
203
|
+
const idempotencyKey = response.headers.get("Idempotency-Key");
|
|
204
|
+
const message = `ENG-AI request failed (${status})${detail ? `: ${detail}` : ""}`;
|
|
205
|
+
return new EngAIError(
|
|
206
|
+
message,
|
|
207
|
+
{
|
|
208
|
+
status,
|
|
209
|
+
code,
|
|
210
|
+
detail,
|
|
211
|
+
requestId,
|
|
212
|
+
retryAfter,
|
|
213
|
+
idempotencyKey,
|
|
214
|
+
method,
|
|
215
|
+
url,
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async _request(pathname, { method = "GET", body = undefined, headers = {} } = {}) {
|
|
221
|
+
const url = this._buildUrl(pathname);
|
|
222
|
+
const requestHeaders = this._buildHeaders(headers);
|
|
223
|
+
const hasBody = body !== undefined;
|
|
224
|
+
|
|
225
|
+
this._log("info", "request.start", {
|
|
226
|
+
method,
|
|
227
|
+
url,
|
|
228
|
+
headers: requestHeaders,
|
|
229
|
+
body: describeBody(body),
|
|
230
|
+
timeout_ms: this.timeoutMs,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const response = await this._fetchWithTimeout(url, {
|
|
235
|
+
method,
|
|
236
|
+
headers: requestHeaders,
|
|
237
|
+
body: hasBody ? JSON.stringify(body) : undefined,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const parsedBody = await this._parseResponseBody(response);
|
|
241
|
+
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
const err = this._buildHttpError(response, parsedBody, method, url);
|
|
244
|
+
this._log("warn", "request.error", err.toJSON());
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._log("info", "request.success", {
|
|
249
|
+
method,
|
|
250
|
+
url,
|
|
251
|
+
status: response.status,
|
|
252
|
+
request_id: extractRequestId(parsedBody),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return parsedBody;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof EngAIError) throw error;
|
|
258
|
+
|
|
259
|
+
if (error?.name === "AbortError") {
|
|
260
|
+
const timeoutErr = new EngAIError(
|
|
261
|
+
`ENG-AI request timeout after ${this.timeoutMs}ms`,
|
|
262
|
+
{ status: 408, code: "request_timeout", method, url, detail: "Request timeout" },
|
|
263
|
+
error
|
|
264
|
+
);
|
|
265
|
+
this._log("warn", "request.timeout", timeoutErr.toJSON());
|
|
266
|
+
throw timeoutErr;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const networkErr = new EngAIError(
|
|
270
|
+
"ENG-AI network error",
|
|
271
|
+
{
|
|
272
|
+
status: 0,
|
|
273
|
+
code: "network_error",
|
|
274
|
+
detail: error?.message || "Unknown network error",
|
|
275
|
+
method,
|
|
276
|
+
url,
|
|
277
|
+
},
|
|
278
|
+
error
|
|
279
|
+
);
|
|
280
|
+
this._log("warn", "request.network_error", networkErr.toJSON());
|
|
281
|
+
throw networkErr;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_requireNonEmptyString(value, fieldName) {
|
|
286
|
+
const normalized = String(value || "").trim();
|
|
287
|
+
if (!normalized) {
|
|
288
|
+
throw new EngAIError(`${fieldName} is required`, {
|
|
289
|
+
status: 400,
|
|
290
|
+
code: "validation_error",
|
|
291
|
+
detail: `${fieldName} must be a non-empty string`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
|
|
11
297
|
async invokeAgent(agentIdOrPayload, maybePayload) {
|
|
12
298
|
const implicitAutonomous = arguments.length === 1;
|
|
13
299
|
const agentId = implicitAutonomous ? null : agentIdOrPayload;
|
|
@@ -29,7 +315,15 @@ export class EngAIClient {
|
|
|
29
315
|
norm_id: payload?.normId ?? payload?.norm_id,
|
|
30
316
|
};
|
|
31
317
|
|
|
32
|
-
|
|
318
|
+
if (!String(body?.message || "").trim()) {
|
|
319
|
+
throw new EngAIError("message is required for invokeAgent", {
|
|
320
|
+
status: 400,
|
|
321
|
+
code: "validation_error",
|
|
322
|
+
detail: "message must be a non-empty string",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const headers = {};
|
|
33
327
|
const idempotencyKey = payload?.idempotencyKey ?? payload?.idempotency_key;
|
|
34
328
|
if (idempotencyKey) {
|
|
35
329
|
headers["Idempotency-Key"] = idempotencyKey;
|
|
@@ -37,19 +331,164 @@ export class EngAIClient {
|
|
|
37
331
|
|
|
38
332
|
const normalizedAgentId = String(agentId || "").trim();
|
|
39
333
|
const endpoint = normalizedAgentId
|
|
40
|
-
?
|
|
41
|
-
:
|
|
334
|
+
? `/sdk/agents/${encodeURIComponent(normalizedAgentId)}/invoke`
|
|
335
|
+
: "/sdk/invoke";
|
|
42
336
|
|
|
43
|
-
|
|
337
|
+
return await this._request(endpoint, {
|
|
44
338
|
method: "POST",
|
|
45
339
|
headers,
|
|
46
|
-
body
|
|
340
|
+
body,
|
|
47
341
|
});
|
|
342
|
+
}
|
|
48
343
|
|
|
49
|
-
|
|
50
|
-
|
|
344
|
+
async createProject(payload) {
|
|
345
|
+
if (!String(payload?.name || "").trim()) {
|
|
346
|
+
throw new EngAIError("name is required for createProject", {
|
|
347
|
+
status: 400,
|
|
348
|
+
code: "validation_error",
|
|
349
|
+
detail: "name must be a non-empty string",
|
|
350
|
+
});
|
|
51
351
|
}
|
|
52
352
|
|
|
53
|
-
|
|
353
|
+
const body = {
|
|
354
|
+
name: payload?.name,
|
|
355
|
+
description: payload?.description,
|
|
356
|
+
work_type: payload?.workType ?? payload?.work_type,
|
|
357
|
+
contractual_start: payload?.contractualStart ?? payload?.contractual_start,
|
|
358
|
+
contractual_deadline: payload?.contractualDeadline ?? payload?.contractual_deadline,
|
|
359
|
+
project_location: payload?.projectLocation ?? payload?.project_location,
|
|
360
|
+
telegram_chat_id: payload?.telegramChatId ?? payload?.telegram_chat_id,
|
|
361
|
+
vision_cameras: payload?.visionCameras ?? payload?.vision_cameras,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return await this._request("/projects/", {
|
|
365
|
+
method: "POST",
|
|
366
|
+
body,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async listProjects(options = {}) {
|
|
371
|
+
const query = new URLSearchParams();
|
|
372
|
+
if (options.limit !== undefined) query.set("limit", String(options.limit));
|
|
373
|
+
if (options.offset !== undefined) query.set("offset", String(options.offset));
|
|
374
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
375
|
+
|
|
376
|
+
return await this._request(`/projects/${suffix}`, {
|
|
377
|
+
method: "GET",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async getProjectContext(projectId) {
|
|
382
|
+
const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
|
|
383
|
+
return await this._request(`/projects/${encodeURIComponent(normalizedProjectId)}/context`, {
|
|
384
|
+
method: "GET",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async setProjectAutonomyMode(projectId, mode, options = {}) {
|
|
389
|
+
const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
|
|
390
|
+
const normalizedMode = String(mode || "").trim().toLowerCase();
|
|
391
|
+
if (!VALID_AUTONOMOUS_TASKS_MODES.has(normalizedMode)) {
|
|
392
|
+
throw new EngAIError("invalid mode for setProjectAutonomyMode", {
|
|
393
|
+
status: 400,
|
|
394
|
+
code: "validation_error",
|
|
395
|
+
detail:
|
|
396
|
+
"mode must be one of: approval_required, approval_delete_only, full_autonomy",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const headers = {};
|
|
401
|
+
const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
|
|
402
|
+
if (idempotencyKey) {
|
|
403
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return await this._request(
|
|
407
|
+
`/projects/${encodeURIComponent(normalizedProjectId)}/autonomous-tasks-mode`,
|
|
408
|
+
{
|
|
409
|
+
method: "PUT",
|
|
410
|
+
headers,
|
|
411
|
+
body: { mode: normalizedMode },
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async listActionableAutomationPlans(limit = 30) {
|
|
417
|
+
const query = new URLSearchParams();
|
|
418
|
+
if (limit !== undefined && limit !== null) {
|
|
419
|
+
query.set("limit", String(limit));
|
|
420
|
+
}
|
|
421
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
422
|
+
return await this._request(`/projects/automation-plans/actionable${suffix}`, {
|
|
423
|
+
method: "GET",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async listProjectAutomationPlans(projectId, options = {}) {
|
|
428
|
+
const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
|
|
429
|
+
const query = new URLSearchParams();
|
|
430
|
+
if (options.status) {
|
|
431
|
+
query.set("status", String(options.status));
|
|
432
|
+
}
|
|
433
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
434
|
+
return await this._request(
|
|
435
|
+
`/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans${suffix}`,
|
|
436
|
+
{
|
|
437
|
+
method: "GET",
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async approveAutomationPlan(projectId, planId, note = undefined, options = {}) {
|
|
443
|
+
const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
|
|
444
|
+
const normalizedPlanId = this._requireNonEmptyString(planId, "planId");
|
|
445
|
+
const headers = {};
|
|
446
|
+
const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
|
|
447
|
+
if (idempotencyKey) {
|
|
448
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
449
|
+
}
|
|
450
|
+
return await this._request(
|
|
451
|
+
`/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans/${encodeURIComponent(normalizedPlanId)}/approve`,
|
|
452
|
+
{
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers,
|
|
455
|
+
body: { note: note ?? undefined },
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async rejectAutomationPlan(projectId, planId, note = undefined, options = {}) {
|
|
461
|
+
const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
|
|
462
|
+
const normalizedPlanId = this._requireNonEmptyString(planId, "planId");
|
|
463
|
+
const headers = {};
|
|
464
|
+
const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
|
|
465
|
+
if (idempotencyKey) {
|
|
466
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
467
|
+
}
|
|
468
|
+
return await this._request(
|
|
469
|
+
`/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans/${encodeURIComponent(normalizedPlanId)}/reject`,
|
|
470
|
+
{
|
|
471
|
+
method: "POST",
|
|
472
|
+
headers,
|
|
473
|
+
body: { note: note ?? undefined },
|
|
474
|
+
}
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async applyAutomationPlan(projectId, planId, options = {}) {
|
|
479
|
+
const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
|
|
480
|
+
const normalizedPlanId = this._requireNonEmptyString(planId, "planId");
|
|
481
|
+
const headers = {};
|
|
482
|
+
const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
|
|
483
|
+
if (idempotencyKey) {
|
|
484
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
485
|
+
}
|
|
486
|
+
return await this._request(
|
|
487
|
+
`/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans/${encodeURIComponent(normalizedPlanId)}/apply`,
|
|
488
|
+
{
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers,
|
|
491
|
+
}
|
|
492
|
+
);
|
|
54
493
|
}
|
|
55
494
|
}
|