@ekodb/ekodb-client 0.12.0 → 0.14.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/dist/client.js CHANGED
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.WebSocketClient = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
39
+ exports.WebSocketClient = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
40
40
  const msgpack_1 = require("@msgpack/msgpack");
41
41
  const query_builder_1 = require("./query-builder");
42
42
  const schema_1 = require("./schema");
@@ -71,6 +71,7 @@ var MergeStrategy;
71
71
  class EkoDBClient {
72
72
  constructor(config, apiKey) {
73
73
  this.token = null;
74
+ this.tokenExpiry = 0;
74
75
  this.rateLimitInfo = null;
75
76
  // Support both old (baseURL, apiKey) and new (config object) signatures
76
77
  if (typeof config === "string") {
@@ -78,7 +79,6 @@ class EkoDBClient {
78
79
  this.apiKey = apiKey;
79
80
  this.shouldRetry = true;
80
81
  this.maxRetries = 3;
81
- this.timeout = 30000;
82
82
  this.format = SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
83
83
  }
84
84
  else {
@@ -86,7 +86,6 @@ class EkoDBClient {
86
86
  this.apiKey = config.apiKey;
87
87
  this.shouldRetry = config.shouldRetry ?? true;
88
88
  this.maxRetries = config.maxRetries ?? 3;
89
- this.timeout = config.timeout ?? 30000;
90
89
  this.format = config.format ?? SerializationFormat.MessagePack; // Default to MessagePack for 2-3x performance
91
90
  }
92
91
  }
@@ -135,6 +134,68 @@ class EkoDBClient {
135
134
  }
136
135
  const result = (await response.json());
137
136
  this.token = result.token;
137
+ // Extract and cache JWT expiry for proactive refresh
138
+ const expiry = this.extractJWTExpiry(result.token);
139
+ this.tokenExpiry = expiry ?? Math.floor(Date.now() / 1000) + 3600; // fallback: 1 hour
140
+ }
141
+ /**
142
+ * Get a valid authentication token.
143
+ *
144
+ * Returns a cached token if it has more than 60s of validity remaining.
145
+ * Otherwise fetches a new one via refreshToken(). This means callers
146
+ * never need to handle token refresh themselves — every getToken() call
147
+ * returns a token that's valid for at least 60 more seconds.
148
+ */
149
+ async getToken() {
150
+ if (this.token) {
151
+ const now = Math.floor(Date.now() / 1000);
152
+ if (now + 60 >= this.tokenExpiry) {
153
+ // Token is about to expire or already expired — refresh proactively
154
+ await this.refreshToken();
155
+ }
156
+ }
157
+ else {
158
+ // No token yet — fetch one
159
+ await this.refreshToken();
160
+ }
161
+ return this.token;
162
+ }
163
+ /**
164
+ * Clear the cached authentication token and expiry.
165
+ * The next request will trigger a fresh token exchange.
166
+ */
167
+ clearTokenCache() {
168
+ this.token = null;
169
+ this.tokenExpiry = 0;
170
+ }
171
+ /**
172
+ * Extract the `exp` claim from a JWT without verifying the signature.
173
+ * Returns the Unix timestamp (seconds) of expiry, or null if parsing fails.
174
+ */
175
+ extractJWTExpiry(token) {
176
+ try {
177
+ const parts = token.split(".");
178
+ if (parts.length !== 3) {
179
+ return null;
180
+ }
181
+ // Convert base64url to standard base64
182
+ let payload = parts[1];
183
+ payload = payload.replace(/-/g, "+").replace(/_/g, "/");
184
+ // Pad to multiple of 4
185
+ const pad = payload.length % 4;
186
+ if (pad) {
187
+ payload += "=".repeat(4 - pad);
188
+ }
189
+ const decoded = atob(payload);
190
+ const claims = JSON.parse(decoded);
191
+ if (typeof claims.exp === "number") {
192
+ return claims.exp;
193
+ }
194
+ return null;
195
+ }
196
+ catch {
197
+ return null;
198
+ }
138
199
  }
139
200
  /**
140
201
  * Extract rate limit information from response headers
@@ -326,6 +387,26 @@ class EkoDBClient {
326
387
  async findById(collection, id) {
327
388
  return this.makeRequest("GET", `/api/find/${collection}/${id}`);
328
389
  }
390
+ /**
391
+ * Find a document by ID with field projection
392
+ * @param collection - Collection name
393
+ * @param id - Document ID
394
+ * @param selectFields - Fields to include in the result
395
+ * @param excludeFields - Fields to exclude from the result
396
+ */
397
+ async findByIdWithProjection(collection, id, selectFields, excludeFields) {
398
+ const params = new URLSearchParams();
399
+ if (selectFields?.length) {
400
+ params.append("select_fields", selectFields.join(","));
401
+ }
402
+ if (excludeFields?.length) {
403
+ params.append("exclude_fields", excludeFields.join(","));
404
+ }
405
+ const url = params.toString()
406
+ ? `/api/find/${collection}/${id}?${params.toString()}`
407
+ : `/api/find/${collection}/${id}`;
408
+ return this.makeRequest("GET", url);
409
+ }
329
410
  /**
330
411
  * Update a document
331
412
  * @param collection - Collection name
@@ -346,6 +427,40 @@ class EkoDBClient {
346
427
  : `/api/update/${collection}/${id}`;
347
428
  return this.makeRequest("PUT", url, record);
348
429
  }
430
+ /**
431
+ * Apply an atomic field action to a single field of a record.
432
+ *
433
+ * Use this instead of `update()` for safe concurrent modifications like
434
+ * incrementing counters, pushing to arrays, or arithmetic operations.
435
+ *
436
+ * @param collection - Collection name
437
+ * @param id - Record ID
438
+ * @param action - The atomic action: increment, decrement, multiply, divide, modulo,
439
+ * push, pop, shift, unshift, remove, append, clear
440
+ * @param field - The field name to apply the action to
441
+ * @param value - The value for the action (omit for pop/shift/clear)
442
+ */
443
+ async updateWithAction(collection, id, action, field, value) {
444
+ const url = `/api/update/${collection}/${id}/action/${action}`;
445
+ return this.makeRequest("PUT", url, {
446
+ field,
447
+ value: value ?? null,
448
+ });
449
+ }
450
+ /**
451
+ * Apply a sequence of atomic field actions to a record in a single request.
452
+ *
453
+ * All actions are applied atomically — the record is fetched once, all actions
454
+ * run in order, and the result is persisted in a single update.
455
+ *
456
+ * @param collection - Collection name
457
+ * @param id - Record ID
458
+ * @param actions - Array of [action, field, value] tuples
459
+ */
460
+ async updateWithActionSequence(collection, id, actions) {
461
+ const url = `/api/update/sequence/${collection}/${id}`;
462
+ return this.makeRequest("PUT", url, actions);
463
+ }
349
464
  /**
350
465
  * Delete a document
351
466
  * @param collection - Collection name
@@ -772,6 +887,36 @@ class EkoDBClient {
772
887
  // Ensure all parameters from SearchQuery are sent to server
773
888
  return this.makeRequest("POST", `/api/search/${collection}`, query, 0, true);
774
889
  }
890
+ /**
891
+ * Get distinct (unique) values for a field across all records in a collection.
892
+ *
893
+ * Results are deduplicated and sorted alphabetically. Supports an optional filter
894
+ * to restrict which records are examined.
895
+ *
896
+ * @param collection - Collection name
897
+ * @param field - Field to get distinct values for
898
+ * @param options - Optional filter and bypass flags
899
+ *
900
+ * @example
901
+ * // All distinct statuses
902
+ * const resp = await client.distinctValues("orders", "status");
903
+ * console.log(resp.values); // ["active", "cancelled", "shipped"]
904
+ *
905
+ * // Only statuses for US orders
906
+ * const resp = await client.distinctValues("orders", "status", {
907
+ * filter: { type: "Condition", content: { field: "region", operator: "Eq", value: "us" } }
908
+ * });
909
+ */
910
+ async distinctValues(collection, field, options = {}) {
911
+ const body = {};
912
+ if (options.filter !== undefined)
913
+ body.filter = options.filter;
914
+ if (options.bypassRipple !== undefined)
915
+ body.bypass_ripple = options.bypassRipple;
916
+ if (options.bypassCache !== undefined)
917
+ body.bypass_cache = options.bypassCache;
918
+ return this.makeRequest("POST", `/api/distinct/${collection}/${field}`, body, 0, true);
919
+ }
775
920
  /**
776
921
  * Health check - verify the ekoDB server is responding
777
922
  */
@@ -791,12 +936,217 @@ class EkoDBClient {
791
936
  async createChatSession(request) {
792
937
  return this.makeRequest("POST", "/api/chat", request, 0, true);
793
938
  }
939
+ /**
940
+ * Stateless raw LLM completion — no session, no history, no RAG.
941
+ *
942
+ * Sends a system prompt and user message directly to the LLM via ekoDB
943
+ * and returns the raw text response without any context injection or
944
+ * conversation management. Use this for structured-output tasks such as
945
+ * planning where the response must be parsed programmatically.
946
+ *
947
+ * @example
948
+ * const resp = await client.rawCompletion({
949
+ * system_prompt: "You are a helpful assistant.",
950
+ * message: "Summarize this in JSON.",
951
+ * max_tokens: 2048,
952
+ * });
953
+ * console.log(resp.content);
954
+ */
955
+ async rawCompletion(request) {
956
+ return this.makeRequest("POST", "/api/chat/complete", request, 0, true);
957
+ }
958
+ /**
959
+ * Stateless raw LLM completion via SSE streaming.
960
+ *
961
+ * Same as rawCompletion() but uses Server-Sent Events to keep the
962
+ * connection alive. Preferred for deployed instances where reverse proxies
963
+ * may kill idle HTTP connections before the LLM responds.
964
+ */
965
+ async rawCompletionStream(request) {
966
+ let token = await this.getToken();
967
+ const url = `${this.baseURL}/api/chat/complete/stream`;
968
+ const response = await fetch(url, {
969
+ method: "POST",
970
+ headers: {
971
+ "Content-Type": "application/json",
972
+ Accept: "text/event-stream",
973
+ Authorization: `Bearer ${token}`,
974
+ },
975
+ body: JSON.stringify(request),
976
+ });
977
+ if (!response.ok) {
978
+ const body = await response.text();
979
+ throw new Error(`SSE raw completion failed (${response.status}): ${body}`);
980
+ }
981
+ const body = await response.text();
982
+ let content = "";
983
+ let lastError = null;
984
+ for (const line of body.split("\n")) {
985
+ if (line.startsWith("data:")) {
986
+ const dataStr = line.slice(5).trim();
987
+ if (!dataStr)
988
+ continue;
989
+ try {
990
+ const eventData = JSON.parse(dataStr);
991
+ if (eventData.token)
992
+ content += eventData.token;
993
+ if (eventData.content)
994
+ content = eventData.content;
995
+ if (eventData.error)
996
+ lastError = eventData.error;
997
+ }
998
+ catch {
999
+ // skip malformed SSE data
1000
+ }
1001
+ }
1002
+ }
1003
+ if (lastError)
1004
+ throw new Error(lastError);
1005
+ return { content };
1006
+ }
1007
+ /**
1008
+ * Stateless raw LLM completion via SSE streaming with token-level progress.
1009
+ *
1010
+ * Same as rawCompletionStream() but invokes `onToken` with each token as it
1011
+ * arrives, allowing callers to show real-time progress.
1012
+ */
1013
+ async rawCompletionStreamWithProgress(request, onToken) {
1014
+ let token = await this.getToken();
1015
+ const url = `${this.baseURL}/api/chat/complete/stream`;
1016
+ const response = await fetch(url, {
1017
+ method: "POST",
1018
+ headers: {
1019
+ "Content-Type": "application/json",
1020
+ Accept: "text/event-stream",
1021
+ Authorization: `Bearer ${token}`,
1022
+ },
1023
+ body: JSON.stringify(request),
1024
+ });
1025
+ if (!response.ok) {
1026
+ const body = await response.text();
1027
+ throw new Error(`SSE raw completion failed (${response.status}): ${body}`);
1028
+ }
1029
+ const body = await response.text();
1030
+ let content = "";
1031
+ let lastError = null;
1032
+ for (const line of body.split("\n")) {
1033
+ if (line.startsWith("data:")) {
1034
+ const dataStr = line.slice(5).trim();
1035
+ if (!dataStr)
1036
+ continue;
1037
+ try {
1038
+ const eventData = JSON.parse(dataStr);
1039
+ if (eventData.token) {
1040
+ content += eventData.token;
1041
+ onToken(eventData.token);
1042
+ }
1043
+ if (eventData.content)
1044
+ content = eventData.content;
1045
+ if (eventData.error)
1046
+ lastError = eventData.error;
1047
+ }
1048
+ catch {
1049
+ // skip malformed SSE data
1050
+ }
1051
+ }
1052
+ }
1053
+ if (lastError)
1054
+ throw new Error(lastError);
1055
+ return { content };
1056
+ }
794
1057
  /**
795
1058
  * Send a message in an existing chat session
796
1059
  */
797
1060
  async chatMessage(sessionId, request) {
798
1061
  return this.makeRequest("POST", `/api/chat/${sessionId}/messages`, request, 0, true);
799
1062
  }
1063
+ /**
1064
+ * Send a message in an existing chat session via SSE streaming.
1065
+ *
1066
+ * Returns an EventStream that emits ChatStreamEvent objects as they arrive:
1067
+ * - `{ type: "chunk", content: "..." }` for each token
1068
+ * - `{ type: "end", messageId, executionTimeMs, tokenUsage?, contextWindow? }` when complete
1069
+ * - `{ type: "error", error: "..." }` on failure
1070
+ *
1071
+ * Preferred over chatMessage() for long-running responses where reverse
1072
+ * proxies may kill idle HTTP connections before the LLM responds.
1073
+ */
1074
+ chatMessageStream(chatId, request) {
1075
+ const stream = new EventStream();
1076
+ (async () => {
1077
+ try {
1078
+ let token = this.getToken();
1079
+ if (!token) {
1080
+ await this.refreshToken();
1081
+ token = this.getToken();
1082
+ }
1083
+ const url = `${this.baseURL}/api/chat/${chatId}/messages/stream`;
1084
+ const response = await fetch(url, {
1085
+ method: "POST",
1086
+ headers: {
1087
+ "Content-Type": "application/json",
1088
+ Accept: "text/event-stream",
1089
+ Authorization: `Bearer ${token}`,
1090
+ },
1091
+ body: JSON.stringify(request),
1092
+ });
1093
+ if (!response.ok) {
1094
+ const body = await response.text();
1095
+ stream.emit("event", {
1096
+ type: "error",
1097
+ error: `SSE chat message stream failed (${response.status}): ${body}`,
1098
+ });
1099
+ stream.close();
1100
+ return;
1101
+ }
1102
+ const body = await response.text();
1103
+ for (const line of body.split("\n")) {
1104
+ if (!line.startsWith("data:"))
1105
+ continue;
1106
+ const dataStr = line.slice(5).trim();
1107
+ if (!dataStr)
1108
+ continue;
1109
+ try {
1110
+ const eventData = JSON.parse(dataStr);
1111
+ if (eventData.error) {
1112
+ stream.emit("event", {
1113
+ type: "error",
1114
+ error: eventData.error,
1115
+ });
1116
+ }
1117
+ else if (eventData.content && eventData.message_id) {
1118
+ // Done event — has full content + message_id
1119
+ stream.emit("event", {
1120
+ type: "end",
1121
+ messageId: eventData.message_id,
1122
+ executionTimeMs: eventData.execution_time_ms ?? 0,
1123
+ tokenUsage: eventData.token_usage,
1124
+ contextWindow: eventData.context_window,
1125
+ });
1126
+ }
1127
+ else if (eventData.token) {
1128
+ stream.emit("event", {
1129
+ type: "chunk",
1130
+ content: eventData.token,
1131
+ });
1132
+ }
1133
+ }
1134
+ catch {
1135
+ // skip malformed SSE data
1136
+ }
1137
+ }
1138
+ stream.close();
1139
+ }
1140
+ catch (err) {
1141
+ stream.emit("event", {
1142
+ type: "error",
1143
+ error: err.message ?? String(err),
1144
+ });
1145
+ stream.close();
1146
+ }
1147
+ })();
1148
+ return stream;
1149
+ }
800
1150
  /**
801
1151
  * Get a chat session by ID
802
1152
  */
@@ -890,6 +1240,14 @@ class EkoDBClient {
890
1240
  async getChatModels() {
891
1241
  return this.makeRequest("GET", "/api/chat_models", undefined, 0, true);
892
1242
  }
1243
+ /**
1244
+ * Get all built-in server-side chat tool definitions.
1245
+ * Returns a list of tool objects with name, description, and parameters fields.
1246
+ * Used by planning agents to discover available tools dynamically.
1247
+ */
1248
+ async getChatTools() {
1249
+ return this.makeRequest("GET", "/api/chat/tools", undefined, 0, true);
1250
+ }
893
1251
  /**
894
1252
  * Get available models for a specific provider
895
1253
  * @param provider - Provider name (e.g., "openai", "anthropic", "perplexity")
@@ -993,6 +1351,204 @@ class EkoDBClient {
993
1351
  await this.makeRequest("DELETE", `/api/functions/${encodeURIComponent(label)}`, undefined, 0, true);
994
1352
  }
995
1353
  // ========================================================================
1354
+ // GOAL API
1355
+ // ========================================================================
1356
+ /** Create a new goal */
1357
+ async goalCreate(data) {
1358
+ return this.makeRequest("POST", "/api/chat/goals", data, 0, true);
1359
+ }
1360
+ /** List all goals */
1361
+ async goalList() {
1362
+ return this.makeRequest("GET", "/api/chat/goals", undefined, 0, true);
1363
+ }
1364
+ /** Get a goal by ID */
1365
+ async goalGet(id) {
1366
+ return this.makeRequest("GET", `/api/chat/goals/${encodeURIComponent(id)}`, undefined, 0, true);
1367
+ }
1368
+ /** Update a goal by ID */
1369
+ async goalUpdate(id, data) {
1370
+ return this.makeRequest("PUT", `/api/chat/goals/${encodeURIComponent(id)}`, data, 0, true);
1371
+ }
1372
+ /** Delete a goal by ID */
1373
+ async goalDelete(id) {
1374
+ await this.makeRequest("DELETE", `/api/chat/goals/${encodeURIComponent(id)}`, undefined, 0, true);
1375
+ }
1376
+ // ── Goal Templates ──
1377
+ /** Create a new goal template */
1378
+ async goalTemplateCreate(data) {
1379
+ return this.makeRequest("POST", "/api/chat/goal-templates", data, 0, true);
1380
+ }
1381
+ /** List all goal templates */
1382
+ async goalTemplateList() {
1383
+ return this.makeRequest("GET", "/api/chat/goal-templates", undefined, 0, true);
1384
+ }
1385
+ /** Get a goal template by ID */
1386
+ async goalTemplateGet(id) {
1387
+ return this.makeRequest("GET", `/api/chat/goal-templates/${encodeURIComponent(id)}`, undefined, 0, true);
1388
+ }
1389
+ /** Update a goal template by ID */
1390
+ async goalTemplateUpdate(id, data) {
1391
+ return this.makeRequest("PUT", `/api/chat/goal-templates/${encodeURIComponent(id)}`, data, 0, true);
1392
+ }
1393
+ /** Delete a goal template by ID */
1394
+ async goalTemplateDelete(id) {
1395
+ await this.makeRequest("DELETE", `/api/chat/goal-templates/${encodeURIComponent(id)}`, undefined, 0, true);
1396
+ }
1397
+ /** Search goals */
1398
+ async goalSearch(query) {
1399
+ const params = new URLSearchParams({ q: query });
1400
+ return this.makeRequest("GET", `/api/chat/goals/search?${params}`, undefined, 0, true);
1401
+ }
1402
+ /** Mark a goal as complete (status -> pending_review) */
1403
+ async goalComplete(id, data) {
1404
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/complete`, data, 0, true);
1405
+ }
1406
+ /** Approve a goal (status -> in_progress) */
1407
+ async goalApprove(id) {
1408
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/approve`, undefined, 0, true);
1409
+ }
1410
+ /** Reject a goal (status -> failed) */
1411
+ async goalReject(id, data) {
1412
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/reject`, data, 0, true);
1413
+ }
1414
+ /** Start a goal step (status -> in_progress) */
1415
+ async goalStepStart(id, stepIndex) {
1416
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/start`, undefined, 0, true);
1417
+ }
1418
+ /** Complete a goal step with result */
1419
+ async goalStepComplete(id, stepIndex, data) {
1420
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/complete`, data, 0, true);
1421
+ }
1422
+ /** Fail a goal step with error */
1423
+ async goalStepFail(id, stepIndex, data) {
1424
+ return this.makeRequest("POST", `/api/chat/goals/${encodeURIComponent(id)}/steps/${stepIndex}/fail`, data, 0, true);
1425
+ }
1426
+ // ========================================================================
1427
+ // TASK API
1428
+ // ========================================================================
1429
+ /** Create a new scheduled task */
1430
+ async taskCreate(data) {
1431
+ return this.makeRequest("POST", "/api/chat/tasks", data, 0, true);
1432
+ }
1433
+ /** List all scheduled tasks */
1434
+ async taskList() {
1435
+ return this.makeRequest("GET", "/api/chat/tasks", undefined, 0, true);
1436
+ }
1437
+ /** Get a task by ID */
1438
+ async taskGet(id) {
1439
+ return this.makeRequest("GET", `/api/chat/tasks/${encodeURIComponent(id)}`, undefined, 0, true);
1440
+ }
1441
+ /** Update a task by ID */
1442
+ async taskUpdate(id, data) {
1443
+ return this.makeRequest("PUT", `/api/chat/tasks/${encodeURIComponent(id)}`, data, 0, true);
1444
+ }
1445
+ /** Delete a task by ID */
1446
+ async taskDelete(id) {
1447
+ await this.makeRequest("DELETE", `/api/chat/tasks/${encodeURIComponent(id)}`, undefined, 0, true);
1448
+ }
1449
+ /** Get tasks that are due at the given time */
1450
+ async taskDue(now) {
1451
+ const params = new URLSearchParams({ now });
1452
+ return this.makeRequest("GET", `/api/chat/tasks/due?${params}`, undefined, 0, true);
1453
+ }
1454
+ /** Start a task (status -> running) */
1455
+ async taskStart(id) {
1456
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/start`, undefined, 0, true);
1457
+ }
1458
+ /** Mark a task as succeeded */
1459
+ async taskSucceed(id, data) {
1460
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/succeed`, data, 0, true);
1461
+ }
1462
+ /** Mark a task as failed */
1463
+ async taskFail(id, data) {
1464
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/fail`, data, 0, true);
1465
+ }
1466
+ /** Pause a task */
1467
+ async taskPause(id) {
1468
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/pause`, undefined, 0, true);
1469
+ }
1470
+ /** Resume a paused task */
1471
+ async taskResume(id, data) {
1472
+ return this.makeRequest("POST", `/api/chat/tasks/${encodeURIComponent(id)}/resume`, data, 0, true);
1473
+ }
1474
+ // ========================================================================
1475
+ // AGENT API
1476
+ // ========================================================================
1477
+ /** Create a new agent */
1478
+ async agentCreate(data) {
1479
+ return this.makeRequest("POST", "/api/chat/agents", data, 0, true);
1480
+ }
1481
+ /** List all agents */
1482
+ async agentList() {
1483
+ return this.makeRequest("GET", "/api/chat/agents", undefined, 0, true);
1484
+ }
1485
+ /** Get an agent by ID */
1486
+ async agentGet(id) {
1487
+ return this.makeRequest("GET", `/api/chat/agents/${encodeURIComponent(id)}`, undefined, 0, true);
1488
+ }
1489
+ /** Get an agent by name */
1490
+ async agentGetByName(name) {
1491
+ return this.makeRequest("GET", `/api/chat/agents/by-name/${encodeURIComponent(name)}`, undefined, 0, true);
1492
+ }
1493
+ /** Update an agent by ID */
1494
+ async agentUpdate(id, data) {
1495
+ return this.makeRequest("PUT", `/api/chat/agents/${encodeURIComponent(id)}`, data, 0, true);
1496
+ }
1497
+ /** Delete an agent by ID */
1498
+ async agentDelete(id) {
1499
+ await this.makeRequest("DELETE", `/api/chat/agents/${encodeURIComponent(id)}`, undefined, 0, true);
1500
+ }
1501
+ /** Get agents by deployment ID */
1502
+ async agentsByDeployment(deploymentId) {
1503
+ return this.makeRequest("GET", `/api/chat/agents/by-deployment/${encodeURIComponent(deploymentId)}`, undefined, 0, true);
1504
+ }
1505
+ // ========================================================================
1506
+ // KV DOCUMENT LINKING
1507
+ // ========================================================================
1508
+ /** Get documents linked to a KV key */
1509
+ async kvGetLinks(key) {
1510
+ return this.makeRequest("GET", `/api/kv/links/${encodeURIComponent(key)}`, undefined, 0, true);
1511
+ }
1512
+ /** Link a document to a KV key */
1513
+ async kvLink(key, collection, documentId) {
1514
+ return this.makeRequest("POST", `/api/kv/link`, { key, collection, document_id: documentId }, 0, true);
1515
+ }
1516
+ /** Unlink a document from a KV key */
1517
+ async kvUnlink(key, collection, documentId) {
1518
+ return this.makeRequest("POST", `/api/kv/unlink`, { key, collection, document_id: documentId }, 0, true);
1519
+ }
1520
+ // ========================================================================
1521
+ // SCHEDULE MANAGEMENT
1522
+ // ========================================================================
1523
+ /** Create a new schedule */
1524
+ async createSchedule(data) {
1525
+ return this.makeRequest("POST", `/api/schedules`, data, 0, true);
1526
+ }
1527
+ /** List all schedules */
1528
+ async listSchedules() {
1529
+ return this.makeRequest("GET", `/api/schedules`, undefined, 0, true);
1530
+ }
1531
+ /** Get a schedule by ID */
1532
+ async getSchedule(id) {
1533
+ return this.makeRequest("GET", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
1534
+ }
1535
+ /** Update a schedule */
1536
+ async updateSchedule(id, data) {
1537
+ return this.makeRequest("PUT", `/api/schedules/${encodeURIComponent(id)}`, data, 0, true);
1538
+ }
1539
+ /** Delete a schedule */
1540
+ async deleteSchedule(id) {
1541
+ await this.makeRequest("DELETE", `/api/schedules/${encodeURIComponent(id)}`, undefined, 0, true);
1542
+ }
1543
+ /** Pause a schedule */
1544
+ async pauseSchedule(id) {
1545
+ return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/pause`, undefined, 0, true);
1546
+ }
1547
+ /** Resume a schedule */
1548
+ async resumeSchedule(id) {
1549
+ return this.makeRequest("POST", `/api/schedules/${encodeURIComponent(id)}/resume`, undefined, 0, true);
1550
+ }
1551
+ // ========================================================================
996
1552
  // COLLECTION UTILITIES
997
1553
  // ========================================================================
998
1554
  /**
@@ -1150,22 +1706,64 @@ class EkoDBClient {
1150
1706
  }
1151
1707
  }
1152
1708
  exports.EkoDBClient = EkoDBClient;
1709
+ /** EventEmitter-like interface for subscriptions and chat streams. */
1710
+ class EventStream {
1711
+ constructor() {
1712
+ this.listeners = new Map();
1713
+ this._closed = false;
1714
+ }
1715
+ on(event, listener) {
1716
+ if (!this.listeners.has(event)) {
1717
+ this.listeners.set(event, []);
1718
+ }
1719
+ this.listeners.get(event).push(listener);
1720
+ return this;
1721
+ }
1722
+ /** @internal */
1723
+ emit(event, data) {
1724
+ const handlers = this.listeners.get(event);
1725
+ if (handlers) {
1726
+ for (const handler of handlers) {
1727
+ handler(data);
1728
+ }
1729
+ }
1730
+ }
1731
+ get closed() {
1732
+ return this._closed;
1733
+ }
1734
+ /** @internal */
1735
+ close() {
1736
+ this._closed = true;
1737
+ this.emit("close");
1738
+ }
1739
+ }
1740
+ exports.EventStream = EventStream;
1153
1741
  /**
1154
- * WebSocket client for real-time queries
1742
+ * WebSocket client for real-time queries, subscriptions, and chat streaming.
1155
1743
  */
1156
1744
  class WebSocketClient {
1157
1745
  constructor(wsURL, token) {
1158
1746
  this.ws = null;
1747
+ this.dispatcherRunning = false;
1748
+ // Dispatcher state
1749
+ this.pendingRequests = new Map();
1750
+ this.subscriptions = new Map();
1751
+ this.chatStreams = new Map();
1752
+ this.registerToolsAck = null;
1753
+ this.messageCounter = 0;
1159
1754
  this.wsURL = wsURL;
1160
1755
  this.token = token;
1161
1756
  }
1757
+ genMessageId() {
1758
+ const counter = this.messageCounter++;
1759
+ return `${Date.now()}-${counter}-${Math.random().toString(36).slice(2, 8)}`;
1760
+ }
1162
1761
  /**
1163
- * Connect to WebSocket
1762
+ * Connect and start the dispatcher.
1164
1763
  */
1165
- async connect() {
1166
- if (this.ws)
1764
+ async ensureConnected() {
1765
+ if (this.ws && this.dispatcherRunning)
1167
1766
  return;
1168
- // Dynamic import for Node.js WebSocket
1169
1767
  const WebSocket = (await Promise.resolve().then(() => __importStar(require("ws")))).default;
1170
1768
  let url = this.wsURL;
1171
1769
  if (!url.endsWith("/api/ws")) {
@@ -1176,43 +1774,323 @@ class WebSocketClient {
1176
1774
  Authorization: `Bearer ${this.token}`,
1177
1775
  },
1178
1776
  });
1179
- return new Promise((resolve, reject) => {
1777
+ await new Promise((resolve, reject) => {
1180
1778
  this.ws.on("open", () => resolve());
1181
1779
  this.ws.on("error", (err) => reject(err));
1182
1780
  });
1781
+ this.spawnDispatcher();
1782
+ }
1783
+ spawnDispatcher() {
1784
+ if (this.dispatcherRunning)
1785
+ return;
1786
+ this.dispatcherRunning = true;
1787
+ this.ws.on("message", (data) => {
1788
+ try {
1789
+ const msg = JSON.parse(data.toString());
1790
+ this.routeMessage(msg);
1791
+ }
1792
+ catch {
1793
+ // Ignore malformed messages
1794
+ }
1795
+ });
1796
+ this.ws.on("close", () => {
1797
+ this.dispatcherRunning = false;
1798
+ // Notify all pending requests
1799
+ for (const [, pending] of this.pendingRequests) {
1800
+ pending.reject(new Error("WebSocket connection closed"));
1801
+ }
1802
+ this.pendingRequests.clear();
1803
+ // Close all chat streams
1804
+ for (const [, stream] of this.chatStreams) {
1805
+ stream.emit("event", { type: "error", error: "Connection closed" });
1806
+ stream.close();
1807
+ }
1808
+ this.chatStreams.clear();
1809
+ // Close all subscriptions
1810
+ for (const [, stream] of this.subscriptions) {
1811
+ stream.close();
1812
+ }
1813
+ this.subscriptions.clear();
1814
+ this.ws = null;
1815
+ });
1816
+ }
1817
+ routeMessage(msg) {
1818
+ switch (msg.type) {
1819
+ case "Success":
1820
+ case "Error": {
1821
+ // Try messageId from top-level, then from payload
1822
+ const messageId = msg.messageId ||
1823
+ msg.message_id ||
1824
+ msg.payload?.message_id ||
1825
+ msg.payload?.messageId;
1826
+ let matched = false;
1827
+ if (messageId && this.pendingRequests.has(messageId)) {
1828
+ const pending = this.pendingRequests.get(messageId);
1829
+ this.pendingRequests.delete(messageId);
1830
+ if (msg.type === "Error") {
1831
+ pending.reject(new Error(msg.message || "Unknown error"));
1832
+ }
1833
+ else {
1834
+ pending.resolve(msg.payload);
1835
+ }
1836
+ matched = true;
1837
+ }
1838
+ if (!matched && this.registerToolsAck) {
1839
+ const ack = this.registerToolsAck;
1840
+ this.registerToolsAck = null;
1841
+ if (msg.type === "Error") {
1842
+ ack.reject(new Error(msg.message || "Tool registration failed"));
1843
+ }
1844
+ else {
1845
+ ack.resolve(msg.payload);
1846
+ }
1847
+ matched = true;
1848
+ }
1849
+ // Server doesn't echo messageId — if there's exactly one pending
1850
+ // request, deliver the response to it (sequential request/response).
1851
+ if (!matched && this.pendingRequests.size === 1) {
1852
+ const entry = this.pendingRequests.entries().next().value;
1853
+ const key = entry[0];
1854
+ const pending = entry[1];
1855
+ this.pendingRequests.delete(key);
1856
+ if (msg.type === "Error") {
1857
+ pending.reject(new Error(msg.message || "Unknown error"));
1858
+ }
1859
+ else {
1860
+ pending.resolve(msg.payload);
1861
+ }
1862
+ }
1863
+ break;
1864
+ }
1865
+ case "MutationNotification": {
1866
+ const payload = msg.payload;
1867
+ const notification = {
1868
+ collection: payload.collection,
1869
+ event: payload.event,
1870
+ recordIds: payload.record_ids || payload.recordIds || [],
1871
+ records: payload.records,
1872
+ timestamp: payload.timestamp,
1873
+ };
1874
+ for (const [collection, stream] of this.subscriptions) {
1875
+ if (collection === notification.collection) {
1876
+ stream.emit("mutation", notification);
1877
+ }
1878
+ }
1879
+ break;
1880
+ }
1881
+ case "ChatStreamChunk": {
1882
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1883
+ const stream = this.chatStreams.get(chatId);
1884
+ if (stream) {
1885
+ stream.emit("event", {
1886
+ type: "chunk",
1887
+ content: msg.payload.content,
1888
+ });
1889
+ }
1890
+ break;
1891
+ }
1892
+ case "ChatStreamEnd": {
1893
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1894
+ const stream = this.chatStreams.get(chatId);
1895
+ if (stream) {
1896
+ stream.emit("event", {
1897
+ type: "end",
1898
+ messageId: msg.payload.message_id || msg.payload.messageId || "",
1899
+ tokenUsage: msg.payload.token_usage || msg.payload.tokenUsage,
1900
+ toolCallHistory: msg.payload.tool_call_history || msg.payload.toolCallHistory,
1901
+ executionTimeMs: msg.payload.execution_time_ms || msg.payload.executionTimeMs || 0,
1902
+ contextWindow: msg.payload.context_window || msg.payload.contextWindow,
1903
+ });
1904
+ this.chatStreams.delete(chatId);
1905
+ stream.close();
1906
+ }
1907
+ break;
1908
+ }
1909
+ case "ChatStreamError": {
1910
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1911
+ const stream = this.chatStreams.get(chatId);
1912
+ if (stream) {
1913
+ stream.emit("event", {
1914
+ type: "error",
1915
+ error: msg.payload.error || msg.payload.message || "Unknown error",
1916
+ });
1917
+ this.chatStreams.delete(chatId);
1918
+ stream.close();
1919
+ }
1920
+ break;
1921
+ }
1922
+ case "ClientToolCall": {
1923
+ const chatId = msg.payload?.chat_id || msg.payload?.chatId;
1924
+ const stream = this.chatStreams.get(chatId);
1925
+ if (stream) {
1926
+ stream.emit("event", {
1927
+ type: "toolCall",
1928
+ chatId,
1929
+ callId: msg.payload.call_id || msg.payload.callId,
1930
+ toolName: msg.payload.tool_name || msg.payload.toolName,
1931
+ arguments: msg.payload.arguments,
1932
+ });
1933
+ }
1934
+ break;
1935
+ }
1936
+ }
1937
+ }
1938
+ async sendRequest(request) {
1939
+ await this.ensureConnected();
1940
+ const messageId = request.messageId || request.message_id;
1941
+ return new Promise((resolve, reject) => {
1942
+ this.pendingRequests.set(messageId, { resolve, reject });
1943
+ try {
1944
+ this.ws.send(JSON.stringify(request));
1945
+ }
1946
+ catch (err) {
1947
+ this.pendingRequests.delete(messageId);
1948
+ reject(err);
1949
+ }
1950
+ });
1183
1951
  }
1184
1952
  /**
1185
- * Find all records in a collection via WebSocket
1953
+ * Find all records in a collection via WebSocket.
1186
1954
  */
1187
1955
  async findAll(collection) {
1188
- await this.connect();
1189
- const messageId = Date.now().toString();
1190
- const request = {
1956
+ const messageId = this.genMessageId();
1957
+ const payload = await this.sendRequest({
1191
1958
  type: "FindAll",
1192
1959
  messageId,
1193
1960
  payload: { collection },
1961
+ });
1962
+ return payload?.data || [];
1963
+ }
1964
+ /**
1965
+ * Subscribe to mutation notifications on a collection.
1966
+ * Returns an EventStream that emits "mutation" events.
1967
+ */
1968
+ async subscribe(collection, options) {
1969
+ await this.ensureConnected();
1970
+ if (this.subscriptions.has(collection)) {
1971
+ throw new Error(`Already subscribed to collection "${collection}"`);
1972
+ }
1973
+ const messageId = this.genMessageId();
1974
+ const stream = new EventStream();
1975
+ this.subscriptions.set(collection, stream);
1976
+ const request = {
1977
+ type: "Subscribe",
1978
+ messageId,
1979
+ payload: {
1980
+ collection,
1981
+ ...(options?.filterField && { filter_field: options.filterField }),
1982
+ ...(options?.filterValue && { filter_value: options.filterValue }),
1983
+ },
1194
1984
  };
1195
- return new Promise((resolve, reject) => {
1985
+ // Send subscribe request and wait for ack
1986
+ try {
1987
+ await this.sendRequest(request);
1988
+ }
1989
+ catch (err) {
1990
+ this.subscriptions.delete(collection);
1991
+ throw err;
1992
+ }
1993
+ return stream;
1994
+ }
1995
+ /**
1996
+ * Send a chat message and receive a streaming response.
1997
+ * Returns an EventStream that emits "event" with ChatStreamEvent objects.
1998
+ */
1999
+ async chatSend(chatId, message, options) {
2000
+ await this.ensureConnected();
2001
+ if (this.chatStreams.has(chatId)) {
2002
+ throw new Error(`Chat stream already active for chatId "${chatId}"`);
2003
+ }
2004
+ const stream = new EventStream();
2005
+ this.chatStreams.set(chatId, stream);
2006
+ const request = {
2007
+ type: "ChatSend",
2008
+ payload: {
2009
+ chat_id: chatId,
2010
+ message,
2011
+ ...(options?.bypassRipple != null && {
2012
+ bypass_ripple: options.bypassRipple,
2013
+ }),
2014
+ ...(options?.clientTools && { client_tools: options.clientTools }),
2015
+ ...(options?.maxIterations != null && {
2016
+ max_iterations: options.maxIterations,
2017
+ }),
2018
+ ...(options?.confirmTools && { confirm_tools: options.confirmTools }),
2019
+ ...(options?.excludeTools && { exclude_tools: options.excludeTools }),
2020
+ },
2021
+ };
2022
+ this.ws.send(JSON.stringify(request));
2023
+ return stream;
2024
+ }
2025
+ /**
2026
+ * Register client-side tools for a chat session.
2027
+ */
2028
+ async registerClientTools(chatId, tools) {
2029
+ await this.ensureConnected();
2030
+ const request = {
2031
+ type: "RegisterClientTools",
2032
+ payload: {
2033
+ chat_id: chatId,
2034
+ tools,
2035
+ },
2036
+ };
2037
+ await new Promise((resolve, reject) => {
2038
+ this.registerToolsAck = {
2039
+ resolve: () => resolve(),
2040
+ reject: (err) => reject(err),
2041
+ };
1196
2042
  this.ws.send(JSON.stringify(request));
1197
- this.ws.once("message", (data) => {
1198
- const response = JSON.parse(data.toString());
1199
- if (response.type === "Error") {
1200
- reject(new Error(response.message));
1201
- }
1202
- else {
1203
- resolve(response.payload?.data || []);
1204
- }
1205
- });
1206
- this.ws.once("error", reject);
1207
2043
  });
1208
2044
  }
1209
2045
  /**
1210
- * Close the WebSocket connection
2046
+ * Send a tool result back to the server during a chat stream.
2047
+ */
2048
+ async sendToolResult(chatId, callId, success, result, error) {
2049
+ await this.ensureConnected();
2050
+ const request = {
2051
+ type: "ClientToolResult",
2052
+ payload: {
2053
+ chat_id: chatId,
2054
+ call_id: callId,
2055
+ success,
2056
+ ...(result !== undefined && { result }),
2057
+ ...(error !== undefined && { error }),
2058
+ },
2059
+ };
2060
+ this.ws.send(JSON.stringify(request));
2061
+ }
2062
+ /**
2063
+ * Stateless raw LLM completion via WebSocket.
2064
+ *
2065
+ * Sends a RawComplete message and waits for the Success response.
2066
+ * Preferred over HTTP for deployed instances: the persistent WSS
2067
+ * connection is already authenticated and won't be killed by reverse
2068
+ * proxy timeouts.
2069
+ */
2070
+ async rawCompletion(request) {
2071
+ await this.ensureConnected();
2072
+ const messageId = this.genMessageId();
2073
+ const payload = await this.sendRequest({
2074
+ type: "RawComplete",
2075
+ messageId,
2076
+ payload: {
2077
+ system_prompt: request.system_prompt,
2078
+ message: request.message,
2079
+ ...(request.provider && { provider: request.provider }),
2080
+ ...(request.model && { model: request.model }),
2081
+ ...(request.max_tokens != null && { max_tokens: request.max_tokens }),
2082
+ },
2083
+ });
2084
+ return { content: payload?.data?.content || "" };
2085
+ }
2086
+ /**
2087
+ * Close the WebSocket connection.
1211
2088
  */
1212
2089
  close() {
1213
2090
  if (this.ws) {
1214
2091
  this.ws.close();
1215
2092
  this.ws = null;
2093
+ this.dispatcherRunning = false;
1216
2094
  }
1217
2095
  }
1218
2096
  }