@cilow/sdk 0.2.1 → 0.3.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.
Files changed (84) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +109 -492
  3. package/dist/abstain.d.ts +43 -0
  4. package/dist/abstain.d.ts.map +1 -0
  5. package/dist/abstain.js +42 -0
  6. package/dist/abstain.js.map +1 -0
  7. package/dist/adapters/anthropic.d.ts +57 -0
  8. package/dist/adapters/anthropic.d.ts.map +1 -0
  9. package/dist/adapters/anthropic.js +57 -0
  10. package/dist/adapters/anthropic.js.map +1 -0
  11. package/dist/adapters/index.d.ts +16 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +16 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/langchain.d.ts +62 -0
  16. package/dist/adapters/langchain.d.ts.map +1 -0
  17. package/dist/adapters/langchain.js +68 -0
  18. package/dist/adapters/langchain.js.map +1 -0
  19. package/dist/adapters/memory.d.ts +105 -0
  20. package/dist/adapters/memory.d.ts.map +1 -0
  21. package/dist/adapters/memory.js +105 -0
  22. package/dist/adapters/memory.js.map +1 -0
  23. package/dist/adapters/openai.d.ts +56 -0
  24. package/dist/adapters/openai.d.ts.map +1 -0
  25. package/dist/adapters/openai.js +64 -0
  26. package/dist/adapters/openai.js.map +1 -0
  27. package/dist/adapters/remaining.d.ts +52 -0
  28. package/dist/adapters/remaining.d.ts.map +1 -0
  29. package/dist/adapters/remaining.js +67 -0
  30. package/dist/adapters/remaining.js.map +1 -0
  31. package/dist/client.d.ts +512 -173
  32. package/dist/client.d.ts.map +1 -0
  33. package/dist/client.js +648 -504
  34. package/dist/client.js.map +1 -1
  35. package/dist/errors.d.ts +25 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +28 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/hash.d.ts +13 -0
  40. package/dist/hash.d.ts.map +1 -0
  41. package/dist/hash.js +92 -0
  42. package/dist/hash.js.map +1 -0
  43. package/dist/index.d.ts +18 -109
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +16 -876
  46. package/dist/index.js.map +1 -1
  47. package/dist/types.d.ts +809 -486
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +18 -17
  50. package/dist/types.js.map +1 -1
  51. package/package.json +30 -103
  52. package/dist/client.d.mts +0 -224
  53. package/dist/client.mjs +0 -505
  54. package/dist/client.mjs.map +0 -1
  55. package/dist/index.d.mts +0 -111
  56. package/dist/index.mjs +0 -863
  57. package/dist/index.mjs.map +0 -1
  58. package/dist/providers/langchain.js +0 -821
  59. package/dist/providers/langchain.js.map +0 -1
  60. package/dist/providers/langchain.mjs +0 -816
  61. package/dist/providers/langchain.mjs.map +0 -1
  62. package/dist/providers/openai.js +0 -737
  63. package/dist/providers/openai.js.map +0 -1
  64. package/dist/providers/openai.mjs +0 -732
  65. package/dist/providers/openai.mjs.map +0 -1
  66. package/dist/providers/vercel.js +0 -866
  67. package/dist/providers/vercel.js.map +0 -1
  68. package/dist/providers/vercel.mjs +0 -860
  69. package/dist/providers/vercel.mjs.map +0 -1
  70. package/dist/react/hooks.d.mts +0 -327
  71. package/dist/react/hooks.d.ts +0 -327
  72. package/dist/react/hooks.js +0 -1183
  73. package/dist/react/hooks.js.map +0 -1
  74. package/dist/react/hooks.mjs +0 -1172
  75. package/dist/react/hooks.mjs.map +0 -1
  76. package/dist/types.d.mts +0 -494
  77. package/dist/types.mjs +0 -14
  78. package/dist/types.mjs.map +0 -1
  79. package/dist/websocket.d.mts +0 -160
  80. package/dist/websocket.d.ts +0 -160
  81. package/dist/websocket.js +0 -342
  82. package/dist/websocket.js.map +0 -1
  83. package/dist/websocket.mjs +0 -339
  84. package/dist/websocket.mjs.map +0 -1
package/dist/client.js CHANGED
@@ -1,509 +1,653 @@
1
- 'use strict';
2
-
3
- // src/client.ts
4
- async function fetchWithRetry(url, options, config) {
5
- const controller = new AbortController();
6
- const timeoutId = setTimeout(() => controller.abort(), config.timeout);
7
- let lastError = null;
8
- for (let attempt = 0; attempt <= config.retries; attempt++) {
9
- try {
10
- const response = await fetch(url, {
11
- ...options,
12
- signal: controller.signal
13
- });
14
- clearTimeout(timeoutId);
15
- if (!response.ok) {
16
- const errorBody = await response.text();
17
- let apiError;
1
+ import { CilowError } from "./errors.js";
2
+ import { canonicalJson, sha256Hex } from "./hash.js";
3
+ /** Current wall clock as epoch MICROSECONDS — the unit every Cilow time field uses. */
4
+ export function nowMicros() {
5
+ return Date.now() * 1000;
6
+ }
7
+ /**
8
+ * The idempotency key `ingest` sends when you pass no `sourceId`: a sha256 CONTENT HASH.
9
+ * The wire requires `source_id` (`RememberRequest.source_id` / `IngestAnyRequest.source_id` are
10
+ * non-optional) and a timestamp would make every retry a fresh write; hashing the content makes a
11
+ * retry of the same payload an idempotent no-op (the engine dedups under the tenant on this key).
12
+ * Pre-image: `"<form>:<content>"`, facts as canonical JSON (sorted keys, compact). The Python SDK's
13
+ * `ingest_source_id` computes the identical key.
14
+ */
15
+ export function ingestSourceId(content) {
16
+ const pre = "facts" in content
17
+ ? `facts:${canonicalJson(content.facts)}`
18
+ : "text" in content
19
+ ? `text:${content.text}`
20
+ : "url" in content
21
+ ? `text:${content.url}`
22
+ : `bytes:${content.contentBase64}`;
23
+ return `sdk-ingest-${sha256Hex(pre).slice(0, 32)}`;
24
+ }
25
+ let rpcSeq = 0;
26
+ /**
27
+ * A thin, typed client for a running cilow-serve. One instance, five verbs:
28
+ *
29
+ * ```ts
30
+ * const cilow = new CilowClient({ baseUrl: "http://localhost:8080", token: "demo" });
31
+ * await cilow.remember([{ subject: "Maya", predicate: "employer", object: "Acme" }]);
32
+ * const r = await cilow.recall("where does Maya work", { anchor: "Maya", attribute: "employer" });
33
+ * if (r.abstained) console.log("engine doesn't know:", r.reason);
34
+ * else console.log(r.claims[0].value); // "Acme"
35
+ * ```
36
+ *
37
+ * Handles the JSON-RPC 2.0 envelope, bearer auth, and error mapping. The abstention
38
+ * contract is surfaced as a plain `abstained` boolean on `recall`/`answer` — never an
39
+ * exception. CilowError is thrown only for protocol / transport / auth failures.
40
+ */
41
+ export class CilowClient {
42
+ baseUrl;
43
+ token;
44
+ scope;
45
+ fetchImpl;
46
+ timeoutMs;
47
+ /** The friendly id-based memories surface: `client.memories.add(...)` etc. */
48
+ memories;
49
+ constructor(opts) {
50
+ if (!opts.baseUrl)
51
+ throw new CilowError("CilowClient requires a baseUrl");
52
+ if (!opts.token)
53
+ throw new CilowError("CilowClient requires a bearer token");
54
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
55
+ this.token = opts.token;
56
+ this.scope = opts.scope ?? {};
57
+ const f = opts.fetch ?? globalThis.fetch;
58
+ if (typeof f !== "function") {
59
+ throw new CilowError("no fetch available — pass `fetch` in CilowClientOptions (Node <18) or use Node 18+");
60
+ }
61
+ this.fetchImpl = f;
62
+ this.timeoutMs = opts.timeoutMs ?? 30_000;
63
+ // The memories sub-API — arrow methods so they close over `this` (and reach the private transport).
64
+ this.memories = {
65
+ add: (content, opts = {}) => this.callRest("add", {
66
+ content,
67
+ ...(opts.contentBase64 !== undefined ? { content_base64: opts.contentBase64 } : {}),
68
+ ...(opts.mime !== undefined ? { mime: opts.mime } : {}),
69
+ ...(opts.subjectHint !== undefined ? { subject_hint: opts.subjectHint } : {}),
70
+ ...(opts.customId !== undefined ? { custom_id: opts.customId } : {}),
71
+ ...(opts.observedAt !== undefined ? { observed_at: opts.observedAt } : {}),
72
+ ...(opts.metadata !== undefined ? { metadata: opts.metadata } : {}),
73
+ }),
74
+ search: (query, opts = {}) => this.callRest("search", {
75
+ query,
76
+ ...(opts.anchor !== undefined ? { anchor: opts.anchor } : {}),
77
+ ...(opts.attribute !== undefined ? { attribute: opts.attribute } : {}),
78
+ ...(opts.mode !== undefined ? { mode: opts.mode } : {}),
79
+ }),
80
+ get: (id) => this.callRest("get", { id }, { allow404: true }),
81
+ list: (opts = {}) => this.callRest("list", {
82
+ offset: opts.offset ?? 0,
83
+ active_only: opts.activeOnly ?? false,
84
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
85
+ }),
86
+ update: (id, value, opts = {}) => this.callRest("update", {
87
+ id,
88
+ value,
89
+ ...(opts.observedAt !== undefined ? { observed_at: opts.observedAt } : {}),
90
+ }),
91
+ delete: (id, opts = {}) => this.callRest("delete", {
92
+ id,
93
+ ...(opts.at !== undefined ? { at: opts.at } : {}),
94
+ }),
95
+ batchAdd: (items) => this.callRest("batch", {
96
+ items: items.map((it) => ({
97
+ content: it.content,
98
+ ...(it.subjectHint !== undefined ? { subject_hint: it.subjectHint } : {}),
99
+ ...(it.customId !== undefined ? { custom_id: it.customId } : {}),
100
+ ...(it.observedAt !== undefined ? { observed_at: it.observedAt } : {}),
101
+ ...(it.metadata !== undefined ? { metadata: it.metadata } : {}),
102
+ })),
103
+ }),
104
+ };
105
+ }
106
+ /** Liveness probe — `GET /health`, no auth. Returns true iff the server is up. */
107
+ async health() {
18
108
  try {
19
- apiError = JSON.parse(errorBody);
20
- } catch {
21
- apiError = {
22
- code: `HTTP_${response.status}`,
23
- message: errorBody || response.statusText
24
- };
109
+ const res = await this.fetchImpl(`${this.baseUrl}/health`);
110
+ if (!res.ok)
111
+ return false;
112
+ const body = (await res.json());
113
+ return body.status === "ok";
25
114
  }
26
- throw new CilowApiError(apiError.message, apiError.code, response.status, apiError.details);
27
- }
28
- return response;
29
- } catch (error) {
30
- lastError = error;
31
- if (error instanceof CilowApiError) {
32
- throw error;
33
- }
34
- if (config.debug) {
35
- console.warn(`Cilow API request failed (attempt ${attempt + 1}/${config.retries + 1}):`, error);
36
- }
37
- if (attempt < config.retries) {
38
- await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100));
39
- }
40
- }
41
- }
42
- clearTimeout(timeoutId);
43
- throw lastError || new Error("Request failed");
44
- }
45
- var CilowApiError = class extends Error {
46
- constructor(message, code, statusCode, details) {
47
- super(message);
48
- this.code = code;
49
- this.statusCode = statusCode;
50
- this.details = details;
51
- this.name = "CilowApiError";
52
- }
53
- };
54
- var CilowClient = class {
55
- constructor(config) {
56
- this.baseUrl = config.apiUrl.replace(/\/$/, "");
57
- this.apiKey = config.apiKey;
58
- this.timeout = config.timeout ?? 3e4;
59
- this.retries = config.retries ?? 3;
60
- this.debug = config.debug ?? false;
61
- const authHeaders = this.apiKey.startsWith("cilow_") ? { "X-API-Key": this.apiKey } : { Authorization: `Bearer ${this.apiKey}` };
62
- this.headers = {
63
- "Content-Type": "application/json",
64
- ...authHeaders,
65
- ...config.headers
66
- };
67
- }
68
- /**
69
- * Make an API request
70
- */
71
- async request(method, path, body, queryParams) {
72
- let url = `${this.baseUrl}${path}`;
73
- if (queryParams) {
74
- const params = new URLSearchParams();
75
- for (const [key, value] of Object.entries(queryParams)) {
76
- if (value !== void 0) {
77
- params.append(key, String(value));
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ /** Ingest `(subject, predicate, object)` facts as bitemporal claims. Returns the write receipt. */
120
+ async remember(facts, opts = {}) {
121
+ const now = opts.observedAt ?? nowMicros();
122
+ return this.call("remember", {
123
+ facts,
124
+ scope: this.mergeScope(opts.scope),
125
+ source: opts.source ?? "operator",
126
+ source_id: opts.sourceId ?? `sdk-${Date.now()}-${rpcSeq}`,
127
+ observed_at: now,
128
+ ...(opts.rawContext !== undefined ? { raw_context: opts.rawContext } : {}),
129
+ ...(opts.branch !== undefined ? { branch: opts.branch } : {}),
130
+ });
131
+ }
132
+ /**
133
+ * Retrieve claims as-of a valid-time T, OR a calibrated abstain. Check `abstained`
134
+ * first. When true, `claims` is empty; use the caller's fallback or safe-deferral policy.
135
+ */
136
+ async recall(query, opts = {}) {
137
+ const now = opts.now ?? nowMicros();
138
+ return this.call("recall", {
139
+ query,
140
+ scope: this.mergeScope(opts.scope),
141
+ as_of: opts.asOf ?? now,
142
+ now,
143
+ ...(opts.anchor !== undefined ? { anchor: opts.anchor } : {}),
144
+ ...(opts.attribute !== undefined ? { attribute: opts.attribute } : {}),
145
+ ...(opts.branch !== undefined ? { branch: opts.branch } : {}),
146
+ });
147
+ }
148
+ /**
149
+ * Present-time synthesized answer with citations, OR a calibrated abstain. When
150
+ * `abstained` is true, `text` is the honored abstain message and `citations` is empty.
151
+ */
152
+ async answer(query, opts = {}) {
153
+ return this.call("answer", {
154
+ query,
155
+ scope: this.mergeScope(opts.scope),
156
+ now: opts.now ?? nowMicros(),
157
+ ...(opts.anchor !== undefined ? { anchor: opts.anchor } : {}),
158
+ ...(opts.attribute !== undefined ? { attribute: opts.attribute } : {}),
159
+ ...(opts.branch !== undefined ? { branch: opts.branch } : {}),
160
+ });
161
+ }
162
+ /**
163
+ * Track A1 create a git-style BRANCH of this tenant's memory (O(1), copies nothing). Write/
164
+ * supersede facts on the branch (`remember(facts, { branch: name })`) and they stay ISOLATED from
165
+ * `main` and other branches; an anchored `recall(q, { branch: name })` inherits main's value as of
166
+ * the fork for keys the branch never wrote. Check `ok`: false ⇒ rejected (see `error`).
167
+ */
168
+ async fork(name, opts = {}) {
169
+ return this.call("fork", {
170
+ name,
171
+ scope: this.mergeScope(opts.scope),
172
+ now: opts.now ?? nowMicros(),
173
+ ...(opts.from !== undefined ? { from: opts.from } : {}),
174
+ });
175
+ }
176
+ /** Track A1 — list this tenant's git-style branches (`main` first, then forks in name order). */
177
+ async branchList(opts = {}) {
178
+ const res = await this.call("branch_list", {
179
+ scope: this.mergeScope(opts.scope),
180
+ });
181
+ return res.branches ?? [];
182
+ }
183
+ /** The as-of-T bitemporal history of an entity's claims, ascending by `valid_from`. */
184
+ async timeline(entity, opts = {}) {
185
+ const res = await this.call("timeline", {
186
+ entity,
187
+ scope: this.mergeScope(opts.scope),
188
+ at: opts.at ?? nowMicros(),
189
+ ...(opts.from !== undefined ? { t_from: opts.from } : {}),
190
+ ...(opts.to !== undefined ? { t_to: opts.to } : {}),
191
+ });
192
+ return res.entries;
193
+ }
194
+ /** Register / resolve an entity name to its canonical id + alias class as-of `at`. */
195
+ async trackEntity(name, opts = {}) {
196
+ return this.call("track_entity", {
197
+ name,
198
+ scope: this.mergeScope(opts.scope),
199
+ at: opts.at ?? nowMicros(),
200
+ });
201
+ }
202
+ /**
203
+ * Report the outcome of a prior `recall` (by its `traceId`) so memory LEARNS — the
204
+ * outcome-feedback loop. A `"positive"` outcome reinforces the cited claims; `"negative"`
205
+ * down-weights them; either way the calibrated abstain gate's q̂ self-tunes. This is
206
+ * in-context learning at the memory layer — no model retraining.
207
+ *
208
+ * Check `.applied`: when FALSE the feedback was rejected (`.error` is `"unknown_trace"` or
209
+ * `"cross_tenant"`) and nothing was mutated. A cross-tenant attempt does NOT consume the
210
+ * owner's trace.
211
+ */
212
+ async feedback(traceId, outcome, opts = {}) {
213
+ return this.call("feedback", {
214
+ trace_id: traceId,
215
+ outcome,
216
+ scope: this.mergeScope(opts.scope),
217
+ });
218
+ }
219
+ /**
220
+ * Run the idle-time episodic→semantic pass for this tenant: promote corroborated claims,
221
+ * collapse supersession chains into queryable transitions, and decay stale never-recalled
222
+ * claims. Retire-not-delete — as-of-T history is preserved. Returns the counts.
223
+ */
224
+ async consolidate(opts = {}) {
225
+ return this.call("consolidate", {
226
+ scope: this.mergeScope(opts.scope),
227
+ now: opts.now ?? nowMicros(),
228
+ });
229
+ }
230
+ /**
231
+ * "Just add text": ingest free PROSE. A language model extracts (subject, predicate, object) facts,
232
+ * written as calibrated bitemporal claims — no hand-structuring. `subjectHint` is the default subject
233
+ * for a fact the text does not name. Check `.ok`: false ⇒ the text couldn't be read (`.error` says why).
234
+ */
235
+ async rememberText(text, subjectHint, opts = {}) {
236
+ return this.call("remember_text", {
237
+ text,
238
+ subject_hint: subjectHint,
239
+ scope: this.mergeScope(opts.scope),
240
+ source_id: opts.sourceId ?? `sdk-${Date.now()}-${++rpcSeq}`,
241
+ observed_at: opts.observedAt ?? nowMicros(),
242
+ });
243
+ }
244
+ /**
245
+ * Make an image part of memory: a vision model reads it into a caption + facts, written as claims —
246
+ * so the image is recallable by an ordinary TEXT query. `imageBase64` is the raw base64 (no data: prefix).
247
+ * Check `.ok`: false ⇒ the image couldn't be read (`.error`).
248
+ */
249
+ async ingestImage(imageBase64, mime, subjectHint, opts = {}) {
250
+ return this.call("ingest_image", {
251
+ image_base64: imageBase64,
252
+ mime,
253
+ subject_hint: subjectHint,
254
+ scope: this.mergeScope(opts.scope),
255
+ source_id: opts.sourceId ?? `sdk-img-${Date.now()}-${++rpcSeq}`,
256
+ observed_at: opts.observedAt ?? nowMicros(),
257
+ });
258
+ }
259
+ /**
260
+ * Make an audio clip part of memory: it's transcribed, then the transcript is written as a claim AND
261
+ * run through the same text→facts extraction prose uses — so the audio is recallable by an ordinary
262
+ * TEXT query. `audioBase64` is the raw base64 (no data: prefix). Check `.ok`: false ⇒ couldn't read (`.error`).
263
+ */
264
+ async ingestAudio(audioBase64, mime, subjectHint, opts = {}) {
265
+ return this.call("ingest_audio", {
266
+ audio_base64: audioBase64,
267
+ mime,
268
+ subject_hint: subjectHint,
269
+ scope: this.mergeScope(opts.scope),
270
+ source_id: opts.sourceId ?? `sdk-aud-${Date.now()}-${++rpcSeq}`,
271
+ observed_at: opts.observedAt ?? nowMicros(),
272
+ });
273
+ }
274
+ /**
275
+ * Make a PDF part of memory via the hybrid path: the text layer is extracted AND each page is rendered
276
+ * to an image read by a VLM, both written as claims — so a document (text-based or scanned) is
277
+ * recallable by an ordinary TEXT query. `pdfBase64` is the raw base64 (no data: prefix). Check `.ok`.
278
+ */
279
+ async ingestPdf(pdfBase64, subjectHint, opts = {}) {
280
+ return this.call("ingest_pdf", {
281
+ pdf_base64: pdfBase64,
282
+ subject_hint: subjectHint,
283
+ scope: this.mergeScope(opts.scope),
284
+ source_id: opts.sourceId ?? `sdk-pdf-${Date.now()}-${++rpcSeq}`,
285
+ observed_at: opts.observedAt ?? nowMicros(),
286
+ });
287
+ }
288
+ /**
289
+ * Ingest clean CSV or JSON DIRECTLY into claims — no LLM, no hallucination, no token cost. Each row is
290
+ * an entity, each column/key a predicate, each value an object. `format` is "csv" or "json";
291
+ * `subjectField` names the column/key that is the entity (omit ⇒ subjectHint + row index).
292
+ */
293
+ async ingestStructured(data, format, subjectHint, opts = {}) {
294
+ return this.call("ingest_structured", {
295
+ data,
296
+ format,
297
+ ...(opts.subjectField !== undefined ? { subject_field: opts.subjectField } : {}),
298
+ subject_hint: subjectHint,
299
+ scope: this.mergeScope(opts.scope),
300
+ source_id: opts.sourceId ?? `sdk-str-${Date.now()}-${++rpcSeq}`,
301
+ observed_at: opts.observedAt ?? nowMicros(),
302
+ });
303
+ }
304
+ /**
305
+ * Primary universal save API: hand Cilow literally any data. Pass exactly one of
306
+ * `contentBase64` (any bytes) or `text` (prose / CSV / JSON / a bare URL) in `content`. The data is
307
+ * sniffed, the ORIGINAL retained durably (`raw_id`/`blob_id`), a document entity + metadata claims
308
+ * written (so even unknown bytes are findable), and the matching typed reader runs when one exists.
309
+ * Reader/provider failures are surfaced in-band as `status: "degraded"` + `reader_error`, not as a
310
+ * calibrated "memory does not know" read abstain.
311
+ */
312
+ async save(content, opts = {}) {
313
+ return this.call("save", {
314
+ ...("contentBase64" in content
315
+ ? { content_base64: content.contentBase64 }
316
+ : { text: content.text }),
317
+ ...(opts.mime !== undefined ? { mime: opts.mime } : {}),
318
+ subject_hint: opts.subjectHint ?? "",
319
+ ...(opts.metadata !== undefined ? { metadata: opts.metadata } : {}),
320
+ scope: this.mergeScope(opts.scope),
321
+ source_id: opts.sourceId ?? `sdk-save-${Date.now()}-${++rpcSeq}`,
322
+ observed_at: opts.observedAt ?? nowMicros(),
323
+ });
324
+ }
325
+ /**
326
+ * Legacy alias for `save`: the universal NEVER-REJECT ingest. Pass exactly one of
327
+ * `contentBase64` (any bytes) or `text` (prose / CSV / JSON / a bare URL) in `content`. The data is
328
+ * sniffed, the ORIGINAL retained durably (`blob_id`), a document entity + metadata claims written
329
+ * (so even unknown bytes are findable), and the matching typed reader runs when one exists (a URL
330
+ * is fetched + snapshotted; replay never re-fetches). `ok` is false only for malformed ARGUMENTS;
331
+ * unknown types and reader failures are `ok: true` with `routed` absent + `reader_error` set.
332
+ */
333
+ async ingestAny(content, opts = {}) {
334
+ return this.call("ingest_any", {
335
+ ...("contentBase64" in content
336
+ ? { content_base64: content.contentBase64 }
337
+ : { text: content.text }),
338
+ ...(opts.mime !== undefined ? { mime: opts.mime } : {}),
339
+ subject_hint: opts.subjectHint ?? "",
340
+ ...(opts.metadata !== undefined ? { metadata: opts.metadata } : {}),
341
+ scope: this.mergeScope(opts.scope),
342
+ source_id: opts.sourceId ?? `sdk-any-${Date.now()}-${++rpcSeq}`,
343
+ observed_at: opts.observedAt ?? nowMicros(),
344
+ });
345
+ }
346
+ /**
347
+ * Retire memory (retire-not-delete), or destroy it when `erase: true` (OF1).
348
+ *
349
+ * Retire: omit `attribute` to forget the whole entity, set it to forget one attribute, add
350
+ * `value` to forget one value. As-of-T history before `at` is preserved.
351
+ *
352
+ * Erase: whole-entity destruction; counters on the receipt. Not complete Art.17.
353
+ */
354
+ async forget(entity, opts = {}) {
355
+ const erase = opts.erase === true;
356
+ return this.call("forget", {
357
+ entity,
358
+ ...(erase
359
+ ? { erase: true }
360
+ : {
361
+ ...(opts.attribute !== undefined ? { attribute: opts.attribute } : {}),
362
+ ...(opts.value !== undefined ? { value: opts.value } : {}),
363
+ }),
364
+ scope: this.mergeScope(opts.scope),
365
+ at: opts.at ?? nowMicros(),
366
+ });
367
+ }
368
+ /** Destructive right-to-be-forgotten path — `forget(entity, { erase: true })`. */
369
+ async erase(entity, opts = {}) {
370
+ return this.forget(entity, { erase: true, scope: opts.scope, at: opts.at });
371
+ }
372
+ /**
373
+ * Curated injectible pack + citations + trunk/suffix — no free-form synthesis (D2).
374
+ * When session trunk is on and `session != 0`, `trunk` is the stable multi-turn prefix.
375
+ */
376
+ async contextPack(query, opts = {}) {
377
+ return this.call("context_pack", {
378
+ query,
379
+ scope: this.mergeScope(opts.scope),
380
+ now: opts.now ?? nowMicros(),
381
+ ...(opts.asOf !== undefined ? { as_of: opts.asOf } : {}),
382
+ ...(opts.anchor !== undefined ? { anchor: opts.anchor } : {}),
383
+ ...(opts.attribute !== undefined ? { attribute: opts.attribute } : {}),
384
+ });
385
+ }
386
+ // ── The four core verbs: ingest / contextPack / forget / explain ─────────
387
+ // `contextPack` and `forget` are above; `ingest` and `explain` complete the set.
388
+ /**
389
+ * The ONE write door (core verb #1): store `{ facts }` | `{ text }` | `{ url }` | `{ contentBase64 }`.
390
+ *
391
+ * - `facts` — `[{ subject, predicate, object, multi? }]`. Rides the `remember` path. A new value for
392
+ * an exclusive attribute SUPERSEDES the old one (history stays queryable via `contextPack({ asOf })`);
393
+ * `multi: true` on a fact lets values coexist. `source` / `rawContext` / `branch` apply here.
394
+ * - `text` (prose / CSV / JSON), `url` (a bare URL — sent as `text`; the server fetches + snapshots
395
+ * it), `contentBase64` (+ `mime` hint) — ride the never-reject `ingest_any` path: the original is
396
+ * retained durably (`content.blob_id`), a document entity + `metadata` claims are written, and the
397
+ * matching typed reader runs when one exists. `subjectHint` / `metadata` apply here.
398
+ *
399
+ * `sourceId` defaults to a CONTENT HASH (`ingestSourceId`) so a retry of the same payload is an
400
+ * idempotent no-op; the key actually sent is echoed on `source_id`. Never throws for content —
401
+ * only for a transport / auth failure.
402
+ */
403
+ async ingest(content, opts = {}) {
404
+ const sourceId = opts.sourceId ?? ingestSourceId(content);
405
+ const common = {
406
+ scope: this.mergeScope(opts.scope),
407
+ source_id: sourceId,
408
+ observed_at: opts.observedAt ?? nowMicros(),
409
+ };
410
+ if ("facts" in content) {
411
+ const wire = await this.call("ingest", {
412
+ facts: content.facts,
413
+ source: opts.source ?? "operator",
414
+ ...(opts.rawContext !== undefined ? { raw_context: opts.rawContext } : {}),
415
+ ...(opts.branch !== undefined ? { branch: opts.branch } : {}),
416
+ ...common,
417
+ });
418
+ return {
419
+ kind: "facts",
420
+ source_id: sourceId,
421
+ ok: wire.ok ?? true,
422
+ facts_ingested: wire.claims_emitted ?? 0,
423
+ deduped: wire.deduped ?? false,
424
+ remember: wire,
425
+ ...(wire.error !== undefined ? { error: wire.error } : {}),
426
+ };
427
+ }
428
+ const payload = "contentBase64" in content
429
+ ? { content_base64: content.contentBase64, ...(content.mime !== undefined ? { mime: content.mime } : {}) }
430
+ : { text: "url" in content ? content.url : content.text };
431
+ const wire = await this.call("ingest", {
432
+ ...payload,
433
+ subject_hint: opts.subjectHint ?? "",
434
+ ...(opts.metadata !== undefined ? { metadata: opts.metadata } : {}),
435
+ ...common,
436
+ });
437
+ return {
438
+ kind: "content",
439
+ source_id: sourceId,
440
+ ok: wire.ok,
441
+ facts_ingested: wire.facts_ingested ?? 0,
442
+ deduped: wire.deduped ?? false,
443
+ remember: wire.remember,
444
+ content: wire,
445
+ ...(wire.trace_id !== undefined ? { trace_id: wire.trace_id } : {}),
446
+ ...(wire.error !== undefined ? { error: wire.error } : {}),
447
+ };
448
+ }
449
+ /**
450
+ * Why did a pack / recall surface what it surfaced? (core verb #4) Pass the `trace_id` from a
451
+ * `contextPack` / `recall` response. The report carries the cited claims (labels resolved, each with
452
+ * its inclusion probability), the read's `best_nonconformity` vs the tenant's grounding threshold
453
+ * `q_hat`, and whether the read was grounded (`isGrounded(r)`) or a gap. Read-only: explaining never
454
+ * consumes the feedback trace. `found: false` + `error` (`unknown_trace` / `cross_tenant`) is in-band.
455
+ */
456
+ async explain(traceId, opts = {}) {
457
+ return this.call("explain", {
458
+ trace_id: traceId,
459
+ scope: this.mergeScope(opts.scope),
460
+ });
461
+ }
462
+ // ── Retained blobs (the lossless raw lane) ───────────────────────────────
463
+ /**
464
+ * Read a retained original back by its hex `blob_id` (from an ingest receipt or `blobList`).
465
+ * Full sha256-verified read by default; pass `offset`/`length` for a chunk-indexed RANGE read
466
+ * of a large blob (EOF-clamped; `total_len` in the response lets you paginate; `byte_len` is
467
+ * the returned window). `found:false` with no `error` is an ordinary miss; `error` in
468
+ * {invalid_id, invalid_range, storage, corrupt} means the range read FAILED — never treat it
469
+ * as absence. Bytes ride back in `data_base64`.
470
+ */
471
+ async blobGet(blobId, opts = {}) {
472
+ return this.call("blob_get", {
473
+ blob_id: blobId,
474
+ ...(opts.offset !== undefined ? { offset: opts.offset } : {}),
475
+ ...(opts.length !== undefined ? { length: opts.length } : {}),
476
+ scope: this.mergeScope(opts.scope),
477
+ });
478
+ }
479
+ /**
480
+ * One retained blob's METADATA (mime, byte length, provenance, metadata pairs, chunk count) by
481
+ * hex `blob_id` — no bytes. `found:false` ⇒ this tenant retains no such blob.
482
+ */
483
+ async blobMeta(blobId, scope) {
484
+ return this.call("blob_meta", { blob_id: blobId, scope: this.mergeScope(scope) });
485
+ }
486
+ /** Enumerate this tenant's retained original blobs with metadata (no bytes), deterministically ordered. */
487
+ async blobList(scope) {
488
+ return this.call("blob_list", { scope: this.mergeScope(scope) });
489
+ }
490
+ // ── Verified Context Runtime ───────────────────────────────────────────
491
+ async createContextWorkspace(name, scope) {
492
+ return this.call("context_workspace_create", { name, scope: this.mergeScope(scope), now: nowMicros() });
493
+ }
494
+ async ingestEvidence(input) {
495
+ return this.call("evidence_ingest", { workspace_id: input.workspaceId, source_id: input.sourceId, source_revision: input.sourceRevision, text: input.text, mime: input.mime ?? "text/plain", metadata: input.metadata ?? {}, coordinates: input.coordinates ?? [], trust: input.trust ?? "user_provided", acl_principals: input.aclPrincipals ?? [], scope: this.mergeScope(input.scope), observed_at: nowMicros() });
496
+ }
497
+ async investigate(workspaceId, query, opts = {}) {
498
+ return this.call("context_investigate", { workspace_id: workspaceId, query, token_budget: opts.tokenBudget ?? 2000, scope: this.mergeScope(opts.scope), now: nowMicros() });
499
+ }
500
+ async compileContext(workspaceId, query, opts) {
501
+ return this.call("context_compile", {
502
+ workspace_id: workspaceId,
503
+ query,
504
+ model_id: opts.modelId,
505
+ tokenizer_id: opts.tokenizerId,
506
+ model_context_token_budget: opts.modelContextTokenBudget ?? 1024,
507
+ audit_payload_byte_budget: opts.auditPayloadByteBudget ?? 256 * 1024,
508
+ prompt_template_id: opts.promptTemplateId ?? "cilow/context/plain/v1",
509
+ as_of: opts.asOf ?? nowMicros(),
510
+ scope: this.mergeScope(opts.scope),
511
+ });
512
+ }
513
+ async getCompiledContext(receiptId, scope) {
514
+ return this.call("context_program_get", {
515
+ receipt_id: receiptId,
516
+ scope: this.mergeScope(scope),
517
+ });
518
+ }
519
+ async getContextTrace(traceId, scope) {
520
+ return this.call("context_trace_get", { trace_id: traceId, scope: this.mergeScope(scope) });
521
+ }
522
+ async recordAgentEpisode(workspaceId, summary, evidenceIds, scope) {
523
+ return this.call("agent_episode_record", { workspace_id: workspaceId, summary, evidence_ids: evidenceIds, scope: this.mergeScope(scope), occurred_at: nowMicros() });
524
+ }
525
+ async proposeMemory(input) {
526
+ return this.call("memory_propose", { workspace_id: input.workspaceId, episode_id: input.episodeId, subject: input.subject, predicate: input.predicate, value: input.value, evidence_ids: input.evidenceIds, scope: this.mergeScope(input.scope), now: nowMicros() });
527
+ }
528
+ async verifyMemory(proposalId, accept, reason, scope) {
529
+ return this.call("memory_verify", { proposal_id: proposalId, accept, reason, scope: this.mergeScope(scope), now: nowMicros() });
530
+ }
531
+ // ── internals ───────────────────────────────────────────────────────────
532
+ mergeScope(override) {
533
+ return { ...this.scope, ...(override ?? {}) };
534
+ }
535
+ /**
536
+ * Issue one JSON-RPC `tools/call` and return the engine's `structuredContent`.
537
+ * Maps every failure mode (transport, HTTP, JSON-RPC error, malformed body) to a CilowError.
538
+ */
539
+ async call(name, args) {
540
+ const id = ++rpcSeq;
541
+ const envelope = {
542
+ jsonrpc: "2.0",
543
+ id,
544
+ method: "tools/call",
545
+ params: { name, arguments: args },
546
+ };
547
+ const controller = new AbortController();
548
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
549
+ let res;
550
+ try {
551
+ res = await this.fetchImpl(`${this.baseUrl}/rpc`, {
552
+ method: "POST",
553
+ headers: {
554
+ "Content-Type": "application/json",
555
+ Authorization: `Bearer ${this.token}`,
556
+ },
557
+ body: JSON.stringify(envelope),
558
+ signal: controller.signal,
559
+ });
560
+ }
561
+ catch (cause) {
562
+ throw new CilowError(`request to ${this.baseUrl}/rpc failed: ${describe(cause)}`, { cause });
563
+ }
564
+ finally {
565
+ clearTimeout(timer);
566
+ }
567
+ let body;
568
+ const text = await res.text();
569
+ try {
570
+ body = text ? JSON.parse(text) : undefined;
78
571
  }
79
- }
80
- const queryString = params.toString();
81
- if (queryString) {
82
- url += `?${queryString}`;
83
- }
84
- }
85
- if (this.debug) {
86
- console.log(`Cilow API: ${method} ${url}`);
87
- }
88
- const response = await fetchWithRetry(
89
- url,
90
- {
91
- method,
92
- headers: this.headers,
93
- body: body ? JSON.stringify(body) : void 0
94
- },
95
- { timeout: this.timeout, retries: this.retries, debug: this.debug }
96
- );
97
- const text = await response.text();
98
- if (!text) {
99
- return void 0;
100
- }
101
- return JSON.parse(text);
102
- }
103
- // ===========================================================================
104
- // Simple API (remember, recall, forget)
105
- // ===========================================================================
106
- /**
107
- * Store a memory (simple API)
108
- *
109
- * @param content - The content to remember
110
- * @param options - Optional configuration
111
- * @returns The created memory ID
112
- *
113
- * @example
114
- * ```typescript
115
- * await cilow.remember("User prefers dark mode", {
116
- * tags: ["preference"],
117
- * userId: "user-123"
118
- * });
119
- * ```
120
- */
121
- async remember(content, options) {
122
- const response = await this.request("POST", "/api/v1/memory/add", {
123
- content,
124
- tags: options?.tags ?? [],
125
- user_id: options?.userId,
126
- session_id: options?.sessionId,
127
- metadata: options?.metadata,
128
- type: options?.tier
129
- });
130
- return response.memory_id || response.id || "";
131
- }
132
- /**
133
- * Search memories (simple API)
134
- *
135
- * @param query - Search query text
136
- * @param options - Search options
137
- * @returns Array of search results
138
- *
139
- * @example
140
- * ```typescript
141
- * const memories = await cilow.recall("user preferences", {
142
- * limit: 10,
143
- * tags: ["preference"]
144
- * });
145
- * ```
146
- */
147
- async recall(query, options) {
148
- const response = await this.request("POST", "/api/v1/memory/search", {
149
- query,
150
- limit: options?.limit ?? 10,
151
- min_score: options?.minRelevance ?? 0.3,
152
- tags: options?.tags,
153
- user_id: options?.userId
154
- });
155
- return (response.results ?? []).map((r) => ({
156
- memory: {
157
- id: r.memory_id,
158
- content: r.content,
159
- tags: r.tags ?? [],
160
- tier: "hot",
161
- status: "active",
162
- accessCount: 0,
163
- tokenCount: Math.ceil(r.content.length / 4),
164
- metadata: {},
165
- createdAt: r.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
166
- updatedAt: r.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
167
- lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
168
- },
169
- score: r.similarity,
170
- highlights: []
171
- }));
172
- }
173
- /**
174
- * Delete memories (simple API)
175
- *
176
- * @param filter - Filter criteria for deletion
177
- * @returns Number of memories deleted
178
- *
179
- * @example
180
- * ```typescript
181
- * // Delete by tags
182
- * await cilow.forget({ tags: ["temporary"] });
183
- *
184
- * // Delete by user
185
- * await cilow.forget({ userId: "user-123" });
186
- *
187
- * // Delete specific memory
188
- * await cilow.forget({ memoryId: "mem-abc" });
189
- * ```
190
- */
191
- async forget(filter) {
192
- if (filter.memoryId) {
193
- await this.deleteMemory(filter.memoryId);
194
- return 1;
195
- }
196
- const response = await this.request("DELETE", "/api/v1/memories/bulk", {
197
- filter_tags: filter.tags,
198
- user_id: filter.userId,
199
- session_id: filter.sessionId,
200
- older_than: filter.olderThan
201
- });
202
- return response.deleted ?? 0;
203
- }
204
- // ===========================================================================
205
- // Memory CRUD Operations
206
- // ===========================================================================
207
- /**
208
- * Create a new memory
209
- */
210
- async createMemory(content, options) {
211
- return this.request("POST", "/api/v1/memories", {
212
- content,
213
- tags: options?.tags ?? [],
214
- user_id: options?.userId,
215
- session_id: options?.sessionId,
216
- metadata: options?.metadata,
217
- tier: options?.tier
218
- });
219
- }
220
- /**
221
- * Get a memory by ID
222
- */
223
- async getMemory(memoryId) {
224
- return this.request("GET", `/api/v1/memories/${memoryId}`);
225
- }
226
- /**
227
- * Update a memory
228
- */
229
- async updateMemory(memoryId, updates) {
230
- return this.request("PATCH", `/api/v1/memories/${memoryId}`, {
231
- content: updates.content,
232
- tags: updates.tags,
233
- metadata: updates.metadata,
234
- tier: updates.tier
235
- });
236
- }
237
- /**
238
- * Delete a memory
239
- */
240
- async deleteMemory(memoryId) {
241
- await this.request("DELETE", `/api/v1/memories/${memoryId}`);
242
- }
243
- /**
244
- * List memories with pagination
245
- */
246
- async listMemories(options) {
247
- const response = await this.request(
248
- "GET",
249
- "/api/v1/memory/list",
250
- void 0,
251
- {
252
- limit: options?.limit ?? 20,
253
- offset: options?.offset ?? 0,
254
- user_id: options?.userId,
255
- session_id: options?.sessionId,
256
- tags: options?.tags?.join(","),
257
- type: options?.tier
258
- }
259
- );
260
- const items = (response.memories ?? []).map((m) => ({
261
- id: m.memory_id || m.id || "",
262
- content: m.content,
263
- tags: m.tags ?? [],
264
- createdAt: m.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
265
- tier: m.type || "hot"
266
- }));
267
- const limit = options?.limit ?? 20;
268
- const offset = options?.offset ?? 0;
269
- const total = response.total ?? items.length;
270
- return {
271
- items,
272
- total,
273
- limit,
274
- offset,
275
- hasMore: offset + items.length < total
276
- };
277
- }
278
- // ===========================================================================
279
- // Search Operations
280
- // ===========================================================================
281
- /**
282
- * Search memories with full options
283
- */
284
- async searchMemories(query) {
285
- const response = await this.request("POST", "/api/v1/memories/search", {
286
- text: query.text,
287
- limit: query.limit ?? 10,
288
- min_relevance: query.minRelevance ?? 0.3,
289
- filter_tags: query.tags,
290
- user_id: query.userId,
291
- session_id: query.sessionId,
292
- tier: query.tier,
293
- created_after: query.createdAfter,
294
- created_before: query.createdBefore,
295
- include_archived: query.includeArchived,
296
- mode: query.mode ?? "hybrid"
297
- });
298
- return response.results ?? [];
299
- }
300
- /**
301
- * Advanced search with reranking and boosts
302
- */
303
- async advancedSearch(query) {
304
- const response = await this.request("POST", "/api/v1/memories/search/advanced", {
305
- text: query.text,
306
- limit: query.limit ?? 10,
307
- min_relevance: query.minRelevance ?? 0.3,
308
- filter_tags: query.tags,
309
- required_tags: query.requiredTags,
310
- excluded_tags: query.excludedTags,
311
- user_id: query.userId,
312
- session_id: query.sessionId,
313
- recency_boost: query.recencyBoost,
314
- frequency_boost: query.frequencyBoost,
315
- metadata_filters: query.metadataFilters,
316
- reranker: query.reranker
317
- });
318
- return response.results ?? [];
319
- }
320
- /**
321
- * Get context for AI applications
322
- */
323
- async getContext(query, options) {
324
- const maxTokens = options?.maxTokens ?? 4e3;
325
- const tokensPerChar = 0.25;
326
- const results = await this.searchMemories({
327
- text: query,
328
- limit: 30,
329
- minRelevance: 0.3,
330
- userId: options?.userId,
331
- tags: options?.tags
332
- });
333
- let context = "";
334
- let estimatedTokens = 0;
335
- const memoryIds = [];
336
- const scores = [];
337
- for (const result of results) {
338
- const memoryText = `[Relevance: ${(result.score * 100).toFixed(0)}%]
339
- ${result.memory.content}
340
-
341
- `;
342
- const memoryTokens = Math.ceil(memoryText.length * tokensPerChar);
343
- if (estimatedTokens + memoryTokens > maxTokens) break;
344
- context += memoryText;
345
- estimatedTokens += memoryTokens;
346
- memoryIds.push(result.memory.id);
347
- scores.push(result.score);
348
- }
349
- return {
350
- context: context.trim(),
351
- memoriesUsed: memoryIds.length,
352
- estimatedTokens,
353
- memoryIds,
354
- scores
355
- };
356
- }
357
- // ===========================================================================
358
- // Conversation Operations
359
- // ===========================================================================
360
- /**
361
- * Store a conversation turn
362
- */
363
- async storeConversation(turn) {
364
- const content = `User: ${turn.userMessage}
365
-
366
- Assistant: ${turn.assistantResponse}`;
367
- const tags = ["conversation"];
368
- if (turn.sessionId) {
369
- tags.push(`session:${turn.sessionId}`);
370
- }
371
- return this.remember(content, {
372
- tags,
373
- userId: turn.userId,
374
- sessionId: turn.sessionId,
375
- metadata: {
376
- type: "conversation",
377
- ...turn.metadata
378
- }
379
- });
380
- }
381
- /**
382
- * Get conversation history for a session
383
- */
384
- async getConversationHistory(sessionId, options) {
385
- const response = await this.listMemories({
386
- sessionId,
387
- tags: ["conversation"],
388
- limit: options?.limit ?? 50,
389
- offset: options?.offset ?? 0
390
- });
391
- return response.items;
392
- }
393
- // ===========================================================================
394
- // Graph Operations
395
- // ===========================================================================
396
- /**
397
- * Get a graph node
398
- */
399
- async getGraphNode(nodeId) {
400
- return this.request("GET", `/api/v1/graph/nodes/${nodeId}`);
401
- }
402
- /**
403
- * Create a graph node
404
- */
405
- async createGraphNode(type, name, properties) {
406
- return this.request("POST", "/api/v1/graph/nodes", {
407
- type,
408
- name,
409
- properties: properties ?? {}
410
- });
411
- }
412
- /**
413
- * Create a graph edge
414
- */
415
- async createGraphEdge(sourceId, targetId, type, properties) {
416
- return this.request("POST", "/api/v1/graph/edges", {
417
- source_id: sourceId,
418
- target_id: targetId,
419
- type,
420
- properties: properties ?? {}
421
- });
422
- }
423
- /**
424
- * Traverse the graph from a starting node
425
- */
426
- async traverseGraph(options) {
427
- return this.request("POST", "/api/v1/graph/traverse", {
428
- start_node_id: options.startNodeId,
429
- max_depth: options.maxDepth ?? 3,
430
- relationship_types: options.relationshipTypes,
431
- limit: options.limit ?? 100,
432
- direction: options.direction ?? "both"
433
- });
434
- }
435
- /**
436
- * Get related nodes for a memory
437
- */
438
- async getRelatedNodes(memoryId) {
439
- const response = await this.request(
440
- "GET",
441
- `/api/v1/memories/${memoryId}/related-nodes`
442
- );
443
- return response.nodes ?? [];
444
- }
445
- // ===========================================================================
446
- // Statistics & Admin
447
- // ===========================================================================
448
- /**
449
- * Get memory statistics
450
- */
451
- async getStats() {
452
- return this.request("GET", "/api/v1/stats");
453
- }
454
- /**
455
- * Get all tags with counts
456
- */
457
- async getTags() {
458
- const response = await this.request("GET", "/api/v1/tags");
459
- return response.tags ?? [];
460
- }
461
- /**
462
- * Get user statistics
463
- */
464
- async getUserStats(userId) {
465
- return this.request("GET", `/api/v1/users/${userId}/stats`);
466
- }
467
- /**
468
- * Health check
469
- */
470
- async healthCheck() {
471
- return this.request("GET", "/health");
472
- }
473
- // ===========================================================================
474
- // Batch Operations
475
- // ===========================================================================
476
- /**
477
- * Batch create memories
478
- */
479
- async batchCreate(items) {
480
- const response = await this.request("POST", "/api/v1/memories/batch", {
481
- memories: items.map((item) => ({
482
- content: item.content,
483
- tags: item.options?.tags ?? [],
484
- user_id: item.options?.userId,
485
- session_id: item.options?.sessionId,
486
- metadata: item.options?.metadata
487
- }))
488
- });
489
- return response.ids ?? [];
490
- }
491
- /**
492
- * Batch delete memories
493
- */
494
- async batchDelete(memoryIds) {
495
- const response = await this.request("DELETE", "/api/v1/memories/batch", {
496
- ids: memoryIds
497
- });
498
- return response.deleted ?? 0;
499
- }
500
- };
501
- function createClient(config) {
502
- return new CilowClient(config);
572
+ catch (cause) {
573
+ throw new CilowError(`malformed JSON response (HTTP ${res.status}): ${text.slice(0, 200)}`, {
574
+ httpStatus: res.status,
575
+ cause,
576
+ });
577
+ }
578
+ const frame = body;
579
+ // A JSON-RPC error envelope (auth, parse, method-not-found, bad-args) — even on a 200.
580
+ if (frame?.error) {
581
+ throw new CilowError(frame.error.message ?? "Cilow JSON-RPC error", {
582
+ code: frame.error.code,
583
+ httpStatus: res.status,
584
+ data: frame.error.data,
585
+ });
586
+ }
587
+ if (!res.ok) {
588
+ throw new CilowError(`Cilow HTTP ${res.status}: ${text.slice(0, 200)}`, {
589
+ httpStatus: res.status,
590
+ });
591
+ }
592
+ // The MCP `tools/call` result carries the typed payload under `result.structuredContent`.
593
+ const result = frame?.result;
594
+ if (!result || result.structuredContent === undefined) {
595
+ throw new CilowError(`Cilow response missing result.structuredContent: ${text.slice(0, 200)}`, { httpStatus: res.status });
596
+ }
597
+ return result.structuredContent;
598
+ }
599
+ /**
600
+ * POST to `/v1/memories/<verb>` and return the parsed JSON body (plain REST, not JSON-RPC). With
601
+ * `allow404`, a 404 resolves to `null` (used by `memories.get`). Other 4xx/5xx throw a CilowError
602
+ * carrying the body's `error` string.
603
+ */
604
+ async callRest(verb, payload, opts = {}) {
605
+ const url = `${this.baseUrl}/v1/memories/${verb}`;
606
+ const controller = new AbortController();
607
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
608
+ let res;
609
+ try {
610
+ res = await this.fetchImpl(url, {
611
+ method: "POST",
612
+ headers: {
613
+ "Content-Type": "application/json",
614
+ Authorization: `Bearer ${this.token}`,
615
+ },
616
+ body: JSON.stringify(payload),
617
+ signal: controller.signal,
618
+ });
619
+ }
620
+ catch (cause) {
621
+ throw new CilowError(`request to ${url} failed: ${describe(cause)}`, { cause });
622
+ }
623
+ finally {
624
+ clearTimeout(timer);
625
+ }
626
+ if (opts.allow404 && res.status === 404)
627
+ return null;
628
+ const text = await res.text();
629
+ let body;
630
+ try {
631
+ body = text ? JSON.parse(text) : undefined;
632
+ }
633
+ catch (cause) {
634
+ throw new CilowError(`malformed JSON response (HTTP ${res.status}): ${text.slice(0, 200)}`, {
635
+ httpStatus: res.status,
636
+ cause,
637
+ });
638
+ }
639
+ if (!res.ok) {
640
+ const err = body?.error;
641
+ throw new CilowError(err ?? `Cilow HTTP ${res.status}: ${text.slice(0, 200)}`, {
642
+ httpStatus: res.status,
643
+ });
644
+ }
645
+ return body;
646
+ }
647
+ }
648
+ function describe(e) {
649
+ if (e instanceof Error)
650
+ return e.message;
651
+ return String(e);
503
652
  }
504
-
505
- exports.CilowApiError = CilowApiError;
506
- exports.CilowClient = CilowClient;
507
- exports.createClient = createClient;
508
- //# sourceMappingURL=client.js.map
509
653
  //# sourceMappingURL=client.js.map