wontopos 2.2.37 → 2.2.39

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/wontopos.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = "2.2.37";
1
+ const VERSION = "2.2.39";
2
2
  const RUNTIME = (() => {
3
3
  const p = globalThis?.process;
4
4
  return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
@@ -13,7 +13,7 @@ function logDebug(msg) {
13
13
  const DEFAULT_BASE_URL = "https://api.wontopos.com";
14
14
  const DEFAULT_MODEL = "tablet-2";
15
15
  const RETRY_ALWAYS = new Set([429]);
16
- const RETRY_IF_IDEMPOTENT = new Set([502, 503]);
16
+ const RETRY_IF_IDEMPOTENT = new Set([408, 502, 503, 504]);
17
17
  const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
18
18
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
19
19
  const MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
@@ -24,6 +24,12 @@ function normalizeStoreId(id) {
24
24
  return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
25
25
  }
26
26
  const WARNED_STORE_IDS_MAX = 1024;
27
+ function assertUsableStoreId(userId) {
28
+ if (userId !== undefined && (typeof userId !== "string" || !userId.trim())) {
29
+ throw new Error(`userId must be a non-blank string; got ${JSON.stringify(userId)}. Omit it to use the client's ` +
30
+ "default store, or pass a real store id — anything else would silently write into the default store.");
31
+ }
32
+ }
27
33
  const WARNED_FILTER_KEYS_MAX = 1024;
28
34
  const warnedStoreIds = new Set();
29
35
  function warnIfStoreIdCollapses(id) {
@@ -197,6 +203,47 @@ const KNOWN_FILTER_KEYS = new Set([
197
203
  "min_importance",
198
204
  ]);
199
205
  const warnedFilterKeys = new Set();
206
+ const KNOWN_SEARCH_KEYS = new Set([
207
+ "cache_control", "speaker", "filters", "verify", "max_images", "extra",
208
+ ]);
209
+ const RESERVED_SEARCH_KEYS = new Set(["user_id", "query", "max_results"]);
210
+ const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
211
+ const KNOWN_ENGRAM_KEYS = new Set(["form", "tz"]);
212
+ function checkSearchOpts(opts) {
213
+ checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
214
+ }
215
+ function checkOpts(opts, known, what) {
216
+ for (const k of Object.keys(opts)) {
217
+ if (RESERVED_SEARCH_KEYS.has(k))
218
+ throw new Error(`${JSON.stringify(k)} is set by the call, not by options — pass it as an argument. ` +
219
+ `An app forwarding untrusted input as options cannot steer the store, the query ` +
220
+ `or the count, and this says so rather than dropping it silently.`);
221
+ if (known.has(k))
222
+ continue;
223
+ const near = [...known].find((n) => n !== "extra" && editWithin(k, n, 2));
224
+ throw new Error(`unknown ${what} option ${JSON.stringify(k)}` +
225
+ (near ? ` — did you mean ${JSON.stringify(near)}?` : "") +
226
+ `. It would have had no effect, and the call would have looked like it worked.` +
227
+ (known.has("extra")
228
+ ? ` Pass it under \`extra\` if the service accepts it and this client does not know it yet.`
229
+ : ""));
230
+ }
231
+ }
232
+ function editWithin(a, b, max) {
233
+ if (Math.abs(a.length - b.length) > max)
234
+ return false;
235
+ const prev = Array.from({ length: b.length + 1 }, (_, j) => j);
236
+ for (let i = 1; i <= a.length; i++) {
237
+ let diag = prev[0];
238
+ prev[0] = i;
239
+ for (let j = 1; j <= b.length; j++) {
240
+ const tmp = prev[j];
241
+ prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
242
+ diag = tmp;
243
+ }
244
+ }
245
+ return prev[b.length] <= max;
246
+ }
200
247
  function warnOnUnknownFilters(filters) {
201
248
  if (!filters || typeof filters !== "object" || Array.isArray(filters))
202
249
  return;
@@ -242,6 +289,11 @@ export class Client {
242
289
  throw new Error("apiKey contains whitespace - check for a stray newline or paste error");
243
290
  if (/[\x00-\x1f\x7f]/.test(key))
244
291
  throw new Error("apiKey contains a control character - check for a stray byte or paste error");
292
+ if (/[^\x00-\x7f]/.test(key)) {
293
+ const bad = key.match(/[^\x00-\x7f]/)?.[0];
294
+ throw new Error(`apiKey contains a non-ASCII character (${JSON.stringify(bad)}) - rich text turns '-' into an ` +
295
+ "en dash; copy the key from a plain-text field");
296
+ }
245
297
  this.apiKey = key;
246
298
  this.base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
247
299
  const tm = opts.timeoutMs;
@@ -250,7 +302,8 @@ export class Client {
250
302
  if (this.model && !MODEL_RE.test(this.model)) {
251
303
  throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
252
304
  }
253
- this.defaultUser = opts.userId || DEFAULT_USER;
305
+ assertUsableStoreId(opts.userId);
306
+ this.defaultUser = opts.userId ?? DEFAULT_USER;
254
307
  const mr = opts.maxRetries ?? opts.retries;
255
308
  this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
256
309
  const dl = opts.deadlineMs;
@@ -298,11 +351,8 @@ export class Client {
298
351
  return `Client(${this.base}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
299
352
  }
300
353
  uid(userId) {
301
- if (userId !== undefined && (typeof userId !== "string" || !userId.trim())) {
302
- throw new Error(`userId must be a non-blank string; got ${JSON.stringify(userId)}. Omit it to use the client's ` +
303
- "default store, or pass a real store id — anything else would silently write into the default store.");
304
- }
305
- const id = userId || this.defaultUser;
354
+ assertUsableStoreId(userId);
355
+ const id = userId ?? this.defaultUser;
306
356
  warnIfStoreIdCollapses(id);
307
357
  return id;
308
358
  }
@@ -324,6 +374,10 @@ export class Client {
324
374
  return this.clone({ model });
325
375
  }
326
376
  withUser(userId) {
377
+ if (userId === undefined) {
378
+ throw new Error("withUser() needs a store id. It was called with undefined — usually a lookup that " +
379
+ "found nothing. Use the client's own default instead of calling withUser at all.");
380
+ }
327
381
  return this.clone({ userId });
328
382
  }
329
383
  withSignal(signal) {
@@ -338,43 +392,49 @@ export class Client {
338
392
  withRetries(maxRetries) {
339
393
  return this.clone({ maxRetries });
340
394
  }
341
- add(content, userId, metadata = {}, opts = {}) {
395
+ async add(content, userId, metadata = {}, opts = {}) {
342
396
  const body = { user_id: this.uid(userId), content, metadata };
343
397
  if (opts.image !== undefined)
344
398
  body.image = normalizeImage(opts.image);
345
399
  return this.post("/api/v1/memory/store", body, opts.idempotencyKey);
346
400
  }
347
- store(content, userId, metadata = {}, opts = {}) {
401
+ async store(content, userId, metadata = {}, opts = {}) {
348
402
  return this.add(content, userId, metadata, opts);
349
403
  }
350
- addTurn(userMsg, assistantMsg, userId, opts = {}) {
404
+ async addTurn(userMsg, assistantMsg, userId, opts = {}) {
351
405
  return this.post("/api/v1/memory/store-turn", { user_id: this.uid(userId), user_msg: userMsg, assistant_msg: assistantMsg }, opts.idempotencyKey);
352
406
  }
353
- addBulk(content, userId, category = "general", timestamp, opts = {}) {
407
+ async addBulk(content, userId, category = "general", timestamp, opts = {}) {
354
408
  const body = { user_id: this.uid(userId), content, category };
355
409
  if (timestamp)
356
410
  body.timestamp = timestamp;
357
411
  return this.post("/api/v1/memory/bulk-store", body, opts.idempotencyKey);
358
412
  }
359
- update(oldMemoryId, newContent, userId, opts = {}) {
413
+ async update(oldMemoryId, newContent, userId, opts = {}) {
360
414
  return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, opts.idempotencyKey);
361
415
  }
362
416
  async search(query, userId, limit = 10, opts = {}) {
417
+ checkSearchOpts(opts);
363
418
  warnOnUnknownFilters(opts.filters);
364
419
  checkCount(limit, "limit");
365
- const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
420
+ const { extra, ...known } = opts;
421
+ const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
366
422
  return mergeResults(r);
367
423
  }
368
424
  async searchSelf(query, userId, limit = 10, opts = {}) {
425
+ checkSearchOpts(opts);
369
426
  warnOnUnknownFilters(opts.filters);
370
427
  checkCount(limit, "limit");
371
- const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
428
+ const { extra, ...known } = opts;
429
+ const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
372
430
  return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
373
431
  }
374
432
  async searchFull(query, userId, limit = 10, opts = {}) {
433
+ checkSearchOpts(opts);
375
434
  warnOnUnknownFilters(opts.filters);
376
435
  checkCount(limit, "limit");
377
- const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
436
+ const { extra, ...known } = opts;
437
+ const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
378
438
  return {
379
439
  ...r,
380
440
  memories: asRecords(r.memories),
@@ -383,6 +443,7 @@ export class Client {
383
443
  };
384
444
  }
385
445
  async recall(query, userId, opts = {}) {
446
+ checkOpts(opts, KNOWN_RECALL_KEYS, "recall");
386
447
  const body = { user_id: this.uid(userId), query };
387
448
  if (opts.limit !== undefined) {
388
449
  checkCount(opts.limit, "limit");
@@ -394,14 +455,15 @@ export class Client {
394
455
  }
395
456
  return this.post("/api/v1/memory/recall", withForm(body, opts.form, opts.tz));
396
457
  }
397
- engram(name, query, userId, opts = {}) {
458
+ async engram(name, query, userId, opts = {}) {
459
+ checkOpts(opts, KNOWN_ENGRAM_KEYS, "engram");
398
460
  return this.post("/api/v1/engram/run", withForm({ name, user_id: this.uid(userId), query }, opts.form, opts.tz));
399
461
  }
400
462
  async history(userId) {
401
463
  const r = await this.post("/api/v1/memory/history", { user_id: this.uid(userId) });
402
464
  return asRecords(r.turns);
403
465
  }
404
- stats(userId) {
466
+ async stats(userId) {
405
467
  return this.post("/api/v1/memory/stats", { user_id: this.uid(userId) });
406
468
  }
407
469
  async usage(days = 7) {
@@ -410,13 +472,13 @@ export class Client {
410
472
  }
411
473
  return this.request("GET", `/api/v1/won/usage?days=${days}`);
412
474
  }
413
- get(userId, memoryId) {
414
- if (!memoryId || typeof memoryId !== "string") {
475
+ async get(userId, memoryId) {
476
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
415
477
  throw new Error("memoryId is required — the id that add/store or listMemories returned.");
416
478
  }
417
- return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId }).then((r) => asObj(r.memory));
479
+ return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then((r) => asObj(r.memory));
418
480
  }
419
- listMemories(userId, opts = {}) {
481
+ async listMemories(userId, opts = {}) {
420
482
  const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
421
483
  if (opts.cursor)
422
484
  body.cursor = opts.cursor;
@@ -444,22 +506,28 @@ export class Client {
444
506
  out.push(m);
445
507
  return out;
446
508
  }
447
- getImage(userId, memoryId) {
448
- if (!memoryId || typeof memoryId !== "string") {
509
+ async exportImages(userId, opts = {}) {
510
+ const out = [];
511
+ for await (const m of this.iterImages(userId, opts))
512
+ out.push(m);
513
+ return out;
514
+ }
515
+ async getImage(userId, memoryId) {
516
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
449
517
  throw new Error("memoryId is required — the id that add/store or listImages returned.");
450
518
  }
451
- return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId });
519
+ return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId.trim() });
452
520
  }
453
- forgetImage(userId, memoryId, opts = {}) {
454
- if (!memoryId || typeof memoryId !== "string") {
521
+ async forgetImage(userId, memoryId, opts = {}) {
522
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
455
523
  throw new Error("memoryId is required — the id of the memory whose image you want removed.");
456
524
  }
457
- const body = { user_id: this.uid(userId), memory_id: memoryId };
525
+ const body = { user_id: this.uid(userId), memory_id: memoryId.trim() };
458
526
  if (opts.preview)
459
527
  body.preview = true;
460
528
  return this.request("DELETE", "/api/v1/memory/image", body);
461
529
  }
462
- listImages(userId, opts = {}) {
530
+ async listImages(userId, opts = {}) {
463
531
  const body = { user_id: this.uid(userId) };
464
532
  if (opts.limit !== undefined)
465
533
  body.limit = opts.limit;
@@ -489,7 +557,7 @@ export class Client {
489
557
  throw new Error(`stopped after ${MAX_PAGES} pages — the store did not end. This is a truncated ` +
490
558
  `answer, not the whole store.`);
491
559
  }
492
- revisions(userId, opts = {}) {
560
+ async revisions(userId, opts = {}) {
493
561
  const body = {};
494
562
  if (opts.include !== undefined)
495
563
  body.include = opts.include;
@@ -502,13 +570,13 @@ export class Client {
502
570
  body.user_id = this.uid(userId);
503
571
  return this.post("/api/v1/won/revisions", body);
504
572
  }
505
- lineage(userId, memoryId) {
506
- if (!memoryId || typeof memoryId !== "string") {
573
+ async lineage(userId, memoryId) {
574
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
507
575
  throw new Error("memoryId is required — the memory whose history you want.");
508
576
  }
509
- return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId });
577
+ return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId.trim() });
510
578
  }
511
- bySpeaker(speaker, userId, opts = {}) {
579
+ async bySpeaker(speaker, userId, opts = {}) {
512
580
  if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
513
581
  throw new Error('speaker is required — "me" for the assistant, or a person\'s name.');
514
582
  }
@@ -532,41 +600,42 @@ export class Client {
532
600
  async listEngrams() {
533
601
  const data = await this.request("GET", "/api/v1/engram");
534
602
  return {
603
+ ...data,
535
604
  engrams: asRecords(data.engrams),
536
605
  forms: asRecords(data.forms),
537
606
  note: typeof data.note === "string" ? data.note : undefined,
538
607
  };
539
608
  }
540
- createStore(userId) {
609
+ async createStore(userId) {
541
610
  return this.post("/api/v1/memory/collection", { user_id: this.uid(userId) });
542
611
  }
543
612
  async listStores() {
544
613
  const r = await this.request("GET", "/api/v1/memory/collections");
545
614
  return asRecords(r.collections);
546
615
  }
547
- deleteStore(userId) {
616
+ async deleteStore(userId) {
548
617
  if (typeof userId !== "string" || !userId.trim())
549
618
  throw new Error("userId is required (a non-blank string) — deleteStore never falls back to the default store.");
550
619
  warnIfStoreIdCollapses(userId);
551
620
  return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
552
621
  }
553
- addSpeaker(speaker, userId) {
622
+ async addSpeaker(speaker, userId) {
554
623
  return this.post("/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
555
624
  }
556
625
  async listSpeakers(userId) {
557
626
  const r = await this.request("GET", `/api/v1/memory/speakers?user_id=${encodeURIComponent(this.uid(userId))}`);
558
627
  return { ...r, speakers: asRecords(r?.speakers) };
559
628
  }
560
- removeSpeaker(speaker, userId) {
629
+ async removeSpeaker(speaker, userId) {
561
630
  return this.request("DELETE", "/api/v1/memory/speakers", { user_id: this.uid(userId), speaker });
562
631
  }
563
- delete(userId, memoryId) {
632
+ async delete(userId, memoryId) {
564
633
  if (typeof memoryId !== "string" || !memoryId.trim()) {
565
634
  throw new Error("memoryId is required (non-blank). To delete every memory in a store, call deleteAll(userId) explicitly.");
566
635
  }
567
636
  return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId.trim() });
568
637
  }
569
- deleteAll(userId) {
638
+ async deleteAll(userId) {
570
639
  if (typeof userId !== "string" || !userId.trim())
571
640
  throw new Error("userId is required (a non-blank string) for deleteAll — anything else would wipe the default store.");
572
641
  warnIfStoreIdCollapses(userId);
@@ -701,7 +770,9 @@ export class Client {
701
770
  if (e?.name === "AbortError" || att.signal.aborted) {
702
771
  throw new APIConnectionError(0, this.abortMessage(att.why()));
703
772
  }
704
- throw e;
773
+ if (e instanceof WosError)
774
+ throw e;
775
+ throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
705
776
  }
706
777
  if (buf.byteLength === 0) {
707
778
  throw new WosError(res.status, "empty image body — the service returned no bytes");
@@ -774,6 +845,14 @@ export class Client {
774
845
  const attempts = this.maxRetries + 1;
775
846
  const logPath = path.split("?")[0];
776
847
  const deadlineAt = this.deadlineAt();
848
+ let payload;
849
+ try {
850
+ payload = body === undefined ? undefined : JSON.stringify(body);
851
+ }
852
+ catch (e) {
853
+ throw new Error(`request body could not be serialized: ${e instanceof Error ? e.message : String(e)}. ` +
854
+ "Pass plain JSON values — an ORM entity, a BigInt or a circular reference cannot be sent.");
855
+ }
777
856
  for (let attempt = 0; attempt < attempts; attempt++) {
778
857
  const start = Date.now();
779
858
  const att = this.beginAttempt(deadlineAt);
@@ -782,7 +861,7 @@ export class Client {
782
861
  res = await this.fetchImpl(`${this.base}${path}`, {
783
862
  method,
784
863
  headers: extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers,
785
- body: body === undefined ? undefined : JSON.stringify(body),
864
+ body: payload,
786
865
  signal: att.signal,
787
866
  redirect: "manual",
788
867
  });
@@ -826,7 +905,14 @@ export class Client {
826
905
  }
827
906
  if (e instanceof WosError)
828
907
  throw e;
829
- throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
908
+ if (!(IDEMPOTENT_METHODS.has(method.toUpperCase()) && attempt + 1 < attempts)) {
909
+ throw new APIConnectionError(0, `network error: ${e?.message ?? e}`);
910
+ }
911
+ att.clear();
912
+ const delay = this.backoffMs(attempt);
913
+ logDebug(`${method} ${logPath} — body dropped mid-stream, retrying in ${delay}ms (attempt ${attempt + 1}/${attempts})`);
914
+ await this.backoffSleep(delay, deadlineAt);
915
+ continue;
830
916
  }
831
917
  finally {
832
918
  att.clear();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.37",
4
- "description": "Wontopos — long-term memory for AI agents. Identical recall in every language, ~100× lower LLM bill.",
3
+ "version": "2.2.39",
4
+ "description": "Wontopos \u2014 long-term memory for AI agents. Identical recall in every language, ~100\u00d7 lower LLM bill.",
5
5
  "keywords": [
6
6
  "memory",
7
7
  "ai",
@@ -20,11 +20,17 @@
20
20
  "types": "./dist/wontopos.d.ts",
21
21
  "exports": {
22
22
  ".": {
23
- "types": "./dist/wontopos.d.ts",
24
- "import": "./dist/wontopos.js",
25
- "require": "./dist/cjs/wontopos.js",
23
+ "import": {
24
+ "types": "./dist/wontopos.d.ts",
25
+ "default": "./dist/wontopos.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/cjs/wontopos.d.cts",
29
+ "default": "./dist/cjs/wontopos.js"
30
+ },
26
31
  "default": "./dist/wontopos.js"
27
- }
32
+ },
33
+ "./package.json": "./package.json"
28
34
  },
29
35
  "files": [
30
36
  "dist",
@@ -35,7 +41,7 @@
35
41
  "node": ">=18"
36
42
  },
37
43
  "scripts": {
38
- "build": "tsc && tsc -p tsconfig.types.json && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json','{\\\"type\\\":\\\"commonjs\\\"}\\n')\"",
44
+ "build": "tsc && tsc -p tsconfig.types.json && tsc -p tsconfig.cjs.json && node -e \"const fs=require('fs');fs.writeFileSync('dist/cjs/package.json','{\\\"type\\\":\\\"commonjs\\\"}\\n');fs.copyFileSync('dist/wontopos.d.ts','dist/cjs/wontopos.d.cts')\"",
39
45
  "test": "npm run build && node --test tests/client.test.mjs tests/cjs.test.cjs",
40
46
  "prepublishOnly": "npm run build && node scripts/pre-publish.mjs"
41
47
  },
@@ -44,5 +50,9 @@
44
50
  },
45
51
  "bugs": {
46
52
  "url": "https://wontopos.com/contact?topic=bug"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/wontopos/wontopos-node.git"
47
57
  }
48
58
  }