@eng-ai/sdk 2.2.0 → 2.5.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,56 @@
2
2
 
3
3
  All notable changes to `@eng-ai/sdk` will be documented in this file.
4
4
 
5
+ ## 2.5.0 - 2026-04-05
6
+
7
+ ### Added
8
+
9
+ - External profile support (API key owner scope):
10
+ - `getProfile()`
11
+ - `updateProfile({ preferredName?, honorific?, crea?, specialty?, avatarUrl? })`
12
+ - CLI `eng-ai` profile commands:
13
+ - `profile get`
14
+ - `profile set --preferred-name ... --honorific ... --crea ... --specialty ... --avatar-url ...`
15
+
16
+ ### Changed
17
+
18
+ - 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.
19
+
20
+ ## 2.4.0 - 2026-04-05
21
+
22
+ ### Added
23
+
24
+ - External user settings support (API key only):
25
+ - `getAISettings()`
26
+ - `setAISettings({ provider, model, projectModel?, apiKey?, googleApiKeyRag?, preferredTimezone? })`
27
+ - `clearAISettings()`
28
+ - `setVoiceLiveEnabled(enabled)`
29
+ - `getGeneralSettings()`
30
+ - `setGeneralSettings(preferredTimezone?)`
31
+ - CLI `eng-ai` commands for external settings:
32
+ - `settings ai get|set|clear`
33
+ - `settings ai voice-live --enabled true|false`
34
+ - `settings general get|set`
35
+
36
+ ## 2.3.0 - 2026-04-05
37
+
38
+ ### Added
39
+
40
+ - External project autonomy support:
41
+ - `setProjectAutonomyMode(projectId, mode, { idempotencyKey? })`
42
+ - `getProjectContext(projectId)`
43
+ - External automation plan approval flow support:
44
+ - `listActionableAutomationPlans(limit?)`
45
+ - `listProjectAutomationPlans(projectId, { status? })`
46
+ - `approveAutomationPlan(projectId, planId, note?, { idempotencyKey? })`
47
+ - `rejectAutomationPlan(projectId, planId, note?, { idempotencyKey? })`
48
+ - `applyAutomationPlan(projectId, planId, { idempotencyKey? })`
49
+ - Official package CLI (`eng-ai`) with commands:
50
+ - `projects autonomy set`
51
+ - `plans actionable list`
52
+ - `plans approve|reject|apply`
53
+ - `plans watch --interval`
54
+
5
55
  ## 2.2.0 - 2026-04-05
6
56
 
7
57
  ### Added
package/README.md CHANGED
@@ -57,8 +57,8 @@ try {
57
57
 
58
58
  Agent mode options:
59
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`).
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
62
 
63
63
  Project creation (API key only):
64
64
 
@@ -73,6 +73,76 @@ const projects = await client.listProjects({ limit: 10, offset: 0 });
73
73
  console.log(project.id, projects.length);
74
74
  ```
75
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
+ External user settings (API key owner scope):
104
+
105
+ ```js
106
+ const ai = await client.getAISettings();
107
+ await client.setAISettings({
108
+ provider: "google",
109
+ model: "gemini-3.1-flash-lite-preview",
110
+ projectModel: "gemini-3.1-flash-lite-preview",
111
+ preferredTimezone: "America/Sao_Paulo",
112
+ });
113
+ await client.setVoiceLiveEnabled(true);
114
+ const general = await client.getGeneralSettings();
115
+ await client.setGeneralSettings(general.preferred_timezone || "America/Sao_Paulo");
116
+ ```
117
+
118
+ External profile (API key owner scope):
119
+
120
+ ```js
121
+ const profile = await client.getProfile();
122
+ await client.updateProfile({
123
+ preferredName: "Felipe",
124
+ honorific: "Eng.",
125
+ crea: "123456/D",
126
+ specialty: "Geotecnia",
127
+ });
128
+ ```
129
+
130
+ CLI (`eng-ai`) included in package:
131
+
132
+ ```bash
133
+ ENG_AI_API_KEY=sk_xxx eng-ai projects autonomy set --project-id <id> --mode approval_required
134
+ ENG_AI_API_KEY=sk_xxx eng-ai plans actionable list --limit 30
135
+ ENG_AI_API_KEY=sk_xxx eng-ai plans approve --project-id <id> --plan-id <id> --note "Approved"
136
+ ENG_AI_API_KEY=sk_xxx eng-ai plans apply --project-id <id> --plan-id <id>
137
+ ENG_AI_API_KEY=sk_xxx eng-ai plans watch --interval 15 --limit 30
138
+ ENG_AI_API_KEY=sk_xxx eng-ai settings ai get
139
+ 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
140
+ ENG_AI_API_KEY=sk_xxx eng-ai settings ai voice-live --enabled true
141
+ ENG_AI_API_KEY=sk_xxx eng-ai settings general set --preferred-timezone America/Sao_Paulo
142
+ ENG_AI_API_KEY=sk_xxx eng-ai profile get
143
+ ENG_AI_API_KEY=sk_xxx eng-ai profile set --preferred-name Felipe --honorific "Eng." --crea "123456/D" --specialty Geotecnia
144
+ ```
145
+
76
146
  Default base URL: `https://api.eng-ai.com/api/v2`.
77
147
 
78
148
  ## Security Notes
package/bin/eng-ai.js ADDED
@@ -0,0 +1,346 @@
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 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
+
51
+ function maskId(value) {
52
+ const text = String(value || "");
53
+ if (text.length <= 8) return text;
54
+ return `${text.slice(0, 4)}...${text.slice(-4)}`;
55
+ }
56
+
57
+ function printHelp() {
58
+ console.log(`
59
+ ENG-AI SDK CLI
60
+
61
+ Usage:
62
+ eng-ai projects autonomy set --project-id <id> --mode <approval_required|approval_delete_only|full_autonomy> [--idempotency-key <key>]
63
+ eng-ai plans actionable list [--limit <n>]
64
+ eng-ai plans approve --project-id <id> --plan-id <id> [--note <text>] [--idempotency-key <key>]
65
+ eng-ai plans reject --project-id <id> --plan-id <id> [--note <text>] [--idempotency-key <key>]
66
+ eng-ai plans apply --project-id <id> --plan-id <id> [--idempotency-key <key>]
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> --model <name> [--project-model <name>] [--api-key <key>] [--google-api-key-rag <key>] [--preferred-timezone <IANA_TZ>]
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>]
76
+
77
+ Environment:
78
+ ENG_AI_API_KEY (required)
79
+ ENG_AI_BASE_URL (optional, default: https://api.eng-ai.com/api/v2)
80
+ `);
81
+ }
82
+
83
+ function buildClient() {
84
+ const apiKey = envOrDefault("ENG_AI_API_KEY");
85
+ if (!apiKey) {
86
+ throw new Error("ENG_AI_API_KEY is required");
87
+ }
88
+ const baseUrl = envOrDefault("ENG_AI_BASE_URL", "https://api.eng-ai.com/api/v2");
89
+ return new EngAIClient(apiKey, baseUrl);
90
+ }
91
+
92
+ function printError(error) {
93
+ if (error instanceof EngAIError) {
94
+ const payload = {
95
+ status: error.status,
96
+ code: error.code,
97
+ detail: error.detail,
98
+ requestId: error.requestId,
99
+ retryAfter: error.retryAfter,
100
+ idempotencyKey: error.idempotencyKey ? maskId(error.idempotencyKey) : null,
101
+ };
102
+ console.error("ENG-AI request failed");
103
+ console.error(JSON.stringify(payload, null, 2));
104
+ return;
105
+ }
106
+ console.error(error?.message || String(error));
107
+ }
108
+
109
+ async function runProjectsCommand(client, subTokens, options) {
110
+ if (subTokens[0] !== "autonomy" || subTokens[1] !== "set") {
111
+ throw new Error("Unsupported projects command. Use: projects autonomy set");
112
+ }
113
+
114
+ const projectId = options["project-id"];
115
+ const mode = options.mode;
116
+ const idempotencyKey = options["idempotency-key"];
117
+ if (!projectId || !mode) {
118
+ throw new Error("Missing required options: --project-id and --mode");
119
+ }
120
+
121
+ const context = await client.setProjectAutonomyMode(projectId, mode, {
122
+ idempotencyKey,
123
+ });
124
+
125
+ console.log("Project autonomy mode updated.");
126
+ console.log(
127
+ JSON.stringify(
128
+ {
129
+ project_id: context.project_id,
130
+ autonomous_tasks_mode: context.autonomous_tasks_mode,
131
+ autonomous_tasks_enabled: context.autonomous_tasks_enabled,
132
+ },
133
+ null,
134
+ 2
135
+ )
136
+ );
137
+ }
138
+
139
+ function printPlansSummary(plans) {
140
+ const rows = Array.isArray(plans) ? plans : [];
141
+ const formatted = rows.map((plan) => ({
142
+ plan_id: plan.id,
143
+ project_id: plan.project_id,
144
+ project_name: plan.project_name || null,
145
+ status: plan.status,
146
+ risk_level: plan.risk_level,
147
+ created_at: plan.created_at,
148
+ }));
149
+ console.log(JSON.stringify(formatted, null, 2));
150
+ }
151
+
152
+ async function runPlansCommand(client, subTokens, options) {
153
+ const action = subTokens[0];
154
+
155
+ if (action === "actionable" && subTokens[1] === "list") {
156
+ const limit = toPositiveInt(options.limit, 30);
157
+ const plans = await client.listActionableAutomationPlans(limit);
158
+ printPlansSummary(plans);
159
+ return;
160
+ }
161
+
162
+ if (action === "watch") {
163
+ const intervalSeconds = toPositiveInt(options.interval, 15);
164
+ const limit = toPositiveInt(options.limit, 30);
165
+ console.log(
166
+ `Watching actionable plans every ${intervalSeconds}s (limit=${limit}). Press Ctrl+C to stop.`
167
+ );
168
+ while (true) {
169
+ const plans = await client.listActionableAutomationPlans(limit);
170
+ const pending = (plans || []).filter((item) => item?.status === "pending_approval");
171
+ const stamp = new Date().toISOString();
172
+ console.log(`\n[${stamp}] actionable=${(plans || []).length} pending=${pending.length}`);
173
+ if (pending.length > 0) {
174
+ printPlansSummary(pending);
175
+ }
176
+ await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
177
+ }
178
+ }
179
+
180
+ if (!["approve", "reject", "apply"].includes(action)) {
181
+ throw new Error("Unsupported plans command. Use actionable list|watch|approve|reject|apply");
182
+ }
183
+
184
+ const projectId = options["project-id"];
185
+ const planId = options["plan-id"];
186
+ const note = options.note;
187
+ const idempotencyKey = options["idempotency-key"];
188
+ if (!projectId || !planId) {
189
+ throw new Error("Missing required options: --project-id and --plan-id");
190
+ }
191
+
192
+ let result;
193
+ if (action === "approve") {
194
+ result = await client.approveAutomationPlan(projectId, planId, note, {
195
+ idempotencyKey,
196
+ });
197
+ } else if (action === "reject") {
198
+ result = await client.rejectAutomationPlan(projectId, planId, note, {
199
+ idempotencyKey,
200
+ });
201
+ } else {
202
+ result = await client.applyAutomationPlan(projectId, planId, {
203
+ idempotencyKey,
204
+ });
205
+ }
206
+
207
+ console.log(`Plan ${action} completed.`);
208
+ console.log(JSON.stringify(result, null, 2));
209
+ }
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
+ });
248
+ console.log("AI settings updated.");
249
+ console.log(JSON.stringify(result, null, 2));
250
+ return;
251
+ }
252
+
253
+ if (action === "voice-live") {
254
+ const enabled = toBoolean(options.enabled, undefined);
255
+ if (enabled === undefined) {
256
+ throw new Error("Missing or invalid --enabled. Use --enabled true|false");
257
+ }
258
+ const result = await client.setVoiceLiveEnabled(enabled);
259
+ console.log("Voice-live toggle updated.");
260
+ console.log(JSON.stringify(result, null, 2));
261
+ return;
262
+ }
263
+ }
264
+
265
+ if (section === "general") {
266
+ if (action === "get") {
267
+ const result = await client.getGeneralSettings();
268
+ console.log(JSON.stringify(result, null, 2));
269
+ return;
270
+ }
271
+
272
+ if (action === "set") {
273
+ const result = await client.setGeneralSettings(options["preferred-timezone"] || null);
274
+ console.log("General settings updated.");
275
+ console.log(JSON.stringify(result, null, 2));
276
+ return;
277
+ }
278
+ }
279
+
280
+ throw new Error("Unsupported settings command. Use ai|get/set/clear/voice-live or general|get/set");
281
+ }
282
+
283
+ async function runProfileCommand(client, subTokens, options) {
284
+ const action = String(subTokens[0] || "").trim().toLowerCase();
285
+ if (action === "get") {
286
+ const result = await client.getProfile();
287
+ console.log(JSON.stringify(result, null, 2));
288
+ return;
289
+ }
290
+
291
+ if (action === "set") {
292
+ const payload = {
293
+ preferredName: options["preferred-name"],
294
+ honorific: options.honorific,
295
+ crea: options.crea,
296
+ specialty: options.specialty,
297
+ avatarUrl: options["avatar-url"],
298
+ };
299
+ const hasAnyField = Object.values(payload).some((value) => value !== undefined);
300
+ if (!hasAnyField) {
301
+ throw new Error(
302
+ "Missing profile fields. Use at least one: --preferred-name, --honorific, --crea, --specialty, --avatar-url"
303
+ );
304
+ }
305
+ const result = await client.updateProfile(payload);
306
+ console.log("Profile updated.");
307
+ console.log(JSON.stringify(result, null, 2));
308
+ return;
309
+ }
310
+
311
+ throw new Error("Unsupported profile command. Use: profile get|set");
312
+ }
313
+
314
+ async function main() {
315
+ const { tokens, options } = parseArgs(process.argv.slice(2));
316
+ const command = tokens[0];
317
+ if (!command || command === "help" || command === "--help") {
318
+ printHelp();
319
+ return;
320
+ }
321
+
322
+ const client = buildClient();
323
+ if (command === "projects") {
324
+ await runProjectsCommand(client, tokens.slice(1), options);
325
+ return;
326
+ }
327
+ if (command === "plans") {
328
+ await runPlansCommand(client, tokens.slice(1), options);
329
+ return;
330
+ }
331
+ if (command === "settings") {
332
+ await runSettingsCommand(client, tokens.slice(1), options);
333
+ return;
334
+ }
335
+ if (command === "profile") {
336
+ await runProfileCommand(client, tokens.slice(1), options);
337
+ return;
338
+ }
339
+
340
+ throw new Error(`Unknown command: ${command}`);
341
+ }
342
+
343
+ main().catch((error) => {
344
+ printError(error);
345
+ process.exit(1);
346
+ });
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@eng-ai/sdk",
3
- "version": "2.2.0",
3
+ "version": "2.5.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,6 +1,11 @@
1
1
  const DEFAULT_BASE_URL = "https://api.eng-ai.com/api/v2";
2
2
  const DEFAULT_TIMEOUT_MS = 60000;
3
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
+ ]);
4
9
 
5
10
  function isPlainObject(value) {
6
11
  return !!value && Object.prototype.toString.call(value) === "[object Object]";
@@ -277,6 +282,18 @@ export class EngAIClient {
277
282
  }
278
283
  }
279
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
+
280
297
  async invokeAgent(agentIdOrPayload, maybePayload) {
281
298
  const implicitAutonomous = arguments.length === 1;
282
299
  const agentId = implicitAutonomous ? null : agentIdOrPayload;
@@ -360,4 +377,201 @@ export class EngAIClient {
360
377
  method: "GET",
361
378
  });
362
379
  }
380
+
381
+ async getAISettings() {
382
+ return await this._request("/settings/llm", {
383
+ method: "GET",
384
+ });
385
+ }
386
+
387
+ async setAISettings(payload) {
388
+ const provider = this._requireNonEmptyString(payload?.provider, "provider").toLowerCase();
389
+ const model = this._requireNonEmptyString(payload?.model, "model");
390
+ const body = {
391
+ provider,
392
+ model,
393
+ project_model: payload?.projectModel ?? payload?.project_model,
394
+ api_key: payload?.apiKey ?? payload?.api_key,
395
+ google_api_key_rag: payload?.googleApiKeyRag ?? payload?.google_api_key_rag,
396
+ preferred_timezone: payload?.preferredTimezone ?? payload?.preferred_timezone,
397
+ };
398
+
399
+ return await this._request("/settings/llm", {
400
+ method: "PUT",
401
+ body,
402
+ });
403
+ }
404
+
405
+ async clearAISettings() {
406
+ return await this._request("/settings/llm", {
407
+ method: "DELETE",
408
+ });
409
+ }
410
+
411
+ async setVoiceLiveEnabled(enabled) {
412
+ return await this._request("/settings/voice-live", {
413
+ method: "PUT",
414
+ body: { enabled: !!enabled },
415
+ });
416
+ }
417
+
418
+ async getGeneralSettings() {
419
+ return await this._request("/settings/general", {
420
+ method: "GET",
421
+ });
422
+ }
423
+
424
+ async setGeneralSettings(preferredTimezone = null) {
425
+ return await this._request("/settings/general", {
426
+ method: "PUT",
427
+ body: {
428
+ preferred_timezone: preferredTimezone || null,
429
+ },
430
+ });
431
+ }
432
+
433
+ async getProfile() {
434
+ return await this._request("/profile", {
435
+ method: "GET",
436
+ });
437
+ }
438
+
439
+ async updateProfile(payload = {}) {
440
+ const body = {
441
+ preferred_name: payload?.preferredName ?? payload?.preferred_name,
442
+ honorific: payload?.honorific,
443
+ crea: payload?.crea ?? payload?.crea_cau,
444
+ specialty: payload?.specialty,
445
+ avatar_url: payload?.avatarUrl ?? payload?.avatar_url,
446
+ };
447
+
448
+ const hasAnyField = Object.values(body).some((value) => value !== undefined);
449
+ if (!hasAnyField) {
450
+ throw new EngAIError("at least one profile field is required for updateProfile", {
451
+ status: 400,
452
+ code: "validation_error",
453
+ detail:
454
+ "provide one of: preferred_name, honorific, crea, specialty, avatar_url",
455
+ });
456
+ }
457
+
458
+ return await this._request("/profile", {
459
+ method: "PUT",
460
+ body,
461
+ });
462
+ }
463
+
464
+ async getProjectContext(projectId) {
465
+ const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
466
+ return await this._request(`/projects/${encodeURIComponent(normalizedProjectId)}/context`, {
467
+ method: "GET",
468
+ });
469
+ }
470
+
471
+ async setProjectAutonomyMode(projectId, mode, options = {}) {
472
+ const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
473
+ const normalizedMode = String(mode || "").trim().toLowerCase();
474
+ if (!VALID_AUTONOMOUS_TASKS_MODES.has(normalizedMode)) {
475
+ throw new EngAIError("invalid mode for setProjectAutonomyMode", {
476
+ status: 400,
477
+ code: "validation_error",
478
+ detail:
479
+ "mode must be one of: approval_required, approval_delete_only, full_autonomy",
480
+ });
481
+ }
482
+
483
+ const headers = {};
484
+ const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
485
+ if (idempotencyKey) {
486
+ headers["Idempotency-Key"] = idempotencyKey;
487
+ }
488
+
489
+ return await this._request(
490
+ `/projects/${encodeURIComponent(normalizedProjectId)}/autonomous-tasks-mode`,
491
+ {
492
+ method: "PUT",
493
+ headers,
494
+ body: { mode: normalizedMode },
495
+ }
496
+ );
497
+ }
498
+
499
+ async listActionableAutomationPlans(limit = 30) {
500
+ const query = new URLSearchParams();
501
+ if (limit !== undefined && limit !== null) {
502
+ query.set("limit", String(limit));
503
+ }
504
+ const suffix = query.toString() ? `?${query.toString()}` : "";
505
+ return await this._request(`/projects/automation-plans/actionable${suffix}`, {
506
+ method: "GET",
507
+ });
508
+ }
509
+
510
+ async listProjectAutomationPlans(projectId, options = {}) {
511
+ const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
512
+ const query = new URLSearchParams();
513
+ if (options.status) {
514
+ query.set("status", String(options.status));
515
+ }
516
+ const suffix = query.toString() ? `?${query.toString()}` : "";
517
+ return await this._request(
518
+ `/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans${suffix}`,
519
+ {
520
+ method: "GET",
521
+ }
522
+ );
523
+ }
524
+
525
+ async approveAutomationPlan(projectId, planId, note = undefined, options = {}) {
526
+ const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
527
+ const normalizedPlanId = this._requireNonEmptyString(planId, "planId");
528
+ const headers = {};
529
+ const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
530
+ if (idempotencyKey) {
531
+ headers["Idempotency-Key"] = idempotencyKey;
532
+ }
533
+ return await this._request(
534
+ `/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans/${encodeURIComponent(normalizedPlanId)}/approve`,
535
+ {
536
+ method: "POST",
537
+ headers,
538
+ body: { note: note ?? undefined },
539
+ }
540
+ );
541
+ }
542
+
543
+ async rejectAutomationPlan(projectId, planId, note = undefined, options = {}) {
544
+ const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
545
+ const normalizedPlanId = this._requireNonEmptyString(planId, "planId");
546
+ const headers = {};
547
+ const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
548
+ if (idempotencyKey) {
549
+ headers["Idempotency-Key"] = idempotencyKey;
550
+ }
551
+ return await this._request(
552
+ `/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans/${encodeURIComponent(normalizedPlanId)}/reject`,
553
+ {
554
+ method: "POST",
555
+ headers,
556
+ body: { note: note ?? undefined },
557
+ }
558
+ );
559
+ }
560
+
561
+ async applyAutomationPlan(projectId, planId, options = {}) {
562
+ const normalizedProjectId = this._requireNonEmptyString(projectId, "projectId");
563
+ const normalizedPlanId = this._requireNonEmptyString(planId, "planId");
564
+ const headers = {};
565
+ const idempotencyKey = options?.idempotencyKey ?? options?.idempotency_key;
566
+ if (idempotencyKey) {
567
+ headers["Idempotency-Key"] = idempotencyKey;
568
+ }
569
+ return await this._request(
570
+ `/projects/${encodeURIComponent(normalizedProjectId)}/automation-plans/${encodeURIComponent(normalizedPlanId)}/apply`,
571
+ {
572
+ method: "POST",
573
+ headers,
574
+ }
575
+ );
576
+ }
363
577
  }