wontopos 2.2.10 → 2.2.12

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.
@@ -32,12 +32,47 @@
32
32
  * runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
33
33
  * `Client.fromEnv()` over keys in source code.
34
34
  */
35
+ /** Quota from a response's `X-RateLimit-*` headers, or `null` if absent. */
36
+ export interface RateLimit {
37
+ limit: number | null;
38
+ remaining: number | null;
39
+ reset: number | null;
40
+ }
35
41
  export declare class WosError extends Error {
36
42
  readonly status: number;
37
43
  /** The server's id for the request when it sent one — include it when contacting support. */
38
44
  readonly requestId?: string;
39
45
  constructor(status: number, message: string, requestId?: string);
40
46
  }
47
+ /** The request never got a response (DNS/TLS/timeout/connection). `status` is 0. */
48
+ export declare class APIConnectionError extends WosError {
49
+ }
50
+ /** 400 — the request was malformed (bad arguments). */
51
+ export declare class BadRequestError extends WosError {
52
+ }
53
+ /** 401 — the API key is missing, wrong, or revoked. */
54
+ export declare class AuthenticationError extends WosError {
55
+ }
56
+ /** 402 — no card on file or the balance is depleted. Top up to continue. */
57
+ export declare class PaymentRequiredError extends WosError {
58
+ }
59
+ /** 403 — the key/model isn't allowed to do this. */
60
+ export declare class PermissionDeniedError extends WosError {
61
+ }
62
+ /** 404 — the store or resource doesn't exist (create the store first). */
63
+ export declare class NotFoundError extends WosError {
64
+ }
65
+ /** 409 — a concurrent write to the same store. Retry. */
66
+ export declare class ConflictError extends WosError {
67
+ }
68
+ /** 429 — too many requests. Back off and retry (the client already retries these). */
69
+ export declare class RateLimitError extends WosError {
70
+ }
71
+ /** 5xx — the service failed. Safe to retry. */
72
+ export declare class ServerError extends WosError {
73
+ }
74
+ /** Build the most specific WosError subclass for an HTTP status. */
75
+ export declare function errorFor(status: number, message: string, requestId?: string): WosError;
41
76
  export interface Usage {
42
77
  input_tokens?: number;
43
78
  output_tokens?: number;
@@ -123,6 +158,14 @@ export interface StatsResult {
123
158
  short_term_turns?: number;
124
159
  [key: string]: unknown;
125
160
  }
161
+ /** `listMemories` → one page of a store's raw memories (original text + metadata, no vector). */
162
+ export interface MemoryPage {
163
+ memories: Memory[];
164
+ count: number;
165
+ /** Pass back as `cursor` for the next page; `null` on the last page. */
166
+ next_cursor: string | null;
167
+ [key: string]: unknown;
168
+ }
126
169
  export interface ModelInfo {
127
170
  id: string;
128
171
  name: string;
@@ -197,6 +240,11 @@ export declare class Client {
197
240
  private readonly maxRetries;
198
241
  /** The store every call uses unless one passes `userId`. */
199
242
  private readonly defaultUser;
243
+ private _rateLimit;
244
+ /** Quota from the MOST RECENT call: `{ limit, remaining, reset }` (or `null` before
245
+ * the first call). Read it to self-throttle — the client already retries 429s, but
246
+ * this lets you slow down before hitting the wall. */
247
+ get rateLimit(): RateLimit | null;
200
248
  constructor(opts: ClientOptions);
201
249
  /** A client whose key comes from `WONTOPOS_API_KEY` (or `WOS_API_KEY`) — keeps
202
250
  * keys out of source code. Node/Bun/Deno-with-env only. */
@@ -235,6 +283,36 @@ export declare class Client {
235
283
  history(userId?: string): Promise<HistoryTurn[]>;
236
284
  /** Memory counts for a store: { total_memories, short_term_turns }. */
237
285
  stats(userId?: string): Promise<StatsResult>;
286
+ /** List a store's stored memories — the original text plus metadata, never the
287
+ * vector. Paginated: pass the returned `next_cursor` back as `cursor` for the next
288
+ * page; a `null` cursor means the last page. Use it to browse or export a store.
289
+ *
290
+ * let cursor: string | null = null;
291
+ * const all: Memory[] = [];
292
+ * do {
293
+ * const page = await mem.listMemories(undefined, { cursor: cursor ?? undefined });
294
+ * all.push(...page.memories);
295
+ * cursor = page.next_cursor;
296
+ * } while (cursor);
297
+ */
298
+ listMemories(userId?: string, opts?: {
299
+ limit?: number;
300
+ cursor?: string;
301
+ }): Promise<MemoryPage>;
302
+ /** Async-iterate every stored memory in a store, paging under the hood — no cursor
303
+ * bookkeeping. Original text + metadata only, never vectors.
304
+ *
305
+ * for await (const m of mem.iterMemories()) console.log(m.id, m.content);
306
+ */
307
+ iterMemories(userId?: string, opts?: {
308
+ pageSize?: number;
309
+ }): AsyncGenerator<Memory>;
310
+ /** Collect ALL of a store's memories into an array (original text + metadata, no vectors). */
311
+ exportMemories(userId?: string): Promise<Memory[]>;
312
+ /** Check connectivity AND that the API key works. Resolves `true`, or rejects — an
313
+ * `AuthenticationError` (bad key), `PaymentRequiredError` (valid key but no card /
314
+ * depleted balance), or `APIConnectionError` (unreachable). Makes one metered request. */
315
+ ping(): Promise<boolean>;
238
316
  /** Available models: `[{ id, name, available, memory }, ...]`. Needs no API key. */
239
317
  listModels(): Promise<ModelInfo[]>;
240
318
  /** Create a store — the `userId` you read and write under. Stores are explicit:
package/dist/wontopos.js CHANGED
@@ -32,7 +32,7 @@
32
32
  * runtime's (never disabled by this SDK; Node 18+ floors TLS at 1.2). Prefer
33
33
  * `Client.fromEnv()` over keys in source code.
34
34
  */
35
- const VERSION = "2.2.10";
35
+ const VERSION = "2.2.12";
36
36
  const USER_AGENT = `wontopos-node/${VERSION}`;
37
37
  const DEFAULT_BASE_URL = "https://api.wontopos.com";
38
38
  const DEFAULT_MODEL = "tablet-1";
@@ -48,17 +48,77 @@ const MODEL_RE = /^[A-Za-z0-9._-]+$/;
48
48
  function maskKey(key) {
49
49
  return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
50
50
  }
51
+ function parseRateLimit(h) {
52
+ const num = (name) => {
53
+ const v = h.get(name);
54
+ if (v == null || v.trim() === "")
55
+ return null; // Number("") is 0 — treat blank as absent
56
+ const n = Number(v);
57
+ return Number.isFinite(n) ? n : null;
58
+ };
59
+ const limit = num("X-RateLimit-Limit");
60
+ const remaining = num("X-RateLimit-Remaining");
61
+ const reset = num("X-RateLimit-Reset");
62
+ if (limit === null && remaining === null && reset === null)
63
+ return null;
64
+ return { limit, remaining, reset };
65
+ }
51
66
  export class WosError extends Error {
52
67
  status;
53
68
  /** The server's id for the request when it sent one — include it when contacting support. */
54
69
  requestId;
55
70
  constructor(status, message, requestId) {
56
71
  super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`);
57
- this.name = "WosError";
72
+ this.name = new.target.name; // the concrete subclass name (RateLimitError, …)
58
73
  this.status = status;
59
74
  this.requestId = requestId;
60
75
  }
61
76
  }
77
+ // Typed subclasses so callers can branch on the failure — `catch (e) { if (e instanceof
78
+ // RateLimitError) … }`. Each is a WosError, so a broad `instanceof WosError` still works.
79
+ /** The request never got a response (DNS/TLS/timeout/connection). `status` is 0. */
80
+ export class APIConnectionError extends WosError {
81
+ }
82
+ /** 400 — the request was malformed (bad arguments). */
83
+ export class BadRequestError extends WosError {
84
+ }
85
+ /** 401 — the API key is missing, wrong, or revoked. */
86
+ export class AuthenticationError extends WosError {
87
+ }
88
+ /** 402 — no card on file or the balance is depleted. Top up to continue. */
89
+ export class PaymentRequiredError extends WosError {
90
+ }
91
+ /** 403 — the key/model isn't allowed to do this. */
92
+ export class PermissionDeniedError extends WosError {
93
+ }
94
+ /** 404 — the store or resource doesn't exist (create the store first). */
95
+ export class NotFoundError extends WosError {
96
+ }
97
+ /** 409 — a concurrent write to the same store. Retry. */
98
+ export class ConflictError extends WosError {
99
+ }
100
+ /** 429 — too many requests. Back off and retry (the client already retries these). */
101
+ export class RateLimitError extends WosError {
102
+ }
103
+ /** 5xx — the service failed. Safe to retry. */
104
+ export class ServerError extends WosError {
105
+ }
106
+ const STATUS_ERRORS = {
107
+ 400: BadRequestError,
108
+ 401: AuthenticationError,
109
+ 402: PaymentRequiredError,
110
+ 403: PermissionDeniedError,
111
+ 404: NotFoundError,
112
+ 409: ConflictError,
113
+ 429: RateLimitError,
114
+ };
115
+ /** Build the most specific WosError subclass for an HTTP status. */
116
+ export function errorFor(status, message, requestId) {
117
+ const Cls = status === 0
118
+ ? APIConnectionError
119
+ : STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
120
+ return new Cls(status, message, requestId);
121
+ }
62
122
  const DEFAULT_USER = "default";
63
123
  /** Wontopos memory client. Store and recall memories, isolated per `userId`. */
64
124
  export class Client {
@@ -70,6 +130,13 @@ export class Client {
70
130
  maxRetries;
71
131
  /** The store every call uses unless one passes `userId`. */
72
132
  defaultUser;
133
+ _rateLimit = null;
134
+ /** Quota from the MOST RECENT call: `{ limit, remaining, reset }` (or `null` before
135
+ * the first call). Read it to self-throttle — the client already retries 429s, but
136
+ * this lets you slow down before hitting the wall. */
137
+ get rateLimit() {
138
+ return this._rateLimit;
139
+ }
73
140
  constructor(opts) {
74
141
  // Trim the key and reject inner whitespace — a stray newline from a file or
75
142
  // env var otherwise turns into a mystery 401 (or a mangled header).
@@ -197,6 +264,54 @@ export class Client {
197
264
  stats(userId) {
198
265
  return this.post("/api/v1/memory/stats", { user_id: this.uid(userId) });
199
266
  }
267
+ /** List a store's stored memories — the original text plus metadata, never the
268
+ * vector. Paginated: pass the returned `next_cursor` back as `cursor` for the next
269
+ * page; a `null` cursor means the last page. Use it to browse or export a store.
270
+ *
271
+ * let cursor: string | null = null;
272
+ * const all: Memory[] = [];
273
+ * do {
274
+ * const page = await mem.listMemories(undefined, { cursor: cursor ?? undefined });
275
+ * all.push(...page.memories);
276
+ * cursor = page.next_cursor;
277
+ * } while (cursor);
278
+ */
279
+ listMemories(userId, opts = {}) {
280
+ const body = { user_id: this.uid(userId), limit: opts.limit ?? 100 };
281
+ if (opts.cursor)
282
+ body.cursor = opts.cursor;
283
+ return this.post("/api/v1/memory/list", body);
284
+ }
285
+ /** Async-iterate every stored memory in a store, paging under the hood — no cursor
286
+ * bookkeeping. Original text + metadata only, never vectors.
287
+ *
288
+ * for await (const m of mem.iterMemories()) console.log(m.id, m.content);
289
+ */
290
+ async *iterMemories(userId, opts = {}) {
291
+ let cursor;
292
+ for (;;) {
293
+ const page = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
294
+ for (const m of page.memories ?? [])
295
+ yield m;
296
+ cursor = page.next_cursor ?? undefined;
297
+ if (!cursor)
298
+ break;
299
+ }
300
+ }
301
+ /** Collect ALL of a store's memories into an array (original text + metadata, no vectors). */
302
+ async exportMemories(userId) {
303
+ const out = [];
304
+ for await (const m of this.iterMemories(userId))
305
+ out.push(m);
306
+ return out;
307
+ }
308
+ /** Check connectivity AND that the API key works. Resolves `true`, or rejects — an
309
+ * `AuthenticationError` (bad key), `PaymentRequiredError` (valid key but no card /
310
+ * depleted balance), or `APIConnectionError` (unreachable). Makes one metered request. */
311
+ async ping() {
312
+ await this.request("GET", "/api/v1/memory/collections");
313
+ return true;
314
+ }
200
315
  // ----- models -----
201
316
  /** Available models: `[{ id, name, available, memory }, ...]`. Needs no API key. */
202
317
  async listModels() {
@@ -324,7 +439,7 @@ export class Client {
324
439
  await new Promise((r) => setTimeout(r, this.backoffMs(attempt)));
325
440
  continue;
326
441
  }
327
- throw new WosError(0, timedOut ? `request timed out after ${this.timeoutMs}ms` : `network error: ${e?.message ?? e}`);
442
+ throw new APIConnectionError(0, timedOut ? `request timed out after ${this.timeoutMs}ms` : `network error: ${e?.message ?? e}`);
328
443
  }
329
444
  clearTimeout(timer);
330
445
  if (RETRY_STATUSES.has(res.status) && attempt + 1 < attempts) {
@@ -336,6 +451,7 @@ export class Client {
336
451
  if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
337
452
  throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
338
453
  }
454
+ this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
339
455
  const text = await this.readCapped(res);
340
456
  if (!res.ok) {
341
457
  // Server returns either Anthropic-style envelope
@@ -361,7 +477,7 @@ export class Client {
361
477
  catch {
362
478
  /* keep raw text */
363
479
  }
364
- throw new WosError(res.status, msg, requestId);
480
+ throw errorFor(res.status, msg, requestId);
365
481
  }
366
482
  return text ? JSON.parse(text) : null;
367
483
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wontopos",
3
- "version": "2.2.10",
3
+ "version": "2.2.12",
4
4
  "description": "Wontopos — long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100× lower LLM bill.",
5
5
  "keywords": [
6
6
  "memory",