@crowkis/client 0.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/index.js ADDED
@@ -0,0 +1,961 @@
1
+ "use strict";
2
+
3
+ const net = require("node:net");
4
+
5
+ class CrowkisError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "CrowkisError";
9
+ }
10
+ }
11
+
12
+ function toBuffer(value) {
13
+ if (Buffer.isBuffer(value)) return value;
14
+ if (value instanceof Uint8Array) return Buffer.from(value);
15
+ return Buffer.from(String(value));
16
+ }
17
+
18
+ function imageBase64(value) {
19
+ if (value === undefined || value === null) return null;
20
+ return toBuffer(value).toString("base64");
21
+ }
22
+
23
+ function bufferToString(value) {
24
+ if (value === undefined || value === null) return null;
25
+ return Buffer.isBuffer(value) ? value.toString() : String(value);
26
+ }
27
+
28
+ function encode(args) {
29
+ const parts = [Buffer.from(`*${args.length}\r\n`)];
30
+ for (const arg of args.map(toBuffer)) {
31
+ parts.push(Buffer.from(`$${arg.length}\r\n`), arg, Buffer.from("\r\n"));
32
+ }
33
+ return Buffer.concat(parts);
34
+ }
35
+
36
+ class RespReader {
37
+ constructor(socket) {
38
+ this.socket = socket;
39
+ this.buffer = Buffer.alloc(0);
40
+ this.waiters = [];
41
+ socket.on("data", (chunk) => {
42
+ this.buffer = Buffer.concat([this.buffer, chunk]);
43
+ this._drain();
44
+ });
45
+ socket.on("error", (error) => this._reject(error));
46
+ socket.on("close", () => this._reject(new CrowkisError("connection closed")));
47
+ }
48
+
49
+ async read() {
50
+ return this._parseOrWait();
51
+ }
52
+
53
+ _parseOrWait() {
54
+ const parsed = this._parseValue(0);
55
+ if (parsed) {
56
+ this.buffer = this.buffer.subarray(parsed.offset);
57
+ return Promise.resolve(parsed.value);
58
+ }
59
+ return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
60
+ }
61
+
62
+ _drain() {
63
+ while (this.waiters.length > 0) {
64
+ const parsed = this._parseValue(0);
65
+ if (!parsed) return;
66
+ this.buffer = this.buffer.subarray(parsed.offset);
67
+ const waiter = this.waiters.shift();
68
+ waiter.resolve(parsed.value);
69
+ }
70
+ }
71
+
72
+ _reject(error) {
73
+ while (this.waiters.length > 0) {
74
+ this.waiters.shift().reject(error);
75
+ }
76
+ }
77
+
78
+ _line(offset) {
79
+ const end = this.buffer.indexOf("\r\n", offset);
80
+ if (end < 0) return null;
81
+ return {
82
+ line: this.buffer.subarray(offset, end),
83
+ offset: end + 2,
84
+ };
85
+ }
86
+
87
+ _parseValue(offset) {
88
+ const first = this.buffer[offset];
89
+ if (first === undefined) return null;
90
+ const line = this._line(offset);
91
+ if (!line) return null;
92
+ const payload = line.line.subarray(1);
93
+ const text = payload.toString();
94
+
95
+ switch (String.fromCharCode(first)) {
96
+ case "+":
97
+ return { value: text, offset: line.offset };
98
+ case "-":
99
+ throw new CrowkisError(text);
100
+ case ":":
101
+ return { value: Number.parseInt(text, 10), offset: line.offset };
102
+ case ",":
103
+ return { value: Number.parseFloat(text), offset: line.offset };
104
+ case "_":
105
+ return { value: null, offset: line.offset };
106
+ case "#":
107
+ return { value: text === "t", offset: line.offset };
108
+ case "$": {
109
+ const len = Number.parseInt(text, 10);
110
+ if (len < 0) return { value: null, offset: line.offset };
111
+ const end = line.offset + len + 2;
112
+ if (this.buffer.length < end) return null;
113
+ return {
114
+ value: this.buffer.subarray(line.offset, line.offset + len),
115
+ offset: end,
116
+ };
117
+ }
118
+ case "*": {
119
+ const count = Number.parseInt(text, 10);
120
+ if (count < 0) return { value: null, offset: line.offset };
121
+ const items = [];
122
+ let cursor = line.offset;
123
+ for (let i = 0; i < count; i += 1) {
124
+ const parsed = this._parseValue(cursor);
125
+ if (!parsed) return null;
126
+ items.push(parsed.value);
127
+ cursor = parsed.offset;
128
+ }
129
+ return { value: items, offset: cursor };
130
+ }
131
+ case "%": {
132
+ const count = Number.parseInt(text, 10);
133
+ const object = {};
134
+ let cursor = line.offset;
135
+ for (let i = 0; i < count; i += 1) {
136
+ const key = this._parseValue(cursor);
137
+ if (!key) return null;
138
+ const value = this._parseValue(key.offset);
139
+ if (!value) return null;
140
+ const name = Buffer.isBuffer(key.value) ? key.value.toString() : String(key.value);
141
+ object[name] = value.value;
142
+ cursor = value.offset;
143
+ }
144
+ return { value: object, offset: cursor };
145
+ }
146
+ case ">": {
147
+ const count = Number.parseInt(text, 10);
148
+ const items = [];
149
+ let cursor = line.offset;
150
+ for (let i = 0; i < count; i += 1) {
151
+ const parsed = this._parseValue(cursor);
152
+ if (!parsed) return null;
153
+ items.push(parsed.value);
154
+ cursor = parsed.offset;
155
+ }
156
+ return { value: items, offset: cursor };
157
+ }
158
+ default:
159
+ throw new CrowkisError(`unexpected RESP frame: ${line.line.toString()}`);
160
+ }
161
+ }
162
+ }
163
+
164
+ function sleep(ms) {
165
+ return new Promise((resolve) => setTimeout(resolve, Math.min(Math.max(Number(ms) || 0, 0), 2000)));
166
+ }
167
+
168
+ function tokenChunks(text, chunkTokens = 4) {
169
+ const size = Math.max(1, Math.min(Number(chunkTokens) || 4, 128));
170
+ const words = String(text).split(/\s+/).filter(Boolean);
171
+ const chunks = [];
172
+ for (let i = 0; i < words.length; i += size) {
173
+ chunks.push(words.slice(i, i + size).join(" "));
174
+ }
175
+ return chunks;
176
+ }
177
+
178
+ async function* streamValues(value) {
179
+ value = await value;
180
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array || typeof value === "string") {
181
+ yield value;
182
+ return;
183
+ }
184
+ if (value && typeof value[Symbol.asyncIterator] === "function") {
185
+ for await (const chunk of value) yield chunk;
186
+ return;
187
+ }
188
+ if (value && typeof value[Symbol.iterator] === "function") {
189
+ for (const chunk of value) yield chunk;
190
+ return;
191
+ }
192
+ yield value;
193
+ }
194
+
195
+ class CrowkisClient {
196
+ constructor(options = {}) {
197
+ this.host = options.host || "127.0.0.1";
198
+ this.port = options.port || 6383;
199
+ this.tenant = options.tenant;
200
+ this.model = options.model;
201
+ this.authToken = options.authToken || options.auth_token;
202
+ this.timeoutMs = options.timeoutMs || 5000;
203
+ this.maxRetries = Math.max(0, options.maxRetries ?? 2);
204
+ this.backoffBaseMs = Math.max(0, options.backoffBaseMs ?? 100);
205
+ this.socket = null;
206
+ this.reader = null;
207
+ this.resp3 = false;
208
+ }
209
+
210
+ connect() {
211
+ if (this.socket) return Promise.resolve();
212
+ return new Promise((resolve, reject) => {
213
+ const socket = net.createConnection({ host: this.host, port: this.port });
214
+ const timer = setTimeout(() => {
215
+ socket.destroy();
216
+ reject(new CrowkisError("connect timeout"));
217
+ }, this.timeoutMs);
218
+ socket.once("connect", () => {
219
+ clearTimeout(timer);
220
+ this.socket = socket;
221
+ this.reader = new RespReader(socket);
222
+ if (!this.authToken) {
223
+ resolve();
224
+ return;
225
+ }
226
+ socket.write(encode(["AUTH", this.authToken]));
227
+ this.reader.read().then(resolve, reject);
228
+ });
229
+ socket.once("error", (error) => {
230
+ clearTimeout(timer);
231
+ reject(error);
232
+ });
233
+ });
234
+ }
235
+
236
+ close() {
237
+ if (this.socket) {
238
+ this.socket.destroy();
239
+ this.socket = null;
240
+ this.reader = null;
241
+ this.resp3 = false;
242
+ }
243
+ }
244
+
245
+ async execute(...args) {
246
+ // Retry connection-level failures with exponential backoff +
247
+ // jitter. RESP-level errors (CrowkisError from a live reply) are
248
+ // NOT retried — the server answered; retrying a rejected command
249
+ // is wrong.
250
+ let attempt = 0;
251
+ for (;;) {
252
+ try {
253
+ await this.connect();
254
+ this.socket.write(encode(args));
255
+ return await this.reader.read();
256
+ } catch (error) {
257
+ const connectionLevel =
258
+ error.code === "ECONNREFUSED" ||
259
+ error.code === "ECONNRESET" ||
260
+ error.code === "EPIPE" ||
261
+ error.code === "ETIMEDOUT" ||
262
+ (error instanceof CrowkisError && /timeout|closed/i.test(error.message));
263
+ if (!connectionLevel || attempt >= this.maxRetries) {
264
+ throw error;
265
+ }
266
+ this.close();
267
+ const delay =
268
+ this.backoffBaseMs * 2 ** attempt * (1 + Math.random() * 0.1);
269
+ await new Promise((r) => setTimeout(r, delay));
270
+ attempt += 1;
271
+ }
272
+ }
273
+ }
274
+
275
+ async _hello3() {
276
+ if (!this.resp3) {
277
+ await this.execute("HELLO", "3");
278
+ this.resp3 = true;
279
+ }
280
+ }
281
+
282
+ async ping() {
283
+ const value = await this.execute("PING");
284
+ return value === "PONG";
285
+ }
286
+
287
+ async get(key) {
288
+ const value = await this.execute("GET", key);
289
+ return Buffer.isBuffer(value) ? value : null;
290
+ }
291
+
292
+ async set(key, value, options = {}) {
293
+ const args = ["SET", key, value];
294
+ if (options.ttl !== undefined) args.push("EX", String(options.ttl));
295
+ await this.execute(...args);
296
+ }
297
+
298
+ _tenant(options = {}) {
299
+ return options.tenant || this.tenant;
300
+ }
301
+
302
+ _model(options = {}) {
303
+ return options.model || this.model;
304
+ }
305
+
306
+ async cset(query, response, options = {}) {
307
+ const args = ["CSET", query, response];
308
+ if (options.ttl !== undefined) args.push("EX", String(options.ttl));
309
+ if (this._tenant(options)) args.push("TENANT", this._tenant(options));
310
+ if (this._model(options)) args.push("MODEL", this._model(options));
311
+ if (options.modelVersion || options.model_version) {
312
+ args.push("MODEL_VERSION", options.modelVersion || options.model_version);
313
+ }
314
+ const image = imageBase64(options.image);
315
+ if (image) args.push("IMAGE", image);
316
+ await this.execute(...args);
317
+ }
318
+
319
+ async cget(query, options = {}) {
320
+ const args = ["CGET", query];
321
+ if (options.threshold !== undefined) args.push("THRESHOLD", String(options.threshold));
322
+ if (this._tenant(options)) args.push("TENANT", this._tenant(options));
323
+ if (this._model(options)) args.push("MODEL", this._model(options));
324
+ if (options.modelVersion || options.model_version) {
325
+ args.push("MODEL_VERSION", options.modelVersion || options.model_version);
326
+ }
327
+ if (options.migrationMode || options.migration_mode) {
328
+ args.push("MIGRATION_MODE", options.migrationMode || options.migration_mode);
329
+ }
330
+ const image = imageBase64(options.image);
331
+ if (image) args.push("IMAGE", image);
332
+ const value = await this.execute(...args);
333
+ if (value && typeof value === "object" && !Buffer.isBuffer(value)) return value.response || null;
334
+ return Buffer.isBuffer(value) ? value : null;
335
+ }
336
+
337
+ async cgetHit(query, options = {}) {
338
+ await this._hello3();
339
+ const args = ["CGET", query];
340
+ if (options.threshold !== undefined) args.push("THRESHOLD", String(options.threshold));
341
+ if (this._tenant(options)) args.push("TENANT", this._tenant(options));
342
+ if (this._model(options)) args.push("MODEL", this._model(options));
343
+ if (options.modelVersion || options.model_version) {
344
+ args.push("MODEL_VERSION", options.modelVersion || options.model_version);
345
+ }
346
+ if (options.migrationMode || options.migration_mode) {
347
+ args.push("MIGRATION_MODE", options.migrationMode || options.migration_mode);
348
+ }
349
+ const image = imageBase64(options.image);
350
+ if (image) args.push("IMAGE", image);
351
+ const value = await this.execute(...args);
352
+ if (!value || Buffer.isBuffer(value)) return null;
353
+ return {
354
+ response: value.response || Buffer.alloc(0),
355
+ text: (value.response || Buffer.alloc(0)).toString(),
356
+ similarity: Number(value.similarity || 0),
357
+ ttlRemaining: Number(value.ttl || 0),
358
+ matchedKey: value.key || Buffer.alloc(0),
359
+ confidence: Number(value.confidence || 0),
360
+ hitType: Buffer.isBuffer(value.hit_type) ? value.hit_type.toString() : String(value.hit_type || "unknown"),
361
+ migrationPending: Boolean(value.migration_pending),
362
+ migrationFromModelVersion: bufferToString(value.migration_from_model_version),
363
+ migrationTargetModelVersion: bufferToString(value.migration_target_model_version),
364
+ migrationCanaryId: bufferToString(value.migration_canary_id),
365
+ migrationPlannedAt: value.migration_planned_at === undefined ? null : Number(value.migration_planned_at),
366
+ };
367
+ }
368
+
369
+ async csim(query, options = {}) {
370
+ await this._hello3();
371
+ const args = ["CSIM", query, "K", String(Math.max(1, options.k || 10))];
372
+ if (this._tenant(options)) args.push("TENANT", this._tenant(options));
373
+ const value = await this.execute(...args);
374
+ return (value || [])
375
+ .filter((item) => item && typeof item === "object" && !Buffer.isBuffer(item))
376
+ .map((item) => ({ key: item.key || Buffer.alloc(0), similarity: Number(item.similarity || 0) }));
377
+ }
378
+
379
+ async cimgget(image, options = {}) {
380
+ await this._hello3();
381
+ const args = ["CIMGGET", imageBase64(image) || ""];
382
+ if (options.threshold !== undefined) args.push("THRESHOLD", String(options.threshold));
383
+ if (this._tenant(options)) args.push("TENANT", this._tenant(options));
384
+ if (this._model(options)) args.push("MODEL", this._model(options));
385
+ const value = await this.execute(...args);
386
+ if (value && typeof value === "object" && !Buffer.isBuffer(value)) return value.response || null;
387
+ return Buffer.isBuffer(value) ? value : null;
388
+ }
389
+
390
+ async cflush(options = {}) {
391
+ const args = ["CFLUSH"];
392
+ if (this._tenant(options)) args.push("TENANT", this._tenant(options));
393
+ return Number(await this.execute(...args));
394
+ }
395
+
396
+ async cvecCount() {
397
+ return Number(await this.execute("CVECCOUNT"));
398
+ }
399
+
400
+ async cembed(text) {
401
+ await this._hello3();
402
+ const value = await this.execute("CEMBED", text);
403
+ return Array.isArray(value) ? value.map(Number) : [];
404
+ }
405
+
406
+ async creuse(query) {
407
+ await this._hello3();
408
+ return this.execute("CREUSE", query);
409
+ }
410
+
411
+ async cthink(query, cotTrace) {
412
+ await this._hello3();
413
+ return this.execute("CTHINK", query, cotTrace);
414
+ }
415
+
416
+ async cwhyevict(query, tenant) {
417
+ await this._hello3();
418
+ const args = ["CWHYEVICT", query];
419
+ const t = tenant || this.tenant;
420
+ if (t) args.push("TENANT", t);
421
+ return this.execute(...args);
422
+ }
423
+
424
+ async cstale(query, tenant) {
425
+ await this._hello3();
426
+ const args = ["CSTALE", query];
427
+ const t = tenant || this.tenant;
428
+ if (t) args.push("TENANT", t);
429
+ return this.execute(...args);
430
+ }
431
+
432
+ async cinvalidate(instruction, { tenant, threshold, limit, commit } = {}) {
433
+ await this._hello3();
434
+ const args = ["CINVALIDATE", instruction];
435
+ const t = tenant || this.tenant;
436
+ if (t) args.push("TENANT", t);
437
+ if (threshold !== undefined) args.push("THRESHOLD", String(threshold));
438
+ if (limit !== undefined) args.push("LIMIT", String(limit));
439
+ if (commit) args.push("COMMIT");
440
+ return this.execute(...args);
441
+ }
442
+
443
+ async cmemset(agent, fact, { user, ex } = {}) {
444
+ await this._hello3();
445
+ const args = ["CMEMSET", agent, fact];
446
+ if (user) args.push("USER", user);
447
+ if (ex !== undefined) args.push("EX", String(ex));
448
+ return this.execute(...args);
449
+ }
450
+
451
+ async cmemget(agent, query, { user, k } = {}) {
452
+ await this._hello3();
453
+ const args = ["CMEMGET", agent, query];
454
+ if (user) args.push("USER", user);
455
+ if (k !== undefined) args.push("K", String(k));
456
+ return this.execute(...args);
457
+ }
458
+
459
+ async cmemextract(agent, conversation, { user, ex } = {}) {
460
+ await this._hello3();
461
+ const args = ["CMEMEXTRACT", agent, conversation];
462
+ if (user) args.push("USER", user);
463
+ if (ex !== undefined) args.push("EX", String(ex));
464
+ return this.execute(...args);
465
+ }
466
+
467
+ async cmemhistory(agent, query, { user, k } = {}) {
468
+ await this._hello3();
469
+ const args = ["CMEMHISTORY", agent, query];
470
+ if (user) args.push("USER", user);
471
+ if (k !== undefined) args.push("K", String(k));
472
+ return this.execute(...args);
473
+ }
474
+
475
+ async cmemasof(agent, query, unixMs, { user, k } = {}) {
476
+ await this._hello3();
477
+ const args = ["CMEMASOF", agent, query, String(unixMs)];
478
+ if (user) args.push("USER", user);
479
+ if (k !== undefined) args.push("K", String(k));
480
+ return this.execute(...args);
481
+ }
482
+
483
+ async cmemforget(agent, { query, user, threshold } = {}) {
484
+ await this._hello3();
485
+ const args = ["CMEMFORGET", agent];
486
+ if (query) args.push(query);
487
+ if (user) args.push("USER", user);
488
+ if (threshold !== undefined) args.push("THRESHOLD", String(threshold));
489
+ return this.execute(...args);
490
+ }
491
+
492
+ async cmemlink(agent, subject, relation, object, { user } = {}) {
493
+ await this._hello3();
494
+ const args = ["CMEMLINK", agent, subject, relation, object];
495
+ if (user) args.push("USER", user);
496
+ return this.execute(...args);
497
+ }
498
+
499
+ async cmemgraph(agent, entity, { user, depth } = {}) {
500
+ await this._hello3();
501
+ const args = ["CMEMGRAPH", agent, entity];
502
+ if (user) args.push("USER", user);
503
+ if (depth !== undefined) args.push("DEPTH", String(depth));
504
+ return this.execute(...args);
505
+ }
506
+
507
+ async cdocAdd(docId, text, { tenant, ex } = {}) {
508
+ await this._hello3();
509
+ const args = ["CDOC", "ADD", docId, text];
510
+ const t = tenant || this.tenant;
511
+ if (t) args.push("TENANT", t);
512
+ if (ex !== undefined) args.push("EX", String(ex));
513
+ return this.execute(...args);
514
+ }
515
+
516
+ async cdocSearch(query, { k, tenant } = {}) {
517
+ await this._hello3();
518
+ const args = ["CDOC", "SEARCH", query];
519
+ if (k !== undefined) args.push("K", String(k));
520
+ const t = tenant || this.tenant;
521
+ if (t) args.push("TENANT", t);
522
+ return this.execute(...args);
523
+ }
524
+
525
+ async csessionAdd(session, role, text, { ex } = {}) {
526
+ await this._hello3();
527
+ const args = ["CSESSION", "ADD", session, role, text];
528
+ if (ex !== undefined) args.push("EX", String(ex));
529
+ return this.execute(...args);
530
+ }
531
+
532
+ async csessionRecent(session, { n } = {}) {
533
+ await this._hello3();
534
+ const args = ["CSESSION", "RECENT", session];
535
+ if (n !== undefined) args.push("N", String(n));
536
+ return this.execute(...args);
537
+ }
538
+
539
+ async csessionSearch(session, query, { k } = {}) {
540
+ await this._hello3();
541
+ const args = ["CSESSION", "SEARCH", session, query];
542
+ if (k !== undefined) args.push("K", String(k));
543
+ return this.execute(...args);
544
+ }
545
+
546
+ async cpin(query, answer, { by, tenant } = {}) {
547
+ await this._hello3();
548
+ const args = ["CPIN", query, answer];
549
+ if (by) args.push("BY", by);
550
+ const t = tenant || this.tenant;
551
+ if (t) args.push("TENANT", t);
552
+ return this.execute(...args);
553
+ }
554
+
555
+ async cpinget(query, { tenant, threshold } = {}) {
556
+ await this._hello3();
557
+ const args = ["CPINGET", query];
558
+ const t = tenant || this.tenant;
559
+ if (t) args.push("TENANT", t);
560
+ if (threshold !== undefined) args.push("THRESHOLD", String(threshold));
561
+ return this.execute(...args);
562
+ }
563
+
564
+ async cpinlist({ tenant } = {}) {
565
+ await this._hello3();
566
+ const args = ["CPINLIST"];
567
+ const t = tenant || this.tenant;
568
+ if (t) args.push("TENANT", t);
569
+ return this.execute(...args);
570
+ }
571
+
572
+ async cunpin(query, { tenant } = {}) {
573
+ await this._hello3();
574
+ const args = ["CUNPIN", query];
575
+ const t = tenant || this.tenant;
576
+ if (t) args.push("TENANT", t);
577
+ return this.execute(...args);
578
+ }
579
+
580
+ async ctoolset(tool, argsStr, result, { ex, tenant } = {}) {
581
+ await this._hello3();
582
+ const args = ["CTOOLSET", tool, argsStr, result];
583
+ if (ex !== undefined) args.push("EX", String(ex));
584
+ const t = tenant || this.tenant;
585
+ if (t) args.push("TENANT", t);
586
+ return this.execute(...args);
587
+ }
588
+
589
+ async ctoolget(tool, argsStr, { tenant } = {}) {
590
+ await this._hello3();
591
+ const args = ["CTOOLGET", tool, argsStr];
592
+ const t = tenant || this.tenant;
593
+ if (t) args.push("TENANT", t);
594
+ return this.execute(...args);
595
+ }
596
+
597
+ async cflag(query, badAnswer, { reason, tenant } = {}) {
598
+ await this._hello3();
599
+ const args = ["CFLAG", query, badAnswer];
600
+ if (reason) args.push("REASON", reason);
601
+ const t = tenant || this.tenant;
602
+ if (t) args.push("TENANT", t);
603
+ return this.execute(...args);
604
+ }
605
+
606
+ async ccheckbad(query, { tenant, threshold } = {}) {
607
+ await this._hello3();
608
+ const args = ["CCHECKBAD", query];
609
+ const t = tenant || this.tenant;
610
+ if (t) args.push("TENANT", t);
611
+ if (threshold !== undefined) args.push("THRESHOLD", String(threshold));
612
+ return this.execute(...args);
613
+ }
614
+
615
+ async cguard(text) {
616
+ await this._hello3();
617
+ return this.execute("CGUARD", text);
618
+ }
619
+
620
+ async coutcheck(text) {
621
+ await this._hello3();
622
+ return this.execute("COUTCHECK", text);
623
+ }
624
+
625
+ // ── Phase 0.1 operator surface ─────────────────────────────────────
626
+
627
+ async cinfo(section) {
628
+ const args = section ? ["CINFO", section] : ["CINFO"];
629
+ const value = await this.execute(...args);
630
+ return value == null ? "" : value.toString();
631
+ }
632
+
633
+ async cbudgetGet(tenant) {
634
+ await this._hello3();
635
+ return this.execute("CBUDGET", "GET", tenant || this.tenant || "default");
636
+ }
637
+
638
+ async cbudgetAlerts() {
639
+ await this._hello3();
640
+ return this.execute("CBUDGET", "ALERTS");
641
+ }
642
+
643
+ // Set a tenant's spend budget. The gateway enforces it: cost-aware routing
644
+ // past the alert %, and a hard block on upstream calls past the circuit %.
645
+ async cbudgetSet(tenant, { dailyUsd, monthlyUsd, costPer1k, alertPct, circuitPct } = {}) {
646
+ await this._hello3();
647
+ const args = ["CBUDGET", "SET", tenant || this.tenant || "default"];
648
+ if (dailyUsd != null) args.push("DAILY", String(dailyUsd));
649
+ if (monthlyUsd != null) args.push("MONTHLY", String(monthlyUsd));
650
+ if (costPer1k != null) args.push("COST", String(costPer1k));
651
+ if (alertPct != null) args.push("ALERT", String(alertPct));
652
+ if (circuitPct != null) args.push("CIRCUIT", String(circuitPct));
653
+ return this.execute(...args);
654
+ }
655
+
656
+ async cdedup(tenant) {
657
+ await this._hello3();
658
+ const args = ["CDEDUP"];
659
+ const t = tenant || this.tenant;
660
+ if (t) args.push("TENANT", t);
661
+ return this.execute(...args);
662
+ }
663
+
664
+ async cpiiReport(tenant) {
665
+ await this._hello3();
666
+ const args = ["CPII", "REPORT"];
667
+ const t = tenant || this.tenant;
668
+ if (t) args.push("TENANT", t);
669
+ return this.execute(...args);
670
+ }
671
+
672
+ async cpiiErase(identifier, tenant) {
673
+ await this._hello3();
674
+ const args = ["CPII", "ERASE", identifier];
675
+ const t = tenant || this.tenant;
676
+ if (t) args.push("TENANT", t);
677
+ return this.execute(...args);
678
+ }
679
+
680
+ async csave(dest) {
681
+ return (await this.execute("CSAVE", dest)) === "OK";
682
+ }
683
+
684
+ async cbgsave(dest) {
685
+ return (await this.execute("CBGSAVE", dest)) === "BGSAVE-STARTED";
686
+ }
687
+
688
+ async creload() {
689
+ await this._hello3();
690
+ return this.execute("CRELOAD");
691
+ }
692
+
693
+ async ckeylimitSet(tenant, { rpm, tpm } = {}) {
694
+ const args = ["CKEYLIMIT", "SET", tenant, "RPM", String(rpm)];
695
+ if (tpm != null) args.push("TPM", String(tpm));
696
+ return (await this.execute(...args)) === "OK";
697
+ }
698
+
699
+ async ckeylimitGet(tenant) {
700
+ await this._hello3();
701
+ return this.execute("CKEYLIMIT", "GET", tenant);
702
+ }
703
+
704
+ async ckeylimitDel(tenant) {
705
+ return Number(await this.execute("CKEYLIMIT", "DEL", tenant)) === 1;
706
+ }
707
+
708
+ // ── freshness sources, evals, prompt versioning, scan, compaction ───────
709
+
710
+ async cscan(cursor = "0", options = {}) {
711
+ const args = ["CSCAN", String(cursor)];
712
+ if (options.match) args.push("MATCH", options.match);
713
+ if (options.count != null) args.push("COUNT", String(options.count));
714
+ if (options.intent) args.push("INTENT", options.intent);
715
+ const value = await this.execute(...args);
716
+ if (Array.isArray(value) && value.length === 2) {
717
+ const next = Buffer.isBuffer(value[0]) ? value[0].toString() : String(value[0]);
718
+ return { cursor: next, keys: value[1] || [] };
719
+ }
720
+ return { cursor: "0", keys: [] };
721
+ }
722
+
723
+ async ceval(evaluator, input, output, options = {}) {
724
+ const args = ["CEVAL", evaluator, input, output];
725
+ if (options.expect != null) args.push("EXPECT", options.expect);
726
+ if (options.threshold != null) args.push("THRESHOLD", String(options.threshold));
727
+ return this.execute(...args);
728
+ }
729
+
730
+ async cpromptSet(name, template) {
731
+ return Number(await this.execute("CPROMPT", "SET", name, template));
732
+ }
733
+
734
+ async cpromptGet(name, version) {
735
+ const args = ["CPROMPT", "GET", name];
736
+ if (version != null) args.push(String(version));
737
+ const v = await this.execute(...args);
738
+ return Buffer.isBuffer(v) ? v : null;
739
+ }
740
+
741
+ async cpromptList() {
742
+ return this.execute("CPROMPT", "LIST");
743
+ }
744
+
745
+ async cpromptVersions(name) {
746
+ return this.execute("CPROMPT", "VERSIONS", name);
747
+ }
748
+
749
+ async csourceLink(sourceId, query, options = {}) {
750
+ const args = ["CSOURCE", "LINK", sourceId, query];
751
+ const tenant = options.tenant || this.tenant;
752
+ if (tenant) args.push("TENANT", tenant);
753
+ return String(await this.execute(...args)) === "OK";
754
+ }
755
+
756
+ async csourcePurge(sourceId) {
757
+ return Number(await this.execute("CSOURCE", "PURGE", sourceId));
758
+ }
759
+
760
+ async csourceList(sourceId) {
761
+ return this.execute("CSOURCE", "LIST", sourceId);
762
+ }
763
+
764
+ async compact() {
765
+ return this.execute("COMPACT");
766
+ }
767
+
768
+ async getOrCompute(query, fn, options = {}) {
769
+ const cached = await this.cget(query, options);
770
+ if (cached) return cached.toString();
771
+ const response = await fn(query);
772
+ const bytes = toBuffer(response);
773
+ await this.cset(query, bytes, options);
774
+ return bytes.toString();
775
+ }
776
+
777
+ async *streamGetOrCompute(query, fn, options = {}) {
778
+ const cached = await this.cget(query, options);
779
+ if (cached) {
780
+ for (const chunk of tokenChunks(cached.toString(), options.chunkTokens || 4)) {
781
+ yield chunk;
782
+ if ((options.delayMs || 20) > 0) await sleep(options.delayMs || 20);
783
+ }
784
+ return;
785
+ }
786
+
787
+ const chunks = [];
788
+ let completed = false;
789
+ try {
790
+ for await (const chunk of streamValues(fn(query))) {
791
+ chunks.push(toBuffer(chunk));
792
+ yield chunk;
793
+ }
794
+ completed = true;
795
+ } finally {
796
+ if (completed) {
797
+ await this.cset(query, Buffer.concat(chunks), options);
798
+ }
799
+ }
800
+ }
801
+
802
+ // ── High-level, model-agnostic cache API (clean names) ──────────────────
803
+ // Works with ANY model or provider. Bring the call; Crowkis caches by meaning.
804
+
805
+ async lookup(prompt, options = {}) {
806
+ // Semantic read → { text, similarity, confidence } or null.
807
+ return this.cgetHit(prompt, options);
808
+ }
809
+
810
+ async store(prompt, answer, options = {}) {
811
+ // Cache an answer for a prompt (options.ttl in seconds).
812
+ return this.cset(prompt, answer, options);
813
+ }
814
+
815
+ async ask(prompt, compute, options = {}) {
816
+ // Recall the answer, else run compute(prompt) — any model — cache it, return it.
817
+ return this.getOrCompute(prompt, compute, options);
818
+ }
819
+
820
+ cached(fn, options = {}) {
821
+ // Wrap ANY async function whose first arg is the prompt in a semantic cache.
822
+ // Model-agnostic — fn can call any provider.
823
+ const self = this;
824
+ return async function (prompt, ...rest) {
825
+ const hit = await self.cget(prompt, options);
826
+ if (hit) return hit.toString();
827
+ const out = await fn(prompt, ...rest);
828
+ await self.cset(prompt, toBuffer(out), options);
829
+ return typeof out === "string" ? out : toBuffer(out).toString();
830
+ };
831
+ }
832
+
833
+ stream(prompt, compute, options = {}) {
834
+ // Streamed recall-or-compute (async iterator of chunks).
835
+ return this.streamGetOrCompute(prompt, compute, options);
836
+ }
837
+
838
+ async similar(prompt, options = {}) {
839
+ return this.csim(prompt, options);
840
+ }
841
+
842
+ async embed(text) {
843
+ return this.cembed(text);
844
+ }
845
+
846
+ async flush(options = {}) {
847
+ return this.cflush(options);
848
+ }
849
+ }
850
+
851
+ class CrowkisAdmin {
852
+ constructor(options = {}) {
853
+ this.baseUrl = options.baseUrl || "http://127.0.0.1:6380";
854
+ this.adminKey = options.adminKey;
855
+ this.timeoutMs = options.timeoutMs || 5000;
856
+ }
857
+
858
+ async _request(method, path, body) {
859
+ const controller = new AbortController();
860
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
861
+ const headers = { "content-type": "application/json" };
862
+ if (this.adminKey) headers["x-crowkis-admin-key"] = this.adminKey;
863
+ try {
864
+ const response = await fetch(new URL(path, this.baseUrl), {
865
+ method,
866
+ headers,
867
+ body: body === undefined ? undefined : JSON.stringify(body),
868
+ signal: controller.signal,
869
+ });
870
+ const text = await response.text();
871
+ if (!response.ok) throw new CrowkisError(text || response.statusText);
872
+ if (!text) return {};
873
+ try {
874
+ return JSON.parse(text);
875
+ } catch (_) {
876
+ return { text };
877
+ }
878
+ } finally {
879
+ clearTimeout(timeout);
880
+ }
881
+ }
882
+
883
+ health() {
884
+ return this._request("GET", "/health");
885
+ }
886
+
887
+ getStats() {
888
+ return this._request("GET", "/stats");
889
+ }
890
+
891
+ updateThreshold(config) {
892
+ return this._request("PUT", "/config/thresholds", config);
893
+ }
894
+
895
+ registerWebhook(webhook) {
896
+ return this._request("POST", "/api/v1/webhooks", webhook);
897
+ }
898
+
899
+ invalidateSource(sourceId, extra = {}) {
900
+ return this._request("POST", "/api/v1/invalidate", { source_id: sourceId, ...extra });
901
+ }
902
+
903
+ flushTenant(tenantId) {
904
+ return this._request("POST", `/api/v1/tenants/${encodeURIComponent(tenantId)}/flush`, {});
905
+ }
906
+
907
+ cacheEntries(options = {}) {
908
+ const query = new URLSearchParams();
909
+ if (options.tenant) query.set("tenant", options.tenant);
910
+ if (options.limit) query.set("limit", String(options.limit));
911
+ const suffix = query.toString() ? `?${query}` : "";
912
+ return this._request("GET", `/api/v1/cache/entries${suffix}`);
913
+ }
914
+
915
+ migrationProgress(options = {}) {
916
+ const query = new URLSearchParams();
917
+ if (options.tenant) query.set("tenant", options.tenant);
918
+ if (options.canaryId || options.canary_id) query.set("canary_id", options.canaryId || options.canary_id);
919
+ if (options.fromModelVersion || options.from_model_version) {
920
+ query.set("from_model_version", options.fromModelVersion || options.from_model_version);
921
+ }
922
+ if (options.targetModelVersion || options.target_model_version) {
923
+ query.set("target_model_version", options.targetModelVersion || options.target_model_version);
924
+ }
925
+ if (options.limit !== undefined) query.set("limit", String(options.limit));
926
+ const suffix = query.toString() ? `?${query}` : "";
927
+ return this._request("GET", `/api/v1/llm/migrations/progress${suffix}`);
928
+ }
929
+
930
+ canaryMigrationProgress(id, options = {}) {
931
+ const query = new URLSearchParams();
932
+ if (options.tenant) query.set("tenant", options.tenant);
933
+ if (options.limit !== undefined) query.set("limit", String(options.limit));
934
+ const suffix = query.toString() ? `?${query}` : "";
935
+ return this._request("GET", `/api/v1/llm/canaries/${encodeURIComponent(id)}/migration-progress${suffix}`);
936
+ }
937
+
938
+ leaseMigrationWork(options = {}) {
939
+ return this._request("POST", "/api/v1/llm/migrations/lease", options);
940
+ }
941
+
942
+ leaseCanaryMigrationWork(id, options = {}) {
943
+ return this._request("POST", `/api/v1/llm/canaries/${encodeURIComponent(id)}/migration-lease`, options);
944
+ }
945
+
946
+ completeMigrationWork(item) {
947
+ return this._request("POST", "/api/v1/llm/migrations/complete", item);
948
+ }
949
+
950
+ failMigrationWork(item) {
951
+ return this._request("POST", "/api/v1/llm/migrations/fail", item);
952
+ }
953
+ }
954
+
955
+ module.exports = {
956
+ Crowkis: CrowkisClient, // idiomatic short name (like `new Redis()`)
957
+ CrowkisClient,
958
+ CrowkisAdmin,
959
+ CrowkisError,
960
+ ...require("./grpc"),
961
+ };