@ekodb/ekodb-client 0.25.0 → 0.26.1

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 CHANGED
@@ -308,8 +308,11 @@ const joinResults = await client.find("users", multiQuery);
308
308
 
309
309
  #### Chat Models
310
310
 
311
- - `getChatModels(): Promise<Record<string, string[]>>` - Get all available chat
312
- models by provider
311
+ - `getChatModels(): Promise<ChatModels>` - Get all available chat models by
312
+ provider (`openai`, `anthropic`, `perplexity`, `gemini`), plus a per-provider
313
+ `providers` status map (`ok`, `not_configured`, `auth_failed`,
314
+ `permission_denied`, `billing`, `rate_limited`, `unavailable`, `unreachable`,
315
+ `request_error`) so a rejected key is distinguishable from a missing one
313
316
  - `getChatModel(provider: string): Promise<string[]>` - Get models for a
314
317
  specific provider
315
318
 
package/dist/client.d.ts CHANGED
@@ -326,12 +326,45 @@ export interface MergeSessionsRequest {
326
326
  bypass_ripple?: boolean;
327
327
  }
328
328
  /**
329
- * Available chat models by provider
329
+ * A provider's state on `GET /api/chat_models`. The union lists the states
330
+ * this client knows; the `string` escape keeps a newer server's status from
331
+ * failing to type-check.
332
+ */
333
+ export type ChatProviderState = "ok" | "not_configured" | "auth_failed" | "permission_denied" | "billing" | "rate_limited" | "unavailable" | "unreachable" | "request_error" | (string & {});
334
+ /**
335
+ * One provider's row in `ChatModels.providers`.
336
+ */
337
+ export interface ChatProviderStatus {
338
+ status: ChatProviderState;
339
+ /**
340
+ * True when the status is the provider's own answer about the configured
341
+ * key. A 5xx, a refused connection, or a missing key says nothing about it.
342
+ */
343
+ verified: boolean;
344
+ /** The provider's own HTTP status, when it answered. */
345
+ http_status?: number;
346
+ /** The provider's own message, when it answered. */
347
+ message?: string;
348
+ /** How many models were listed, when the status is `ok`. */
349
+ model_count?: number;
350
+ }
351
+ /**
352
+ * Available chat models by provider, and why each list looks the way it does.
330
353
  */
331
354
  export interface ChatModels {
332
355
  openai: string[];
333
356
  anthropic: string[];
334
357
  perplexity: string[];
358
+ /** Google Gemini models. Absent from a server that predates the field. */
359
+ gemini?: string[];
360
+ /**
361
+ * Per-provider status keyed by provider name. A rejected key reports
362
+ * `auth_failed` where a missing one reports `not_configured`, so an empty
363
+ * list is never ambiguous. Absent from a server that predates the map.
364
+ */
365
+ providers?: {
366
+ [provider: string]: ChatProviderStatus;
367
+ };
335
368
  }
336
369
  /**
337
370
  * Request to compact a chat session's history on demand.
@@ -1213,9 +1246,21 @@ export declare class EkoDBClient {
1213
1246
  agentsByDeployment(deploymentId: string): Promise<Record>;
1214
1247
  /** Get documents linked to a KV key */
1215
1248
  kvGetLinks(key: string): Promise<Record>;
1216
- /** Link a document to a KV key */
1217
- kvLink(key: string, collection: string, documentId: string): Promise<Record>;
1218
- /** Unlink a document from a KV key */
1249
+ /**
1250
+ * Link a document to a KV key.
1251
+ *
1252
+ * The identifying triple goes in the path; the body carries the optional
1253
+ * link payload (`keys`, `field_path`, `metadata`), and an empty object means
1254
+ * "no extra link data".
1255
+ */
1256
+ kvLink(key: string, collection: string, documentId: string, linkData?: {
1257
+ keys?: string[];
1258
+ field_path?: string;
1259
+ metadata?: {
1260
+ [key: string]: string;
1261
+ };
1262
+ }): Promise<Record>;
1263
+ /** Unlink a document from a KV key. DELETE, with the triple in the path. */
1219
1264
  kvUnlink(key: string, collection: string, documentId: string): Promise<Record>;
1220
1265
  /** Create a new schedule */
1221
1266
  createSchedule(data: Record): Promise<Record>;
@@ -1227,10 +1272,25 @@ export declare class EkoDBClient {
1227
1272
  updateSchedule(id: string, data: Record): Promise<Record>;
1228
1273
  /** Delete a schedule */
1229
1274
  deleteSchedule(id: string): Promise<void>;
1230
- /** Pause a schedule */
1275
+ /**
1276
+ * Pause a schedule.
1277
+ *
1278
+ * There is no `/pause` endpoint — pausing is a partial update of the
1279
+ * schedule's `enabled` flag. This previously POSTed to
1280
+ * `/api/schedules/{id}/pause`, which has never existed and always 404'd.
1281
+ */
1231
1282
  pauseSchedule(id: string): Promise<Record>;
1232
- /** Resume a schedule */
1283
+ /**
1284
+ * Resume a paused schedule. See {@link pauseSchedule} for why this is an
1285
+ * update rather than its own endpoint.
1286
+ */
1233
1287
  resumeSchedule(id: string): Promise<Record>;
1288
+ /**
1289
+ * Shared implementation for pause/resume: a partial update carrying only
1290
+ * `enabled`. The server recomputes the next execution time when `enabled`
1291
+ * changes, so nothing else needs sending.
1292
+ */
1293
+ private setScheduleEnabled;
1234
1294
  /**
1235
1295
  * Check if a collection exists
1236
1296
  * @param collection - Collection name to check
@@ -1389,6 +1449,19 @@ export type ChatStreamEvent = {
1389
1449
  } | {
1390
1450
  type: "error";
1391
1451
  error: string;
1452
+ /**
1453
+ * The provider-failure classification (`provider_auth_failed`,
1454
+ * `provider_permission_denied`, `provider_billing`,
1455
+ * `provider_rate_limited`, `provider_unavailable`,
1456
+ * `provider_unreachable`, `provider_not_configured`,
1457
+ * `provider_request_error`), when the failure was the LLM provider's
1458
+ * answer. Absent for a transport failure or a plain server error.
1459
+ */
1460
+ errorKind?: string;
1461
+ provider?: string;
1462
+ /** The provider's own HTTP status. */
1463
+ providerStatus?: number;
1464
+ retryAfterSecs?: number;
1392
1465
  };
1393
1466
  /** Definition for a client-side tool the LLM can call. */
1394
1467
  export interface ClientToolDefinition {
package/dist/client.js CHANGED
@@ -2,39 +2,6 @@
2
2
  /**
3
3
  * ekoDB TypeScript Client
4
4
  */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.HealthStatus = exports.HealthUnknown = exports.HealthDegraded = exports.HealthOK = exports.RateLimitError = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.SerializationFormat = void 0;
40
7
  exports.parseHealthStatus = parseHealthStatus;
@@ -360,11 +327,11 @@ class EkoDBClient {
360
327
  // ONLY these operations support MessagePack
361
328
  const msgpackPaths = [
362
329
  "/api/insert/",
363
- "/api/batch_insert/",
330
+ "/api/batch/insert/",
364
331
  "/api/update/",
365
- "/api/batch_update/",
332
+ "/api/batch/update/",
366
333
  "/api/delete/",
367
- "/api/batch_delete/",
334
+ "/api/batch/delete/",
368
335
  ];
369
336
  // Check if path starts with any MessagePack-supported operation
370
337
  for (const prefix of msgpackPaths) {
@@ -1454,7 +1421,22 @@ class EkoDBClient {
1454
1421
  stream.close();
1455
1422
  return;
1456
1423
  }
1424
+ // The `event:` name applies to the data lines that follow it, until
1425
+ // the blank line that ends the frame. An error frame ends the stream:
1426
+ // nothing after it is surfaced and the body is not read to the end,
1427
+ // so a server or proxy that keeps the connection open after an error
1428
+ // cannot hang the caller (the Rust and Go clients stop the same way).
1429
+ let eventName = "";
1430
+ let stopped = false;
1457
1431
  const emitLine = (line) => {
1432
+ if (line.startsWith("event:")) {
1433
+ eventName = line.slice(6).trim();
1434
+ return;
1435
+ }
1436
+ if (line.trim() === "") {
1437
+ eventName = "";
1438
+ return;
1439
+ }
1458
1440
  if (!line.startsWith("data:"))
1459
1441
  return;
1460
1442
  const dataStr = line.slice(5).trim();
@@ -1462,10 +1444,16 @@ class EkoDBClient {
1462
1444
  return;
1463
1445
  try {
1464
1446
  const eventData = JSON.parse(dataStr);
1465
- if (eventData.error) {
1447
+ // An error frame is one the server names `error`, or whose
1448
+ // payload carries an `error`; a `message`-only payload is still
1449
+ // the error rather than a frame to skip, and the text is always a
1450
+ // string (`streamErrorText`).
1451
+ if (eventData.error != null || eventName === "error") {
1452
+ stopped = true;
1466
1453
  stream.emit("event", {
1467
1454
  type: "error",
1468
- error: eventData.error,
1455
+ error: streamErrorText(eventData),
1456
+ ...providerFailureFields(eventData),
1469
1457
  });
1470
1458
  }
1471
1459
  else if (eventData.content && eventData.message_id) {
@@ -1501,20 +1489,29 @@ class EkoDBClient {
1501
1489
  break;
1502
1490
  buffer += decoder.decode(value, { stream: true });
1503
1491
  let nl;
1504
- while ((nl = buffer.indexOf("\n")) >= 0) {
1492
+ while (!stopped && (nl = buffer.indexOf("\n")) >= 0) {
1505
1493
  emitLine(buffer.slice(0, nl));
1506
1494
  buffer = buffer.slice(nl + 1);
1507
1495
  }
1496
+ if (stopped) {
1497
+ await reader.cancel?.()?.catch?.(() => { });
1498
+ break;
1499
+ }
1500
+ }
1501
+ if (!stopped) {
1502
+ buffer += decoder.decode();
1503
+ if (buffer)
1504
+ emitLine(buffer);
1508
1505
  }
1509
- buffer += decoder.decode();
1510
- if (buffer)
1511
- emitLine(buffer);
1512
1506
  }
1513
1507
  else {
1514
1508
  // Fallback for environments/tests without a readable body stream.
1515
1509
  const body = await response.text();
1516
- for (const line of body.split("\n"))
1510
+ for (const line of body.split("\n")) {
1517
1511
  emitLine(line);
1512
+ if (stopped)
1513
+ break;
1514
+ }
1518
1515
  }
1519
1516
  stream.close();
1520
1517
  }
@@ -1913,15 +1910,21 @@ class EkoDBClient {
1913
1910
  // ========================================================================
1914
1911
  /** Get documents linked to a KV key */
1915
1912
  async kvGetLinks(key) {
1916
- return this.makeRequest("GET", `/api/kv/links/${encodeURIComponent(key)}`, undefined, 0, true);
1913
+ return this.makeRequest("GET", `/api/kv/${encodeURIComponent(key)}/links`, undefined, 0, true);
1917
1914
  }
1918
- /** Link a document to a KV key */
1919
- async kvLink(key, collection, documentId) {
1920
- return this.makeRequest("POST", `/api/kv/link`, { key, collection, document_id: documentId }, 0, true);
1915
+ /**
1916
+ * Link a document to a KV key.
1917
+ *
1918
+ * The identifying triple goes in the path; the body carries the optional
1919
+ * link payload (`keys`, `field_path`, `metadata`), and an empty object means
1920
+ * "no extra link data".
1921
+ */
1922
+ async kvLink(key, collection, documentId, linkData = {}) {
1923
+ return this.makeRequest("POST", `/api/kv/${encodeURIComponent(key)}/links/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}`, linkData, 0, true);
1921
1924
  }
1922
- /** Unlink a document from a KV key */
1925
+ /** Unlink a document from a KV key. DELETE, with the triple in the path. */
1923
1926
  async kvUnlink(key, collection, documentId) {
1924
- return this.makeRequest("POST", `/api/kv/unlink`, { key, collection, document_id: documentId }, 0, true);
1927
+ return this.makeRequest("DELETE", `/api/kv/${encodeURIComponent(key)}/links/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}`, undefined, 0, true);
1925
1928
  }
1926
1929
  // ========================================================================
1927
1930
  // SCHEDULE MANAGEMENT
@@ -1946,13 +1949,30 @@ class EkoDBClient {
1946
1949
  async deleteSchedule(id) {
1947
1950
  await this.makeRequest("DELETE", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
1948
1951
  }
1949
- /** Pause a schedule */
1952
+ /**
1953
+ * Pause a schedule.
1954
+ *
1955
+ * There is no `/pause` endpoint — pausing is a partial update of the
1956
+ * schedule's `enabled` flag. This previously POSTed to
1957
+ * `/api/schedules/{id}/pause`, which has never existed and always 404'd.
1958
+ */
1950
1959
  async pauseSchedule(id) {
1951
- return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/pause`, undefined, 0, true);
1960
+ return this.setScheduleEnabled(id, false);
1952
1961
  }
1953
- /** Resume a schedule */
1962
+ /**
1963
+ * Resume a paused schedule. See {@link pauseSchedule} for why this is an
1964
+ * update rather than its own endpoint.
1965
+ */
1954
1966
  async resumeSchedule(id) {
1955
- return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/resume`, undefined, 0, true);
1967
+ return this.setScheduleEnabled(id, true);
1968
+ }
1969
+ /**
1970
+ * Shared implementation for pause/resume: a partial update carrying only
1971
+ * `enabled`. The server recomputes the next execution time when `enabled`
1972
+ * changes, so nothing else needs sending.
1973
+ */
1974
+ async setScheduleEnabled(id, enabled) {
1975
+ return this.makeRequest("PUT", `/api/schedules/${encodeURIComponent(id)}`, { enabled }, 0, true);
1956
1976
  }
1957
1977
  // ========================================================================
1958
1978
  // COLLECTION UTILITIES
@@ -2216,6 +2236,32 @@ class EkoDBClient {
2216
2236
  }
2217
2237
  }
2218
2238
  exports.EkoDBClient = EkoDBClient;
2239
+ /**
2240
+ * The text of a stream error frame: the first of `error` / `message` that is
2241
+ * a non-empty string, else a fixed fallback — a structured `error` object is
2242
+ * still an error, never a non-string `error` on the event. Shared by the SSE
2243
+ * and WebSocket routes so the two cannot drift.
2244
+ */
2245
+ function streamErrorText(payload) {
2246
+ const text = (value) => typeof value === "string" && value ? value : undefined;
2247
+ return text(payload.error) ?? text(payload.message) ?? "Unknown error";
2248
+ }
2249
+ /**
2250
+ * The classification fields of a stream error frame, only those present, so
2251
+ * a plain error stays `{ type, error }`.
2252
+ */
2253
+ function providerFailureFields(eventData) {
2254
+ const fields = {};
2255
+ if (typeof eventData.error_kind === "string")
2256
+ fields.errorKind = eventData.error_kind;
2257
+ if (typeof eventData.provider === "string")
2258
+ fields.provider = eventData.provider;
2259
+ if (typeof eventData.provider_status === "number")
2260
+ fields.providerStatus = eventData.provider_status;
2261
+ if (typeof eventData.retry_after_secs === "number")
2262
+ fields.retryAfterSecs = eventData.retry_after_secs;
2263
+ return fields;
2264
+ }
2219
2265
  /** EventEmitter-like interface for subscriptions and chat streams. */
2220
2266
  class EventStream {
2221
2267
  constructor() {
@@ -2429,7 +2475,7 @@ class WebSocketClient {
2429
2475
  return this.connectPromise;
2430
2476
  }
2431
2477
  async openSocket() {
2432
- const WebSocket = (await Promise.resolve().then(() => __importStar(require("ws")))).default;
2478
+ const WebSocket = (await import("ws")).default;
2433
2479
  let url = this.wsURL;
2434
2480
  if (!url.endsWith("/api/ws")) {
2435
2481
  url += "/api/ws";
@@ -2761,9 +2807,12 @@ class WebSocketClient {
2761
2807
  const chatId = msg.payload?.chat_id || msg.payload?.chatId;
2762
2808
  const stream = this.chatStreams.get(chatId);
2763
2809
  if (stream) {
2810
+ // The text guard and the classification are the SSE route's,
2811
+ // so the two routes emit the same shape.
2764
2812
  stream.emit("event", {
2765
2813
  type: "error",
2766
- error: msg.payload.error || msg.payload.message || "Unknown error",
2814
+ error: streamErrorText(msg.payload),
2815
+ ...providerFailureFields(msg.payload),
2767
2816
  });
2768
2817
  this.chatStreams.delete(chatId);
2769
2818
  stream.close();