wontopos 2.2.11 → 2.2.13
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.d.ts +54 -0
- package/dist/wontopos.js +159 -20
- package/package.json +1 -1
package/dist/wontopos.d.ts
CHANGED
|
@@ -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;
|
|
@@ -205,6 +240,11 @@ export declare class Client {
|
|
|
205
240
|
private readonly maxRetries;
|
|
206
241
|
/** The store every call uses unless one passes `userId`. */
|
|
207
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;
|
|
208
248
|
constructor(opts: ClientOptions);
|
|
209
249
|
/** A client whose key comes from `WONTOPOS_API_KEY` (or `WOS_API_KEY`) — keeps
|
|
210
250
|
* keys out of source code. Node/Bun/Deno-with-env only. */
|
|
@@ -259,6 +299,20 @@ export declare class Client {
|
|
|
259
299
|
limit?: number;
|
|
260
300
|
cursor?: string;
|
|
261
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>;
|
|
262
316
|
/** Available models: `[{ id, name, available, memory }, ...]`. Needs no API key. */
|
|
263
317
|
listModels(): Promise<ModelInfo[]>;
|
|
264
318
|
/** Create a store — the `userId` you read and write under. Stores are explicit:
|
package/dist/wontopos.js
CHANGED
|
@@ -32,12 +32,17 @@
|
|
|
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.
|
|
35
|
+
const VERSION = "2.2.13";
|
|
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";
|
|
39
|
-
/**
|
|
40
|
-
const
|
|
39
|
+
/** 429 = rate-limited BEFORE processing → always safe to retry (no write, no bill). */
|
|
40
|
+
const RETRY_ALWAYS = new Set([429]);
|
|
41
|
+
/** 502/503 are ambiguous for a write (gateway may return them AFTER the backend
|
|
42
|
+
* processed + billed the request), so retry them only for idempotent methods —
|
|
43
|
+
* a retried POST could double-store / double-bill. */
|
|
44
|
+
const RETRY_IF_IDEMPOTENT = new Set([502, 503]);
|
|
45
|
+
const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
41
46
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
42
47
|
/** Refuse to buffer absurd responses (real ones are a few KB) — protects the
|
|
43
48
|
* process if a self-hosted baseUrl points somewhere broken or hostile. */
|
|
@@ -48,17 +53,77 @@ const MODEL_RE = /^[A-Za-z0-9._-]+$/;
|
|
|
48
53
|
function maskKey(key) {
|
|
49
54
|
return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
|
|
50
55
|
}
|
|
56
|
+
function parseRateLimit(h) {
|
|
57
|
+
const num = (name) => {
|
|
58
|
+
const v = h.get(name);
|
|
59
|
+
if (v == null || v.trim() === "")
|
|
60
|
+
return null; // Number("") is 0 — treat blank as absent
|
|
61
|
+
const n = Number(v);
|
|
62
|
+
return Number.isFinite(n) ? n : null;
|
|
63
|
+
};
|
|
64
|
+
const limit = num("X-RateLimit-Limit");
|
|
65
|
+
const remaining = num("X-RateLimit-Remaining");
|
|
66
|
+
const reset = num("X-RateLimit-Reset");
|
|
67
|
+
if (limit === null && remaining === null && reset === null)
|
|
68
|
+
return null;
|
|
69
|
+
return { limit, remaining, reset };
|
|
70
|
+
}
|
|
51
71
|
export class WosError extends Error {
|
|
52
72
|
status;
|
|
53
73
|
/** The server's id for the request when it sent one — include it when contacting support. */
|
|
54
74
|
requestId;
|
|
55
75
|
constructor(status, message, requestId) {
|
|
56
76
|
super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`);
|
|
57
|
-
this.name =
|
|
77
|
+
this.name = new.target.name; // the concrete subclass name (RateLimitError, …)
|
|
58
78
|
this.status = status;
|
|
59
79
|
this.requestId = requestId;
|
|
60
80
|
}
|
|
61
81
|
}
|
|
82
|
+
// Typed subclasses so callers can branch on the failure — `catch (e) { if (e instanceof
|
|
83
|
+
// RateLimitError) … }`. Each is a WosError, so a broad `instanceof WosError` still works.
|
|
84
|
+
/** The request never got a response (DNS/TLS/timeout/connection). `status` is 0. */
|
|
85
|
+
export class APIConnectionError extends WosError {
|
|
86
|
+
}
|
|
87
|
+
/** 400 — the request was malformed (bad arguments). */
|
|
88
|
+
export class BadRequestError extends WosError {
|
|
89
|
+
}
|
|
90
|
+
/** 401 — the API key is missing, wrong, or revoked. */
|
|
91
|
+
export class AuthenticationError extends WosError {
|
|
92
|
+
}
|
|
93
|
+
/** 402 — no card on file or the balance is depleted. Top up to continue. */
|
|
94
|
+
export class PaymentRequiredError extends WosError {
|
|
95
|
+
}
|
|
96
|
+
/** 403 — the key/model isn't allowed to do this. */
|
|
97
|
+
export class PermissionDeniedError extends WosError {
|
|
98
|
+
}
|
|
99
|
+
/** 404 — the store or resource doesn't exist (create the store first). */
|
|
100
|
+
export class NotFoundError extends WosError {
|
|
101
|
+
}
|
|
102
|
+
/** 409 — a concurrent write to the same store. Retry. */
|
|
103
|
+
export class ConflictError extends WosError {
|
|
104
|
+
}
|
|
105
|
+
/** 429 — too many requests. Back off and retry (the client already retries these). */
|
|
106
|
+
export class RateLimitError extends WosError {
|
|
107
|
+
}
|
|
108
|
+
/** 5xx — the service failed. Safe to retry. */
|
|
109
|
+
export class ServerError extends WosError {
|
|
110
|
+
}
|
|
111
|
+
const STATUS_ERRORS = {
|
|
112
|
+
400: BadRequestError,
|
|
113
|
+
401: AuthenticationError,
|
|
114
|
+
402: PaymentRequiredError,
|
|
115
|
+
403: PermissionDeniedError,
|
|
116
|
+
404: NotFoundError,
|
|
117
|
+
409: ConflictError,
|
|
118
|
+
429: RateLimitError,
|
|
119
|
+
};
|
|
120
|
+
/** Build the most specific WosError subclass for an HTTP status. */
|
|
121
|
+
export function errorFor(status, message, requestId) {
|
|
122
|
+
const Cls = status === 0
|
|
123
|
+
? APIConnectionError
|
|
124
|
+
: STATUS_ERRORS[status] ?? (status >= 500 && status < 600 ? ServerError : WosError);
|
|
125
|
+
return new Cls(status, message, requestId);
|
|
126
|
+
}
|
|
62
127
|
const DEFAULT_USER = "default";
|
|
63
128
|
/** Wontopos memory client. Store and recall memories, isolated per `userId`. */
|
|
64
129
|
export class Client {
|
|
@@ -70,6 +135,13 @@ export class Client {
|
|
|
70
135
|
maxRetries;
|
|
71
136
|
/** The store every call uses unless one passes `userId`. */
|
|
72
137
|
defaultUser;
|
|
138
|
+
_rateLimit = null;
|
|
139
|
+
/** Quota from the MOST RECENT call: `{ limit, remaining, reset }` (or `null` before
|
|
140
|
+
* the first call). Read it to self-throttle — the client already retries 429s, but
|
|
141
|
+
* this lets you slow down before hitting the wall. */
|
|
142
|
+
get rateLimit() {
|
|
143
|
+
return this._rateLimit;
|
|
144
|
+
}
|
|
73
145
|
constructor(opts) {
|
|
74
146
|
// Trim the key and reject inner whitespace — a stray newline from a file or
|
|
75
147
|
// env var otherwise turns into a mystery 401 (or a mangled header).
|
|
@@ -86,7 +158,11 @@ export class Client {
|
|
|
86
158
|
throw new Error(`invalid model name: ${JSON.stringify(this.model)} (letters, digits, '.', '_', '-' only)`);
|
|
87
159
|
}
|
|
88
160
|
this.defaultUser = opts.userId || DEFAULT_USER;
|
|
89
|
-
|
|
161
|
+
// Coerce a non-finite maxRetries (NaN/Infinity) back to the default — otherwise
|
|
162
|
+
// NaN makes `attempts = NaN + 1` and the loop runs zero requests then throws
|
|
163
|
+
// "retries exhausted"; Infinity would retry forever.
|
|
164
|
+
const mr = opts.maxRetries;
|
|
165
|
+
this.maxRetries = typeof mr === "number" && Number.isFinite(mr) ? Math.max(0, Math.floor(mr)) : 2;
|
|
90
166
|
this.headers = {
|
|
91
167
|
"X-API-Key": this.apiKey,
|
|
92
168
|
"Content-Type": "application/json",
|
|
@@ -98,8 +174,9 @@ export class Client {
|
|
|
98
174
|
// An API key on plain HTTP travels readable by anyone on the path. Loopback
|
|
99
175
|
// is fine (local dev / self-hosted on the same box); anything else gets a
|
|
100
176
|
// warning, not an error, so private-network gateways keep working.
|
|
101
|
-
const host = this.base.replace(/^https
|
|
102
|
-
|
|
177
|
+
const host = this.base.replace(/^https?:\/\//i, "").split(/[/:]/)[0].toLowerCase();
|
|
178
|
+
// Scheme compare is case-insensitive — `HTTP://` connects in plaintext too.
|
|
179
|
+
if (/^http:\/\//i.test(this.base) && !LOOPBACK_HOSTS.has(host)) {
|
|
103
180
|
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
104
181
|
}
|
|
105
182
|
}
|
|
@@ -107,10 +184,13 @@ export class Client {
|
|
|
107
184
|
* keys out of source code. Node/Bun/Deno-with-env only. */
|
|
108
185
|
static fromEnv(opts = {}) {
|
|
109
186
|
const env = globalThis?.process?.env;
|
|
110
|
-
|
|
111
|
-
|
|
187
|
+
// First env var with a NON-EMPTY value wins — an empty WONTOPOS_API_KEY must
|
|
188
|
+
// not shadow a valid WOS_API_KEY (`??` only skips null/undefined, not "").
|
|
189
|
+
const key = [env?.WONTOPOS_API_KEY, env?.WOS_API_KEY].find((v) => v && v.trim());
|
|
190
|
+
if (!key)
|
|
112
191
|
throw new Error("set WONTOPOS_API_KEY (or WOS_API_KEY) in the environment");
|
|
113
|
-
|
|
192
|
+
// apiKey LAST so a stray apiKey in opts can't override the env key.
|
|
193
|
+
return new Client({ ...opts, apiKey: key });
|
|
114
194
|
}
|
|
115
195
|
/** Never expose the key: `JSON.stringify(client)` gets the redacted view. */
|
|
116
196
|
toJSON() {
|
|
@@ -177,7 +257,9 @@ export class Client {
|
|
|
177
257
|
// ----- read -----
|
|
178
258
|
/** Semantic search over a store's memories. Returns memories, most relevant first. */
|
|
179
259
|
async search(query, userId, limit = 10, opts = {}) {
|
|
180
|
-
|
|
260
|
+
// Reserved fields win over ...opts: an app that forwards untrusted input as
|
|
261
|
+
// opts must not be able to override the store (user_id), query, or limit.
|
|
262
|
+
const r = await this.post("/api/v1/memory/search", { ...opts, user_id: this.uid(userId), query, max_results: limit });
|
|
181
263
|
return (r.memories ?? []);
|
|
182
264
|
}
|
|
183
265
|
/** One-call LLM context: short-term turns + long-term matches + surrounding context. */
|
|
@@ -215,6 +297,40 @@ export class Client {
|
|
|
215
297
|
body.cursor = opts.cursor;
|
|
216
298
|
return this.post("/api/v1/memory/list", body);
|
|
217
299
|
}
|
|
300
|
+
/** Async-iterate every stored memory in a store, paging under the hood — no cursor
|
|
301
|
+
* bookkeeping. Original text + metadata only, never vectors.
|
|
302
|
+
*
|
|
303
|
+
* for await (const m of mem.iterMemories()) console.log(m.id, m.content);
|
|
304
|
+
*/
|
|
305
|
+
async *iterMemories(userId, opts = {}) {
|
|
306
|
+
let cursor;
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
for (;;) {
|
|
309
|
+
const page = await this.listMemories(userId, { limit: opts.pageSize ?? 100, cursor });
|
|
310
|
+
for (const m of page.memories ?? [])
|
|
311
|
+
yield m;
|
|
312
|
+
const next = page.next_cursor ?? undefined;
|
|
313
|
+
// Stop on last page OR a server that repeats a cursor (would loop forever).
|
|
314
|
+
if (!next || seen.has(next))
|
|
315
|
+
break;
|
|
316
|
+
seen.add(next);
|
|
317
|
+
cursor = next;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Collect ALL of a store's memories into an array (original text + metadata, no vectors). */
|
|
321
|
+
async exportMemories(userId) {
|
|
322
|
+
const out = [];
|
|
323
|
+
for await (const m of this.iterMemories(userId))
|
|
324
|
+
out.push(m);
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
/** Check connectivity AND that the API key works. Resolves `true`, or rejects — an
|
|
328
|
+
* `AuthenticationError` (bad key), `PaymentRequiredError` (valid key but no card /
|
|
329
|
+
* depleted balance), or `APIConnectionError` (unreachable). Makes one metered request. */
|
|
330
|
+
async ping() {
|
|
331
|
+
await this.request("GET", "/api/v1/memory/collections");
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
218
334
|
// ----- models -----
|
|
219
335
|
/** Available models: `[{ id, name, available, memory }, ...]`. Needs no API key. */
|
|
220
336
|
async listModels() {
|
|
@@ -236,8 +352,8 @@ export class Client {
|
|
|
236
352
|
}
|
|
237
353
|
/** Delete a store and ALL its memories. Returns `{ user_id, status }`. */
|
|
238
354
|
deleteStore(userId) {
|
|
239
|
-
if (!userId)
|
|
240
|
-
throw new Error("userId is required — deleteStore never falls back to the default store.");
|
|
355
|
+
if (!userId || !userId.trim())
|
|
356
|
+
throw new Error("userId is required (non-blank) — deleteStore never falls back to the default store.");
|
|
241
357
|
return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
|
|
242
358
|
}
|
|
243
359
|
// ----- speakers (who said it) -----
|
|
@@ -269,8 +385,8 @@ export class Client {
|
|
|
269
385
|
/** Delete ALL memories for a store (GDPR erase). `userId` is required on purpose -
|
|
270
386
|
* this is destructive, so it never falls back to the default store. */
|
|
271
387
|
deleteAll(userId) {
|
|
272
|
-
if (!userId)
|
|
273
|
-
throw new Error("userId is required for deleteAll —
|
|
388
|
+
if (!userId || !userId.trim())
|
|
389
|
+
throw new Error("userId is required (non-blank) for deleteAll — a blank/whitespace id would wipe the default store.");
|
|
274
390
|
return this.post("/api/v1/memory/forget", { user_id: userId });
|
|
275
391
|
}
|
|
276
392
|
// ----- internal -----
|
|
@@ -283,6 +399,10 @@ export class Client {
|
|
|
283
399
|
const s = Number(retryAfter);
|
|
284
400
|
if (Number.isFinite(s) && s >= 0)
|
|
285
401
|
return Math.min(30_000, s * 1000);
|
|
402
|
+
// HTTP-date form (RFC 9110), e.g. "Wed, 21 Oct 2015 07:28:00 GMT".
|
|
403
|
+
const t = Date.parse(retryAfter);
|
|
404
|
+
if (Number.isFinite(t))
|
|
405
|
+
return Math.min(30_000, Math.max(0, t - Date.now()));
|
|
286
406
|
}
|
|
287
407
|
return Math.min(8_000, 500 * 2 ** attempt) + Math.random() * 250;
|
|
288
408
|
}
|
|
@@ -342,19 +462,38 @@ export class Client {
|
|
|
342
462
|
await new Promise((r) => setTimeout(r, this.backoffMs(attempt)));
|
|
343
463
|
continue;
|
|
344
464
|
}
|
|
345
|
-
throw new
|
|
465
|
+
throw new APIConnectionError(0, timedOut ? `request timed out after ${this.timeoutMs}ms` : `network error: ${e?.message ?? e}`);
|
|
346
466
|
}
|
|
347
|
-
|
|
348
|
-
|
|
467
|
+
const retryable = RETRY_ALWAYS.has(res.status) ||
|
|
468
|
+
(RETRY_IF_IDEMPOTENT.has(res.status) && IDEMPOTENT_METHODS.has(method.toUpperCase()));
|
|
469
|
+
if (retryable && attempt + 1 < attempts) {
|
|
470
|
+
clearTimeout(timer);
|
|
349
471
|
const retryAfter = res.headers.get("Retry-After");
|
|
350
472
|
void res.body?.cancel();
|
|
351
473
|
await new Promise((r) => setTimeout(r, this.backoffMs(attempt, retryAfter)));
|
|
352
474
|
continue;
|
|
353
475
|
}
|
|
354
476
|
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
477
|
+
clearTimeout(timer);
|
|
355
478
|
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
356
479
|
}
|
|
357
|
-
|
|
480
|
+
this._rateLimit = parseRateLimit(res.headers) ?? this._rateLimit;
|
|
481
|
+
// Keep the abort timer alive THROUGH the body read: fetch resolves on
|
|
482
|
+
// headers, so a server that sends headers then trickles/hangs the body
|
|
483
|
+
// would otherwise stall forever. An abort mid-body surfaces as a timeout.
|
|
484
|
+
let text;
|
|
485
|
+
try {
|
|
486
|
+
text = await this.readCapped(res);
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
if (e?.name === "AbortError" || ctrl.signal.aborted) {
|
|
490
|
+
throw new APIConnectionError(0, `request timed out after ${this.timeoutMs}ms`);
|
|
491
|
+
}
|
|
492
|
+
throw e;
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
clearTimeout(timer);
|
|
496
|
+
}
|
|
358
497
|
if (!res.ok) {
|
|
359
498
|
// Server returns either Anthropic-style envelope
|
|
360
499
|
// {"type":"error","error":{"type":...,"message":...,"request_id":...}}
|
|
@@ -379,7 +518,7 @@ export class Client {
|
|
|
379
518
|
catch {
|
|
380
519
|
/* keep raw text */
|
|
381
520
|
}
|
|
382
|
-
throw
|
|
521
|
+
throw errorFor(res.status, msg, requestId);
|
|
383
522
|
}
|
|
384
523
|
return text ? JSON.parse(text) : null;
|
|
385
524
|
}
|
package/package.json
CHANGED