wontopos 2.2.37 → 2.2.38

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.
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WME = exports.Client = exports.ServerError = exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.PermissionDeniedError = exports.PaymentRequiredError = exports.AuthenticationError = exports.BadRequestError = exports.APIConnectionError = exports.WosError = exports.CONTEXT_LIMIT_MAX = exports.CONTEXT_LIMIT_MIN = exports.SEARCH_LIMIT_MAX = exports.SEARCH_LIMIT_MIN = void 0;
4
4
  exports._resetStoreIdWarnings = _resetStoreIdWarnings;
5
5
  exports._resetFilterWarnings = _resetFilterWarnings;
6
- const VERSION = "2.2.37";
6
+ const VERSION = "2.2.38";
7
7
  const RUNTIME = (() => {
8
8
  const p = globalThis?.process;
9
9
  return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
@@ -29,6 +29,12 @@ function normalizeStoreId(id) {
29
29
  return id.toLowerCase().replace(/[^a-z0-9_]/g, "_");
30
30
  }
31
31
  const WARNED_STORE_IDS_MAX = 1024;
32
+ function assertUsableStoreId(userId) {
33
+ if (userId !== undefined && (typeof userId !== "string" || !userId.trim())) {
34
+ throw new Error(`userId must be a non-blank string; got ${JSON.stringify(userId)}. Omit it to use the client's ` +
35
+ "default store, or pass a real store id — anything else would silently write into the default store.");
36
+ }
37
+ }
32
38
  const WARNED_FILTER_KEYS_MAX = 1024;
33
39
  const warnedStoreIds = new Set();
34
40
  function warnIfStoreIdCollapses(id) {
@@ -212,6 +218,47 @@ const KNOWN_FILTER_KEYS = new Set([
212
218
  "min_importance",
213
219
  ]);
214
220
  const warnedFilterKeys = new Set();
221
+ const KNOWN_SEARCH_KEYS = new Set([
222
+ "cache_control", "speaker", "filters", "verify", "max_images", "extra",
223
+ ]);
224
+ const RESERVED_SEARCH_KEYS = new Set(["user_id", "query", "max_results"]);
225
+ const KNOWN_RECALL_KEYS = new Set(["form", "tz", "limit", "context_limit"]);
226
+ function checkSearchOpts(opts) {
227
+ checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
228
+ }
229
+ function checkOpts(opts, known, what) {
230
+ for (const k of Object.keys(opts)) {
231
+ if (RESERVED_SEARCH_KEYS.has(k))
232
+ throw new Error(`${JSON.stringify(k)} is set by the call, not by options — pass it as an argument. ` +
233
+ `An app forwarding untrusted input as options cannot steer the store, the query ` +
234
+ `or the count, and this says so rather than dropping it silently.`);
235
+ if (known.has(k))
236
+ continue;
237
+ const near = [...known].find((n) => n !== "extra" && editWithin(k, n, 2));
238
+ throw new Error(`unknown ${what} option ${JSON.stringify(k)}` +
239
+ (near ? ` — did you mean ${JSON.stringify(near)}?` : "") +
240
+ `. The service drops keys it does not know and answers anyway, so this would ` +
241
+ `have looked like it worked.` +
242
+ (known.has("extra")
243
+ ? ` Pass it under \`extra\` if the service accepts it and this client does not know it yet.`
244
+ : ""));
245
+ }
246
+ }
247
+ function editWithin(a, b, max) {
248
+ if (Math.abs(a.length - b.length) > max)
249
+ return false;
250
+ const prev = Array.from({ length: b.length + 1 }, (_, j) => j);
251
+ for (let i = 1; i <= a.length; i++) {
252
+ let diag = prev[0];
253
+ prev[0] = i;
254
+ for (let j = 1; j <= b.length; j++) {
255
+ const tmp = prev[j];
256
+ prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
257
+ diag = tmp;
258
+ }
259
+ }
260
+ return prev[b.length] <= max;
261
+ }
215
262
  function warnOnUnknownFilters(filters) {
216
263
  if (!filters || typeof filters !== "object" || Array.isArray(filters))
217
264
  return;
@@ -257,6 +304,11 @@ class Client {
257
304
  throw new Error("apiKey contains whitespace - check for a stray newline or paste error");
258
305
  if (/[\x00-\x1f\x7f]/.test(key))
259
306
  throw new Error("apiKey contains a control character - check for a stray byte or paste error");
307
+ if (/[^\x00-\x7f]/.test(key)) {
308
+ const bad = key.match(/[^\x00-\x7f]/)?.[0];
309
+ throw new Error(`apiKey contains a non-ASCII character (${JSON.stringify(bad)}) - rich text turns '-' into an ` +
310
+ "en dash; copy the key from a plain-text field");
311
+ }
260
312
  this.apiKey = key;
261
313
  this.base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
262
314
  const tm = opts.timeoutMs;
@@ -265,7 +317,8 @@ class Client {
265
317
  if (this.model && !MODEL_RE.test(this.model)) {
266
318
  throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
267
319
  }
268
- this.defaultUser = opts.userId || DEFAULT_USER;
320
+ assertUsableStoreId(opts.userId);
321
+ this.defaultUser = opts.userId ?? DEFAULT_USER;
269
322
  const mr = opts.maxRetries ?? opts.retries;
270
323
  this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
271
324
  const dl = opts.deadlineMs;
@@ -313,11 +366,8 @@ class Client {
313
366
  return `Client(${this.base}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
314
367
  }
315
368
  uid(userId) {
316
- if (userId !== undefined && (typeof userId !== "string" || !userId.trim())) {
317
- throw new Error(`userId must be a non-blank string; got ${JSON.stringify(userId)}. Omit it to use the client's ` +
318
- "default store, or pass a real store id — anything else would silently write into the default store.");
319
- }
320
- const id = userId || this.defaultUser;
369
+ assertUsableStoreId(userId);
370
+ const id = userId ?? this.defaultUser;
321
371
  warnIfStoreIdCollapses(id);
322
372
  return id;
323
373
  }
@@ -339,6 +389,10 @@ class Client {
339
389
  return this.clone({ model });
340
390
  }
341
391
  withUser(userId) {
392
+ if (userId === undefined) {
393
+ throw new Error("withUser() needs a store id. It was called with undefined — usually a lookup that " +
394
+ "found nothing. Use the client's own default instead of calling withUser at all.");
395
+ }
342
396
  return this.clone({ userId });
343
397
  }
344
398
  withSignal(signal) {
@@ -375,21 +429,27 @@ class Client {
375
429
  return this.post("/api/v1/memory/supersede", { user_id: this.uid(userId), old_memory_id: oldMemoryId, new_content: newContent }, opts.idempotencyKey);
376
430
  }
377
431
  async search(query, userId, limit = 10, opts = {}) {
432
+ checkSearchOpts(opts);
378
433
  warnOnUnknownFilters(opts.filters);
379
434
  checkCount(limit, "limit");
380
- const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
435
+ const { extra, ...known } = opts;
436
+ const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
381
437
  return mergeResults(r);
382
438
  }
383
439
  async searchSelf(query, userId, limit = 10, opts = {}) {
440
+ checkSearchOpts(opts);
384
441
  warnOnUnknownFilters(opts.filters);
385
442
  checkCount(limit, "limit");
386
- const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
443
+ const { extra, ...known } = opts;
444
+ const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
387
445
  return { memories: asRecords(r.memories), self_memories: asRecords(r.self_memories) };
388
446
  }
389
447
  async searchFull(query, userId, limit = 10, opts = {}) {
448
+ checkSearchOpts(opts);
390
449
  warnOnUnknownFilters(opts.filters);
391
450
  checkCount(limit, "limit");
392
- const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
451
+ const { extra, ...known } = opts;
452
+ const r = await this.post("/api/v1/memory/search", { ...extra, ...known, user_id: this.uid(userId), query, max_results: limit });
393
453
  return {
394
454
  ...r,
395
455
  memories: asRecords(r.memories),
@@ -398,6 +458,7 @@ class Client {
398
458
  };
399
459
  }
400
460
  async recall(query, userId, opts = {}) {
461
+ checkOpts(opts, KNOWN_RECALL_KEYS, "recall");
401
462
  const body = { user_id: this.uid(userId), query };
402
463
  if (opts.limit !== undefined) {
403
464
  checkCount(opts.limit, "limit");
@@ -426,10 +487,10 @@ class Client {
426
487
  return this.request("GET", `/api/v1/won/usage?days=${days}`);
427
488
  }
428
489
  get(userId, memoryId) {
429
- if (!memoryId || typeof memoryId !== "string") {
490
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
430
491
  throw new Error("memoryId is required — the id that add/store or listMemories returned.");
431
492
  }
432
- return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId }).then((r) => asObj(r.memory));
493
+ return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then((r) => asObj(r.memory));
433
494
  }
434
495
  listMemories(userId, opts = {}) {
435
496
  const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
@@ -460,16 +521,16 @@ class Client {
460
521
  return out;
461
522
  }
462
523
  getImage(userId, memoryId) {
463
- if (!memoryId || typeof memoryId !== "string") {
524
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
464
525
  throw new Error("memoryId is required — the id that add/store or listImages returned.");
465
526
  }
466
- return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId });
527
+ return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId.trim() });
467
528
  }
468
529
  forgetImage(userId, memoryId, opts = {}) {
469
- if (!memoryId || typeof memoryId !== "string") {
530
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
470
531
  throw new Error("memoryId is required — the id of the memory whose image you want removed.");
471
532
  }
472
- const body = { user_id: this.uid(userId), memory_id: memoryId };
533
+ const body = { user_id: this.uid(userId), memory_id: memoryId.trim() };
473
534
  if (opts.preview)
474
535
  body.preview = true;
475
536
  return this.request("DELETE", "/api/v1/memory/image", body);
@@ -518,10 +579,10 @@ class Client {
518
579
  return this.post("/api/v1/won/revisions", body);
519
580
  }
520
581
  lineage(userId, memoryId) {
521
- if (!memoryId || typeof memoryId !== "string") {
582
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
522
583
  throw new Error("memoryId is required — the memory whose history you want.");
523
584
  }
524
- return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId });
585
+ return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId.trim() });
525
586
  }
526
587
  bySpeaker(speaker, userId, opts = {}) {
527
588
  if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
@@ -789,6 +850,14 @@ class Client {
789
850
  const attempts = this.maxRetries + 1;
790
851
  const logPath = path.split("?")[0];
791
852
  const deadlineAt = this.deadlineAt();
853
+ let payload;
854
+ try {
855
+ payload = body === undefined ? undefined : JSON.stringify(body);
856
+ }
857
+ catch (e) {
858
+ throw new Error(`request body could not be serialized: ${e instanceof Error ? e.message : String(e)}. ` +
859
+ "Pass plain JSON values — an ORM entity, a BigInt or a circular reference cannot be sent.");
860
+ }
792
861
  for (let attempt = 0; attempt < attempts; attempt++) {
793
862
  const start = Date.now();
794
863
  const att = this.beginAttempt(deadlineAt);
@@ -797,7 +866,7 @@ class Client {
797
866
  res = await this.fetchImpl(`${this.base}${path}`, {
798
867
  method,
799
868
  headers: extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers,
800
- body: body === undefined ? undefined : JSON.stringify(body),
869
+ body: payload,
801
870
  signal: att.signal,
802
871
  redirect: "manual",
803
872
  });
@@ -490,7 +490,10 @@ export interface SearchFilters {
490
490
  }
491
491
  /** Test hook — the warn-once set is process-global by design. */
492
492
  export declare function _resetFilterWarnings(): void;
493
- /** Known `search` options; extra fields pass through to the API untouched. */
493
+ /** Known `search` options. An unknown key is refused before anything is sent — the
494
+ * service drops keys it does not recognise and answers normally, so a misspelling
495
+ * could not be told from an option that worked. Put a genuinely new one under
496
+ * `extra`. */
494
497
  export interface SearchOptions {
495
498
  /** Recall caching. A hit inside the TTL bills at 0.1× — but the FIRST call
496
499
  * writes the cache and bills the query tokens at 2× (`5m`) or 3× (`1h`), so
@@ -519,7 +522,9 @@ export interface SearchOptions {
519
522
  */
520
523
  verify?: number;
521
524
  /**
522
- * How many image memories the answer may carry, 0–5. Default 1; 0 asks for none.
525
+ * How many image memories the answer may carry, 0–5. Omit it and the service uses
526
+ * 1; `0` asks for none. The MCP server defaults its own tool to 0 instead, so most
527
+ * searches through it carry no image rows.
523
528
  * Out of range is refused, not clamped — silently cutting 5 to 1 would leave you
524
529
  * believing you got five.
525
530
  *
@@ -527,7 +532,13 @@ export interface SearchOptions {
527
532
  * than answering with no images.
528
533
  */
529
534
  max_images?: number;
530
- [key: string]: unknown;
535
+ /**
536
+ * Fields the service accepts that this version does not know about, merged into the
537
+ * request as written. Every other key is refused, so a typo cannot reach the wire —
538
+ * `verfy: 3` used to be sent, dropped by the service, and answered normally, leaving
539
+ * a caller billed for one pass believing they had bought three.
540
+ */
541
+ extra?: Record<string, unknown>;
531
542
  }
532
543
  /**
533
544
  * Per-call options for a write.
@@ -690,6 +701,11 @@ export declare class Client {
690
701
  /** Supersede an old memory with new content. Payload first, userId last — same shape as add/search. */
691
702
  update(oldMemoryId: string, newContent: string, userId?: string, opts?: WriteOptions): Promise<UpdateResult>;
692
703
  /** Search a store's memories. Returns them most relevant first.
704
+ *
705
+ * `limit` is 5-20, and out of range is refused rather than clamped: asking for 50
706
+ * and silently receiving 20 reads as "that is all there is". The default is 10, so
707
+ * a call that passes no count is unaffected. Before 2.2.35 the count was sent on
708
+ * unchecked.
693
709
  *
694
710
  * `limit` bounds `memories`, not the returned array. On a model that keeps the
695
711
  * assistant's own words separate (Scroll 1.2+) those come back as well, so the
package/dist/wontopos.js CHANGED
@@ -1,4 +1,4 @@
1
- const VERSION = "2.2.37";
1
+ const VERSION = "2.2.38";
2
2
  const RUNTIME = (() => {
3
3
  const p = globalThis?.process;
4
4
  return p?.version ? ` (node/${p.version}; ${p.platform}-${p.arch})` : "";
@@ -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
+ function checkSearchOpts(opts) {
212
+ checkOpts(opts, KNOWN_SEARCH_KEYS, "search");
213
+ }
214
+ function checkOpts(opts, known, what) {
215
+ for (const k of Object.keys(opts)) {
216
+ if (RESERVED_SEARCH_KEYS.has(k))
217
+ throw new Error(`${JSON.stringify(k)} is set by the call, not by options — pass it as an argument. ` +
218
+ `An app forwarding untrusted input as options cannot steer the store, the query ` +
219
+ `or the count, and this says so rather than dropping it silently.`);
220
+ if (known.has(k))
221
+ continue;
222
+ const near = [...known].find((n) => n !== "extra" && editWithin(k, n, 2));
223
+ throw new Error(`unknown ${what} option ${JSON.stringify(k)}` +
224
+ (near ? ` — did you mean ${JSON.stringify(near)}?` : "") +
225
+ `. The service drops keys it does not know and answers anyway, so this would ` +
226
+ `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) {
@@ -360,21 +414,27 @@ export class Client {
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");
@@ -411,10 +472,10 @@ export class Client {
411
472
  return this.request("GET", `/api/v1/won/usage?days=${days}`);
412
473
  }
413
474
  get(userId, memoryId) {
414
- if (!memoryId || typeof memoryId !== "string") {
475
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
415
476
  throw new Error("memoryId is required — the id that add/store or listMemories returned.");
416
477
  }
417
- return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId }).then((r) => asObj(r.memory));
478
+ return this.post("/api/v1/memory/get", { user_id: this.uid(userId), memory_id: memoryId.trim() }).then((r) => asObj(r.memory));
418
479
  }
419
480
  listMemories(userId, opts = {}) {
420
481
  const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
@@ -445,16 +506,16 @@ export class Client {
445
506
  return out;
446
507
  }
447
508
  getImage(userId, memoryId) {
448
- if (!memoryId || typeof memoryId !== "string") {
509
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
449
510
  throw new Error("memoryId is required — the id that add/store or listImages returned.");
450
511
  }
451
- return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId });
512
+ return this.requestBytes("/api/v1/memory/image", { user_id: this.uid(userId), memory_id: memoryId.trim() });
452
513
  }
453
514
  forgetImage(userId, memoryId, opts = {}) {
454
- if (!memoryId || typeof memoryId !== "string") {
515
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
455
516
  throw new Error("memoryId is required — the id of the memory whose image you want removed.");
456
517
  }
457
- const body = { user_id: this.uid(userId), memory_id: memoryId };
518
+ const body = { user_id: this.uid(userId), memory_id: memoryId.trim() };
458
519
  if (opts.preview)
459
520
  body.preview = true;
460
521
  return this.request("DELETE", "/api/v1/memory/image", body);
@@ -503,10 +564,10 @@ export class Client {
503
564
  return this.post("/api/v1/won/revisions", body);
504
565
  }
505
566
  lineage(userId, memoryId) {
506
- if (!memoryId || typeof memoryId !== "string") {
567
+ if (typeof memoryId !== "string" || !memoryId.trim()) {
507
568
  throw new Error("memoryId is required — the memory whose history you want.");
508
569
  }
509
- return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId });
570
+ return this.post("/api/v1/memory/lineage", { user_id: this.uid(userId), memory_id: memoryId.trim() });
510
571
  }
511
572
  bySpeaker(speaker, userId, opts = {}) {
512
573
  if (!speaker || typeof speaker !== "string" || speaker.trim() === "") {
@@ -774,6 +835,14 @@ export class Client {
774
835
  const attempts = this.maxRetries + 1;
775
836
  const logPath = path.split("?")[0];
776
837
  const deadlineAt = this.deadlineAt();
838
+ let payload;
839
+ try {
840
+ payload = body === undefined ? undefined : JSON.stringify(body);
841
+ }
842
+ catch (e) {
843
+ throw new Error(`request body could not be serialized: ${e instanceof Error ? e.message : String(e)}. ` +
844
+ "Pass plain JSON values — an ORM entity, a BigInt or a circular reference cannot be sent.");
845
+ }
777
846
  for (let attempt = 0; attempt < attempts; attempt++) {
778
847
  const start = Date.now();
779
848
  const att = this.beginAttempt(deadlineAt);
@@ -782,7 +851,7 @@ export class Client {
782
851
  res = await this.fetchImpl(`${this.base}${path}`, {
783
852
  method,
784
853
  headers: extraHeaders ? { ...this.headers, ...extraHeaders } : this.headers,
785
- body: body === undefined ? undefined : JSON.stringify(body),
854
+ body: payload,
786
855
  signal: att.signal,
787
856
  redirect: "manual",
788
857
  });
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.38",
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
  }