@nexus-ai-fs/api-client 0.9.18
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/README.md +12 -0
- package/dist/index.cjs +769 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +352 -0
- package/dist/index.d.ts +352 -0
- package/dist/index.js +749 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var NexusApiError = class extends Error {
|
|
10
|
+
status;
|
|
11
|
+
code;
|
|
12
|
+
constructor(message, status, code) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "NexusApiError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var AuthenticationError = class extends NexusApiError {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message, 401, "authentication_error");
|
|
22
|
+
this.name = "AuthenticationError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var ForbiddenError = class extends NexusApiError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message, 403, "forbidden");
|
|
28
|
+
this.name = "ForbiddenError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var NotFoundError = class extends NexusApiError {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message, 404, "not_found");
|
|
34
|
+
this.name = "NotFoundError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ConflictError = class extends NexusApiError {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message, 409, "conflict");
|
|
40
|
+
this.name = "ConflictError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var RateLimitError = class extends NexusApiError {
|
|
44
|
+
retryAfter;
|
|
45
|
+
constructor(message, retryAfter) {
|
|
46
|
+
super(message, 429, "rate_limit_error");
|
|
47
|
+
this.name = "RateLimitError";
|
|
48
|
+
this.retryAfter = retryAfter;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var ServerError = class extends NexusApiError {
|
|
52
|
+
constructor(message, status) {
|
|
53
|
+
super(message, status, "server_error");
|
|
54
|
+
this.name = "ServerError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var NetworkError = class extends NexusApiError {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message, 0, "network_error");
|
|
60
|
+
this.name = "NetworkError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var TimeoutError = class extends NexusApiError {
|
|
64
|
+
constructor(message) {
|
|
65
|
+
super(message, 0, "timeout_error");
|
|
66
|
+
this.name = "TimeoutError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var AbortError = class extends NexusApiError {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(message, 0, "abort_error");
|
|
72
|
+
this.name = "AbortError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/case-transform.ts
|
|
77
|
+
function snakeToCamel(str) {
|
|
78
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
79
|
+
}
|
|
80
|
+
function camelToSnake(str) {
|
|
81
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
82
|
+
}
|
|
83
|
+
function transformKeys(value, transformer) {
|
|
84
|
+
if (value === null || value === void 0) {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
return value.map((item) => transformKeys(item, transformer));
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== "object") {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [key, val] of Object.entries(value)) {
|
|
98
|
+
result[transformer(key)] = transformKeys(val, transformer);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
function snakeToCamelKeys(value) {
|
|
103
|
+
return transformKeys(value, snakeToCamel);
|
|
104
|
+
}
|
|
105
|
+
function camelToSnakeKeys(value) {
|
|
106
|
+
return transformKeys(value, camelToSnake);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/fetch-client.ts
|
|
110
|
+
var DEFAULT_BASE_URL = "http://localhost:2026";
|
|
111
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
112
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
113
|
+
var INITIAL_RETRY_DELAY = 500;
|
|
114
|
+
var MAX_RETRY_DELAY = 8e3;
|
|
115
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
116
|
+
var FetchClient = class {
|
|
117
|
+
apiKey;
|
|
118
|
+
baseUrl;
|
|
119
|
+
timeout;
|
|
120
|
+
maxRetries;
|
|
121
|
+
fetchFn;
|
|
122
|
+
transformEnabled;
|
|
123
|
+
agentId;
|
|
124
|
+
subject;
|
|
125
|
+
zoneId;
|
|
126
|
+
constructor(options) {
|
|
127
|
+
this.apiKey = options.apiKey;
|
|
128
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
129
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
130
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
131
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
132
|
+
this.transformEnabled = options.transformKeys ?? true;
|
|
133
|
+
this.agentId = options.agentId;
|
|
134
|
+
this.subject = options.subject;
|
|
135
|
+
this.zoneId = options.zoneId;
|
|
136
|
+
}
|
|
137
|
+
async get(path, options) {
|
|
138
|
+
return this.request("GET", path, void 0, options);
|
|
139
|
+
}
|
|
140
|
+
async post(path, body, options) {
|
|
141
|
+
return this.request("POST", path, body, options);
|
|
142
|
+
}
|
|
143
|
+
async put(path, body, options) {
|
|
144
|
+
return this.request("PUT", path, body, options);
|
|
145
|
+
}
|
|
146
|
+
async patch(path, body, options) {
|
|
147
|
+
return this.request("PATCH", path, body, options);
|
|
148
|
+
}
|
|
149
|
+
async delete(path, options) {
|
|
150
|
+
return this.request("DELETE", path, void 0, options);
|
|
151
|
+
}
|
|
152
|
+
async postNoContent(path, body, options) {
|
|
153
|
+
await this.requestRaw("POST", path, body, options);
|
|
154
|
+
}
|
|
155
|
+
async deleteNoContent(path, options) {
|
|
156
|
+
await this.requestRaw("DELETE", path, void 0, options);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Execute an arbitrary HTTP request through the authenticated client.
|
|
160
|
+
*
|
|
161
|
+
* Returns the raw `Response` — no JSON parsing, no key transformation,
|
|
162
|
+
* no retries. Auth and identity headers are injected automatically.
|
|
163
|
+
* Timeout is enforced (defaults to client timeout, overridable per-request).
|
|
164
|
+
*
|
|
165
|
+
* Body is sent as-is (no JSON.stringify) since callers provide pre-formatted strings.
|
|
166
|
+
*
|
|
167
|
+
* Intended for the API Console and similar exploratory tools that need
|
|
168
|
+
* full control over the request/response while still using real auth.
|
|
169
|
+
*/
|
|
170
|
+
async rawRequest(method, path, body, options) {
|
|
171
|
+
const url = `${this.baseUrl}${path}`;
|
|
172
|
+
const headers = this.buildHeaders(method, options);
|
|
173
|
+
const effectiveTimeout = options?.timeout ?? this.timeout;
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
176
|
+
const userSignal = options?.signal;
|
|
177
|
+
const onUserAbort = () => controller.abort();
|
|
178
|
+
if (userSignal) {
|
|
179
|
+
if (userSignal.aborted) {
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
throw new AbortError("Request aborted");
|
|
182
|
+
}
|
|
183
|
+
userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
return await this.fetchFn(url, {
|
|
187
|
+
method,
|
|
188
|
+
headers,
|
|
189
|
+
body: body ?? void 0,
|
|
190
|
+
signal: controller.signal
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
194
|
+
if (userSignal?.aborted) {
|
|
195
|
+
throw new AbortError("Request aborted");
|
|
196
|
+
}
|
|
197
|
+
throw new TimeoutError("Request timed out");
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timeoutId);
|
|
202
|
+
if (userSignal) {
|
|
203
|
+
userSignal.removeEventListener("abort", onUserAbort);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ===========================================================================
|
|
208
|
+
// Core request logic
|
|
209
|
+
// ===========================================================================
|
|
210
|
+
async request(method, path, body, options) {
|
|
211
|
+
const response = await this.requestRaw(method, path, body, options);
|
|
212
|
+
if (response.status === 204) {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
const json = await response.json();
|
|
216
|
+
return this.transformEnabled ? snakeToCamelKeys(json) : json;
|
|
217
|
+
}
|
|
218
|
+
async requestRaw(method, path, body, options) {
|
|
219
|
+
const url = `${this.baseUrl}${path}`;
|
|
220
|
+
const headers = this.buildHeaders(method, options);
|
|
221
|
+
const effectiveTimeout = options?.timeout ?? this.timeout;
|
|
222
|
+
const transformedBody = body !== void 0 && this.transformEnabled ? camelToSnakeKeys(body) : body;
|
|
223
|
+
let lastError;
|
|
224
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
225
|
+
if (attempt > 0 && lastError) {
|
|
226
|
+
const delay = this.computeRetryDelay(attempt, lastError);
|
|
227
|
+
await sleep(delay);
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const response = await this.executeFetch(
|
|
231
|
+
url,
|
|
232
|
+
method,
|
|
233
|
+
headers,
|
|
234
|
+
transformedBody,
|
|
235
|
+
effectiveTimeout,
|
|
236
|
+
options?.signal
|
|
237
|
+
);
|
|
238
|
+
if (response.ok || response.status === 204) {
|
|
239
|
+
return response;
|
|
240
|
+
}
|
|
241
|
+
const error = await this.buildError(response);
|
|
242
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < this.maxRetries) {
|
|
243
|
+
lastError = error;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof NexusApiError && !RETRYABLE_STATUS_CODES.has(error.status)) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
if (attempt < this.maxRetries) {
|
|
252
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (error instanceof NexusApiError) {
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
throw new NetworkError(
|
|
259
|
+
error instanceof Error ? error.message : "Request failed"
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
throw lastError ?? new NetworkError("Request failed");
|
|
264
|
+
}
|
|
265
|
+
// ===========================================================================
|
|
266
|
+
// Helpers
|
|
267
|
+
// ===========================================================================
|
|
268
|
+
async executeFetch(url, method, headers, body, timeout, userSignal) {
|
|
269
|
+
if (userSignal?.aborted) {
|
|
270
|
+
throw new AbortError("Request aborted");
|
|
271
|
+
}
|
|
272
|
+
const controller = new AbortController();
|
|
273
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
274
|
+
const onUserAbort = () => controller.abort();
|
|
275
|
+
if (userSignal) {
|
|
276
|
+
userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
return await this.fetchFn(url, {
|
|
280
|
+
method,
|
|
281
|
+
headers,
|
|
282
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
283
|
+
signal: controller.signal
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
287
|
+
if (userSignal?.aborted) {
|
|
288
|
+
throw new AbortError("Request aborted");
|
|
289
|
+
}
|
|
290
|
+
throw new TimeoutError("Request timed out");
|
|
291
|
+
}
|
|
292
|
+
throw error;
|
|
293
|
+
} finally {
|
|
294
|
+
clearTimeout(timeoutId);
|
|
295
|
+
if (userSignal) {
|
|
296
|
+
userSignal.removeEventListener("abort", onUserAbort);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
buildHeaders(method, options) {
|
|
301
|
+
const headers = {
|
|
302
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
303
|
+
Accept: "application/json"
|
|
304
|
+
};
|
|
305
|
+
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
|
306
|
+
headers["Content-Type"] = "application/json";
|
|
307
|
+
}
|
|
308
|
+
if (options?.idempotencyKey) {
|
|
309
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
310
|
+
}
|
|
311
|
+
if (this.agentId) headers["X-Agent-ID"] = this.agentId;
|
|
312
|
+
if (this.subject) headers["X-Nexus-Subject"] = this.subject;
|
|
313
|
+
if (this.zoneId) headers["X-Nexus-Zone-ID"] = this.zoneId;
|
|
314
|
+
if (options?.headers) {
|
|
315
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
316
|
+
headers[key] = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return headers;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Map HTTP response to a typed error.
|
|
323
|
+
*
|
|
324
|
+
* Subclasses can override this to add domain-specific error mapping
|
|
325
|
+
* (e.g. 402 → InsufficientCreditsError in nexus-pay-ts).
|
|
326
|
+
*/
|
|
327
|
+
async buildError(response) {
|
|
328
|
+
let message;
|
|
329
|
+
try {
|
|
330
|
+
const body = await response.json();
|
|
331
|
+
message = body.detail ?? `HTTP ${response.status}`;
|
|
332
|
+
} catch {
|
|
333
|
+
message = `HTTP ${response.status}`;
|
|
334
|
+
}
|
|
335
|
+
switch (response.status) {
|
|
336
|
+
case 401:
|
|
337
|
+
return new AuthenticationError(message);
|
|
338
|
+
case 403:
|
|
339
|
+
return new ForbiddenError(message);
|
|
340
|
+
case 404:
|
|
341
|
+
return new NotFoundError(message);
|
|
342
|
+
case 409:
|
|
343
|
+
return new ConflictError(message);
|
|
344
|
+
case 429: {
|
|
345
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
346
|
+
const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : void 0;
|
|
347
|
+
return new RateLimitError(
|
|
348
|
+
message,
|
|
349
|
+
Number.isNaN(retryAfter) ? void 0 : retryAfter
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
default:
|
|
353
|
+
if (response.status >= 500) {
|
|
354
|
+
return new ServerError(message, response.status);
|
|
355
|
+
}
|
|
356
|
+
return new NexusApiError(message, response.status, "api_error");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
computeRetryDelay(attempt, lastError) {
|
|
360
|
+
if (lastError instanceof RateLimitError && lastError.retryAfter !== void 0) {
|
|
361
|
+
return lastError.retryAfter * 1e3;
|
|
362
|
+
}
|
|
363
|
+
const exponentialDelay = INITIAL_RETRY_DELAY * Math.pow(2, attempt - 1);
|
|
364
|
+
const cappedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY);
|
|
365
|
+
return Math.random() * cappedDelay;
|
|
366
|
+
}
|
|
367
|
+
// ===========================================================================
|
|
368
|
+
// Knowledge platform helpers (Issue #2930)
|
|
369
|
+
// ===========================================================================
|
|
370
|
+
/**
|
|
371
|
+
* List all aspect names attached to an entity.
|
|
372
|
+
*/
|
|
373
|
+
async getAspects(urn) {
|
|
374
|
+
const result = await this.get(
|
|
375
|
+
`/api/v2/aspects/${encodeURIComponent(urn)}`
|
|
376
|
+
);
|
|
377
|
+
return result.aspects ?? [];
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get a specific aspect for an entity. Returns null if not found.
|
|
381
|
+
*/
|
|
382
|
+
async getAspect(urn, name) {
|
|
383
|
+
try {
|
|
384
|
+
return await this.get(
|
|
385
|
+
`/api/v2/aspects/${encodeURIComponent(urn)}/${encodeURIComponent(name)}`
|
|
386
|
+
);
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get extracted schema for a data file. Returns null if no schema.
|
|
393
|
+
*/
|
|
394
|
+
async getCatalogSchema(path) {
|
|
395
|
+
const encodedPath = encodeURIComponent(path.replace(/^\//, ""));
|
|
396
|
+
const result = await this.get(
|
|
397
|
+
`/api/v2/catalog/schema/${encodedPath}`
|
|
398
|
+
);
|
|
399
|
+
return result.schema ?? null;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Search for data files containing a specific column name.
|
|
403
|
+
*/
|
|
404
|
+
async searchByColumn(column) {
|
|
405
|
+
const result = await this.get(
|
|
406
|
+
`/api/v2/catalog/search?column=${encodeURIComponent(column)}`
|
|
407
|
+
);
|
|
408
|
+
return result.results ?? [];
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Replay metadata change log records.
|
|
412
|
+
*/
|
|
413
|
+
async replayChanges(opts) {
|
|
414
|
+
const params = new URLSearchParams();
|
|
415
|
+
if (opts?.fromSequence !== void 0) params.set("from_sequence", String(opts.fromSequence));
|
|
416
|
+
if (opts?.entityUrn) params.set("entity_urn", opts.entityUrn);
|
|
417
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
418
|
+
const qs = params.toString();
|
|
419
|
+
return this.get(`/api/v2/ops/replay${qs ? `?${qs}` : ""}`);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
function sleep(ms) {
|
|
423
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/sse-client.ts
|
|
427
|
+
var DEFAULT_BUFFER_CAPACITY = 1e3;
|
|
428
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 100;
|
|
429
|
+
var INITIAL_RECONNECT_DELAY_MS = 500;
|
|
430
|
+
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
431
|
+
var RingBuffer = class {
|
|
432
|
+
constructor(capacity) {
|
|
433
|
+
this.capacity = capacity;
|
|
434
|
+
this.buffer = new Array(capacity);
|
|
435
|
+
}
|
|
436
|
+
buffer;
|
|
437
|
+
head = 0;
|
|
438
|
+
count = 0;
|
|
439
|
+
/** Monotonically increasing counter of total items ever pushed (never wraps). */
|
|
440
|
+
_totalPushed = 0;
|
|
441
|
+
get size() {
|
|
442
|
+
return this.count;
|
|
443
|
+
}
|
|
444
|
+
/** Total number of items pushed since creation/clear (never decreases). */
|
|
445
|
+
get totalPushed() {
|
|
446
|
+
return this._totalPushed;
|
|
447
|
+
}
|
|
448
|
+
push(item) {
|
|
449
|
+
this.buffer[this.head] = item;
|
|
450
|
+
this.head = (this.head + 1) % this.capacity;
|
|
451
|
+
if (this.count < this.capacity) {
|
|
452
|
+
this.count++;
|
|
453
|
+
}
|
|
454
|
+
this._totalPushed++;
|
|
455
|
+
}
|
|
456
|
+
/** Return items in insertion order (oldest first). */
|
|
457
|
+
toArray() {
|
|
458
|
+
if (this.count === 0) return [];
|
|
459
|
+
const result = [];
|
|
460
|
+
const start = this.count < this.capacity ? 0 : this.head;
|
|
461
|
+
for (let i = 0; i < this.count; i++) {
|
|
462
|
+
const index = (start + i) % this.capacity;
|
|
463
|
+
result.push(this.buffer[index]);
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
/** Return the last N items (newest first becomes oldest first). */
|
|
468
|
+
lastN(n) {
|
|
469
|
+
if (n <= 0 || this.count === 0) return [];
|
|
470
|
+
const take = Math.min(n, this.count);
|
|
471
|
+
const all = this.toArray();
|
|
472
|
+
return all.slice(all.length - take);
|
|
473
|
+
}
|
|
474
|
+
clear() {
|
|
475
|
+
this.buffer.fill(void 0);
|
|
476
|
+
this.head = 0;
|
|
477
|
+
this.count = 0;
|
|
478
|
+
this._totalPushed = 0;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var SseClient = class {
|
|
482
|
+
baseUrl;
|
|
483
|
+
apiKey;
|
|
484
|
+
fetchFn;
|
|
485
|
+
buffer;
|
|
486
|
+
flushIntervalMs;
|
|
487
|
+
agentId;
|
|
488
|
+
subject;
|
|
489
|
+
zoneId;
|
|
490
|
+
abortController = null;
|
|
491
|
+
flushTimer = null;
|
|
492
|
+
reconnectAttempt = 0;
|
|
493
|
+
lastEventId;
|
|
494
|
+
connected = false;
|
|
495
|
+
eventHandler = null;
|
|
496
|
+
errorHandler = null;
|
|
497
|
+
reconnectHandler = null;
|
|
498
|
+
constructor(options) {
|
|
499
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
500
|
+
this.apiKey = options.apiKey;
|
|
501
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
502
|
+
this.buffer = new RingBuffer(options.bufferCapacity ?? DEFAULT_BUFFER_CAPACITY);
|
|
503
|
+
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
504
|
+
this.agentId = options.agentId;
|
|
505
|
+
this.subject = options.subject;
|
|
506
|
+
this.zoneId = options.zoneId;
|
|
507
|
+
}
|
|
508
|
+
onEvent(handler) {
|
|
509
|
+
this.eventHandler = handler;
|
|
510
|
+
}
|
|
511
|
+
onError(handler) {
|
|
512
|
+
this.errorHandler = handler;
|
|
513
|
+
}
|
|
514
|
+
onReconnect(handler) {
|
|
515
|
+
this.reconnectHandler = handler;
|
|
516
|
+
}
|
|
517
|
+
get isConnected() {
|
|
518
|
+
return this.connected;
|
|
519
|
+
}
|
|
520
|
+
async connect(path) {
|
|
521
|
+
this.disconnect();
|
|
522
|
+
this.abortController = new AbortController();
|
|
523
|
+
this.startFlushTimer();
|
|
524
|
+
await this.connectWithRetry(path);
|
|
525
|
+
}
|
|
526
|
+
disconnect() {
|
|
527
|
+
this.abortController?.abort();
|
|
528
|
+
this.abortController = null;
|
|
529
|
+
this.stopFlushTimer();
|
|
530
|
+
this.connected = false;
|
|
531
|
+
this.reconnectAttempt = 0;
|
|
532
|
+
this.lastFlushedTotal = 0;
|
|
533
|
+
}
|
|
534
|
+
getBufferedEvents() {
|
|
535
|
+
return this.buffer.toArray();
|
|
536
|
+
}
|
|
537
|
+
clearBuffer() {
|
|
538
|
+
this.buffer.clear();
|
|
539
|
+
this.lastFlushedTotal = 0;
|
|
540
|
+
}
|
|
541
|
+
// ===========================================================================
|
|
542
|
+
// Internal
|
|
543
|
+
// ===========================================================================
|
|
544
|
+
async connectWithRetry(path) {
|
|
545
|
+
while (this.abortController && !this.abortController.signal.aborted) {
|
|
546
|
+
try {
|
|
547
|
+
await this.streamEvents(path);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (this.abortController?.signal.aborted) return;
|
|
550
|
+
this.connected = false;
|
|
551
|
+
this.reconnectAttempt++;
|
|
552
|
+
this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
|
|
553
|
+
this.reconnectHandler?.(this.reconnectAttempt);
|
|
554
|
+
const delay = this.computeReconnectDelay();
|
|
555
|
+
await sleep2(delay);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async streamEvents(path) {
|
|
560
|
+
const url = `${this.baseUrl}${path}`;
|
|
561
|
+
const headers = {
|
|
562
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
563
|
+
Accept: "text/event-stream"
|
|
564
|
+
};
|
|
565
|
+
if (this.agentId) headers["X-Agent-ID"] = this.agentId;
|
|
566
|
+
if (this.subject) headers["X-Nexus-Subject"] = this.subject;
|
|
567
|
+
if (this.zoneId) headers["X-Nexus-Zone-ID"] = this.zoneId;
|
|
568
|
+
if (this.lastEventId) {
|
|
569
|
+
headers["Last-Event-ID"] = this.lastEventId;
|
|
570
|
+
}
|
|
571
|
+
const response = await this.fetchFn(url, {
|
|
572
|
+
headers,
|
|
573
|
+
signal: this.abortController?.signal
|
|
574
|
+
});
|
|
575
|
+
if (!response.ok) {
|
|
576
|
+
throw new Error(`SSE connection failed: HTTP ${response.status}`);
|
|
577
|
+
}
|
|
578
|
+
if (!response.body) {
|
|
579
|
+
throw new Error("SSE response has no body");
|
|
580
|
+
}
|
|
581
|
+
this.connected = true;
|
|
582
|
+
this.reconnectAttempt = 0;
|
|
583
|
+
const reader = response.body.getReader();
|
|
584
|
+
const decoder = new TextDecoder();
|
|
585
|
+
let partial = "";
|
|
586
|
+
try {
|
|
587
|
+
while (true) {
|
|
588
|
+
const { done, value } = await reader.read();
|
|
589
|
+
if (done) break;
|
|
590
|
+
partial += decoder.decode(value, { stream: true });
|
|
591
|
+
const events = this.parseEvents(partial);
|
|
592
|
+
partial = events.remaining;
|
|
593
|
+
for (const event of events.parsed) {
|
|
594
|
+
this.buffer.push(event);
|
|
595
|
+
if (event.id) {
|
|
596
|
+
this.lastEventId = event.id;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} finally {
|
|
601
|
+
reader.releaseLock();
|
|
602
|
+
this.connected = false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
parseEvents(text) {
|
|
606
|
+
const parsed = [];
|
|
607
|
+
const blocks = text.split("\n\n");
|
|
608
|
+
const remaining = blocks.pop() ?? "";
|
|
609
|
+
for (const block of blocks) {
|
|
610
|
+
if (!block.trim()) continue;
|
|
611
|
+
let id;
|
|
612
|
+
let event = "message";
|
|
613
|
+
let data = "";
|
|
614
|
+
let retry;
|
|
615
|
+
for (const line of block.split("\n")) {
|
|
616
|
+
if (line.startsWith("id:")) {
|
|
617
|
+
id = line.slice(3).trim();
|
|
618
|
+
} else if (line.startsWith("event:")) {
|
|
619
|
+
event = line.slice(6).trim();
|
|
620
|
+
} else if (line.startsWith("data:")) {
|
|
621
|
+
data += (data ? "\n" : "") + line.slice(5).trim();
|
|
622
|
+
} else if (line.startsWith("retry:")) {
|
|
623
|
+
const val = parseInt(line.slice(6).trim(), 10);
|
|
624
|
+
if (!Number.isNaN(val)) retry = val;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (data || event !== "message") {
|
|
628
|
+
parsed.push({ id, event, data, retry });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return { parsed, remaining };
|
|
632
|
+
}
|
|
633
|
+
/** Tracks total events flushed via the monotonic totalPushed counter. */
|
|
634
|
+
lastFlushedTotal = 0;
|
|
635
|
+
startFlushTimer() {
|
|
636
|
+
this.lastFlushedTotal = this.buffer.totalPushed;
|
|
637
|
+
this.flushTimer = setInterval(() => {
|
|
638
|
+
const currentTotal = this.buffer.totalPushed;
|
|
639
|
+
if (currentTotal > this.lastFlushedTotal) {
|
|
640
|
+
const newCount = currentTotal - this.lastFlushedTotal;
|
|
641
|
+
const newEvents = this.buffer.lastN(newCount);
|
|
642
|
+
this.lastFlushedTotal = currentTotal;
|
|
643
|
+
if (newEvents.length > 0) {
|
|
644
|
+
this.eventHandler?.(newEvents);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}, this.flushIntervalMs);
|
|
648
|
+
}
|
|
649
|
+
stopFlushTimer() {
|
|
650
|
+
if (this.flushTimer) {
|
|
651
|
+
clearInterval(this.flushTimer);
|
|
652
|
+
this.flushTimer = null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
computeReconnectDelay() {
|
|
656
|
+
const exponential = INITIAL_RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempt - 1);
|
|
657
|
+
const capped = Math.min(exponential, MAX_RECONNECT_DELAY_MS);
|
|
658
|
+
return capped + Math.random() * capped * 0.1;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
function sleep2(ms) {
|
|
662
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/config.ts
|
|
666
|
+
var DEFAULT_BASE_URL2 = "http://localhost:2026";
|
|
667
|
+
function resolveConfig(overrides) {
|
|
668
|
+
const yamlConfig = readYamlConfig();
|
|
669
|
+
const envUrl = readEnv("NEXUS_URL");
|
|
670
|
+
const envApiKey = readEnv("NEXUS_API_KEY");
|
|
671
|
+
return {
|
|
672
|
+
apiKey: overrides?.apiKey ?? yamlConfig.apiKey ?? envApiKey ?? "",
|
|
673
|
+
baseUrl: overrides?.baseUrl ?? yamlConfig.url ?? envUrl ?? DEFAULT_BASE_URL2,
|
|
674
|
+
timeout: overrides?.timeout,
|
|
675
|
+
maxRetries: overrides?.maxRetries,
|
|
676
|
+
fetch: overrides?.fetch,
|
|
677
|
+
transformKeys: overrides?.transformKeys,
|
|
678
|
+
agentId: overrides?.agentId ?? yamlConfig.agentId ?? readEnv("NEXUS_AGENT_ID"),
|
|
679
|
+
subject: overrides?.subject ?? readEnv("NEXUS_SUBJECT"),
|
|
680
|
+
zoneId: overrides?.zoneId ?? yamlConfig.zoneId ?? readEnv("NEXUS_ZONE_ID")
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function readEnv(name) {
|
|
684
|
+
try {
|
|
685
|
+
return typeof process !== "undefined" ? process.env[name] : void 0;
|
|
686
|
+
} catch {
|
|
687
|
+
return void 0;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function readYamlConfig() {
|
|
691
|
+
try {
|
|
692
|
+
const fs = __require("fs");
|
|
693
|
+
const os = __require("os");
|
|
694
|
+
const path = __require("path");
|
|
695
|
+
const candidates = [];
|
|
696
|
+
let dir = path.resolve(".");
|
|
697
|
+
for (let i = 0; i < 20; i++) {
|
|
698
|
+
candidates.push(path.join(dir, "nexus.yaml"));
|
|
699
|
+
candidates.push(path.join(dir, "nexus.yml"));
|
|
700
|
+
try {
|
|
701
|
+
if (fs.existsSync(path.join(dir, ".git"))) break;
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
const parent = path.dirname(dir);
|
|
705
|
+
if (parent === dir) break;
|
|
706
|
+
dir = parent;
|
|
707
|
+
}
|
|
708
|
+
candidates.push(path.join(os.homedir(), ".nexus", "config.yaml"));
|
|
709
|
+
for (const configPath of candidates) {
|
|
710
|
+
try {
|
|
711
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
712
|
+
let url = extractYamlValue(content, "url");
|
|
713
|
+
const apiKey = extractYamlValue(content, "api_key");
|
|
714
|
+
const agentId = extractYamlValue(content, "agent_id");
|
|
715
|
+
const zoneId = extractYamlValue(content, "zone_id");
|
|
716
|
+
if (!url) {
|
|
717
|
+
const httpPort = extractIndentedYamlValue(content, "ports", "http");
|
|
718
|
+
if (httpPort) {
|
|
719
|
+
url = `http://localhost:${httpPort}`;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
url: url ?? void 0,
|
|
724
|
+
apiKey: apiKey ?? void 0,
|
|
725
|
+
agentId: agentId ?? void 0,
|
|
726
|
+
zoneId: zoneId ?? void 0
|
|
727
|
+
};
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return {};
|
|
732
|
+
} catch {
|
|
733
|
+
return {};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function extractYamlValue(content, key) {
|
|
737
|
+
const regex = new RegExp(`^${key}:\\s*["']?([^"'\\n]+)["']?`, "m");
|
|
738
|
+
const match = regex.exec(content);
|
|
739
|
+
return match?.[1]?.trim() ?? null;
|
|
740
|
+
}
|
|
741
|
+
function extractIndentedYamlValue(content, parent, child) {
|
|
742
|
+
const regex = new RegExp(`^${parent}:\\s*\\n(?:[ ]{2}\\w+:[^\\n]*\\n)*?[ ]{2}${child}:\\s*["']?([^"'\\n]+)["']?`, "m");
|
|
743
|
+
const match = regex.exec(content);
|
|
744
|
+
return match?.[1]?.trim() ?? null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export { AbortError, AuthenticationError, ConflictError, FetchClient, ForbiddenError, NetworkError, NexusApiError, NotFoundError, RateLimitError, RingBuffer, ServerError, SseClient, TimeoutError, camelToSnake, camelToSnakeKeys, resolveConfig, snakeToCamel, snakeToCamelKeys, transformKeys };
|
|
748
|
+
//# sourceMappingURL=index.js.map
|
|
749
|
+
//# sourceMappingURL=index.js.map
|