@babav/knowledge-core-client 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Babav Knowledge Core — TypeScript client.
3
+ *
4
+ * Portable: uses the global `fetch` + `ReadableStream`, so it runs unchanged in
5
+ * Supabase Edge Functions (Deno) and Railway (Node 18+). Zero dependencies.
6
+ *
7
+ * Wire shapes are snake_case to mirror the Knowledge Core API 1:1 — so an API
8
+ * change is reflected here with a one-to-one type edit and never drifts.
9
+ *
10
+ * Auth: every call carries an X-API-Key. Use TenantClient with a TENANT key for
11
+ * all data ops; use AdminClient with the ADMIN key for tenant / API-key / agent
12
+ * management. The key is server-side only — never ship it to a browser.
13
+ */
14
+ // ---------------------------------------------------------------------------
15
+ // Errors
16
+ // ---------------------------------------------------------------------------
17
+ /** Thrown on any non-2xx response. `detail` is the structured body when present
18
+ * (e.g. {error:"attachments_pending", attachments:[...]} or
19
+ * {error:"attachment_exceeds_context_window", ...}). */
20
+ export class KnowledgeCoreError extends Error {
21
+ status;
22
+ detail;
23
+ path;
24
+ constructor(status, detail, path) {
25
+ super(`KnowledgeCore ${status} on ${path}: ${JSON.stringify(detail)}`);
26
+ this.status = status;
27
+ this.detail = detail;
28
+ this.path = path;
29
+ this.name = "KnowledgeCoreError";
30
+ }
31
+ /** Convenience: the `error` code for structured guard responses. */
32
+ get code() {
33
+ const d = this.detail;
34
+ return d?.detail?.error ?? d?.error;
35
+ }
36
+ }
37
+ class HttpBase {
38
+ baseUrl;
39
+ apiKey;
40
+ timeoutMs;
41
+ _fetch;
42
+ constructor(opts) {
43
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
44
+ this.apiKey = opts.apiKey;
45
+ this.timeoutMs = opts.timeoutMs;
46
+ this._fetch = opts.fetch ?? fetch;
47
+ }
48
+ url(path, query) {
49
+ const u = new URL(this.baseUrl + path);
50
+ if (query) {
51
+ for (const [k, v] of Object.entries(query)) {
52
+ if (v !== undefined)
53
+ u.searchParams.set(k, String(v));
54
+ }
55
+ }
56
+ return u.toString();
57
+ }
58
+ async raw(method, path, opts = {}) {
59
+ const headers = { "x-api-key": this.apiKey, ...(opts.headers ?? {}) };
60
+ let body;
61
+ if (opts.json !== undefined) {
62
+ headers["content-type"] = "application/json";
63
+ body = JSON.stringify(opts.json);
64
+ }
65
+ else if (opts.body !== undefined) {
66
+ body = opts.body;
67
+ }
68
+ let signal = opts.signal;
69
+ let timer;
70
+ if (!signal && this.timeoutMs) {
71
+ const ac = new AbortController();
72
+ timer = setTimeout(() => ac.abort(), this.timeoutMs);
73
+ signal = ac.signal;
74
+ }
75
+ try {
76
+ return await this._fetch(this.url(path, opts.query), { method, headers, body, signal });
77
+ }
78
+ finally {
79
+ if (timer)
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+ async request(method, path, opts = {}) {
84
+ const res = await this.raw(method, path, opts);
85
+ const text = await res.text();
86
+ const parsed = text ? safeJson(text) : null;
87
+ if (!res.ok)
88
+ throw new KnowledgeCoreError(res.status, parsed ?? text, path);
89
+ return parsed;
90
+ }
91
+ /** Auto-paginate a list endpoint into a single array. */
92
+ async pageAll(path, query = {}) {
93
+ const out = [];
94
+ let cursor = undefined;
95
+ do {
96
+ const page = await this.request("GET", path, {
97
+ query: { ...query, cursor: cursor ?? undefined },
98
+ });
99
+ out.push(...page.items);
100
+ cursor = page.next_cursor;
101
+ } while (cursor);
102
+ return out;
103
+ }
104
+ }
105
+ function safeJson(t) {
106
+ try {
107
+ return JSON.parse(t);
108
+ }
109
+ catch {
110
+ return t;
111
+ }
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Tenant client (data ops) — use a TENANT key
115
+ // ---------------------------------------------------------------------------
116
+ export class KnowledgeCoreClient extends HttpBase {
117
+ // --- query (agent-anchored) ---
118
+ query(agentId, body) {
119
+ return this.request("POST", `/v1/agents/${agentId}/query`, { json: body });
120
+ }
121
+ /** Streaming query (SSE). Resolves when the stream ends. */
122
+ async queryStream(agentId, body, handlers, signal) {
123
+ const res = await this.raw("POST", `/v1/agents/${agentId}/query/stream`, { json: body, signal });
124
+ if (!res.ok || !res.body) {
125
+ const t = await res.text();
126
+ throw new KnowledgeCoreError(res.status, safeJson(t), `/v1/agents/${agentId}/query/stream`);
127
+ }
128
+ const reader = res.body.getReader();
129
+ const decoder = new TextDecoder();
130
+ let buf = "";
131
+ for (;;) {
132
+ const { value, done } = await reader.read();
133
+ if (done)
134
+ break;
135
+ buf += decoder.decode(value, { stream: true });
136
+ let idx;
137
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
138
+ const frame = buf.slice(0, idx);
139
+ buf = buf.slice(idx + 2);
140
+ dispatchSse(frame, handlers);
141
+ }
142
+ }
143
+ if (buf.trim())
144
+ dispatchSse(buf, handlers);
145
+ }
146
+ // --- retrieve (cross-corpus primitive, no generation) ---
147
+ retrieve(body) {
148
+ return this.request("POST", "/v1/retrieve", { json: body });
149
+ }
150
+ // --- corpora ---
151
+ corpora = {
152
+ create: (b) => this.request("POST", "/v1/corpora", { json: b }),
153
+ list: (q) => this.request("GET", "/v1/corpora", { query: q }),
154
+ listAll: () => this.pageAll("/v1/corpora"),
155
+ get: (id) => this.request("GET", `/v1/corpora/${id}`),
156
+ update: (id, b) => this.request("PATCH", `/v1/corpora/${id}`, { json: b }),
157
+ delete: (id, confirmName) => this.request("DELETE", `/v1/corpora/${id}`, { query: { confirm: confirmName } }),
158
+ listFolders: (id) => this.pageAll(`/v1/corpora/${id}/folders`),
159
+ listDocuments: (id, q) => this.request("GET", `/v1/corpora/${id}/documents`, { query: q }),
160
+ createDocument: (id, b) => this.request("POST", `/v1/corpora/${id}/documents`, { json: b }),
161
+ ingest: (id, b) => this.request("POST", `/v1/corpora/${id}/ingest`, { json: b }),
162
+ };
163
+ // --- folders ---
164
+ folders = {
165
+ create: (corpusId, name) => this.request("POST", `/v1/corpora/${corpusId}/folders`, { json: { name } }),
166
+ rename: (folderId, name) => this.request("PATCH", `/v1/folders/${folderId}`, { json: { name } }),
167
+ delete: (folderId) => this.request("DELETE", `/v1/folders/${folderId}`),
168
+ listDocuments: (folderId, q) => this.request("GET", `/v1/folders/${folderId}/documents`, { query: q }),
169
+ };
170
+ // --- documents ---
171
+ documents = {
172
+ get: (id) => this.request("GET", `/v1/documents/${id}`),
173
+ getMetadata: (id) => this.request("GET", `/v1/documents/${id}/metadata`),
174
+ patchMetadata: (id, custom_metadata) => this.request("PATCH", `/v1/documents/${id}/metadata`, { json: { custom_metadata } }),
175
+ update: (id, b) => this.request("PATCH", `/v1/documents/${id}`, { json: b }),
176
+ contentUrl: (id, disposition = "inline") => this.request("GET", `/v1/documents/${id}/content-url`, { query: { disposition } }),
177
+ delete: (id) => this.request("DELETE", `/v1/documents/${id}`),
178
+ };
179
+ // --- conversations ---
180
+ conversations = {
181
+ create: (b) => this.request("POST", "/v1/conversations", { json: b ?? {} }),
182
+ list: (q) => this.request("GET", "/v1/conversations", { query: q }),
183
+ get: (id) => this.request("GET", `/v1/conversations/${id}`),
184
+ update: (id, b) => this.request("PATCH", `/v1/conversations/${id}`, { json: b }),
185
+ delete: (id) => this.request("DELETE", `/v1/conversations/${id}`),
186
+ /** Filter by custom_metadata using the same metadata filter as documents. */
187
+ search: (b) => this.request("POST", "/v1/conversations/search", { json: b }),
188
+ listMessages: (id, q) => this.request("GET", `/v1/conversations/${id}/messages`, { query: q }),
189
+ };
190
+ // --- conversation attachments (pinned full-document review) ---
191
+ attachments = {
192
+ /** Upload + attach a document. `data` is the raw file bytes. Returns status=parsing. */
193
+ upload: (conversationId, a) => this.request("POST", `/v1/conversations/${conversationId}/documents`, {
194
+ body: a.data,
195
+ headers: { "content-type": a.content_type },
196
+ query: { filename: a.filename, content_type: a.content_type, custom_metadata: a.custom_metadata ? JSON.stringify(a.custom_metadata) : undefined },
197
+ }),
198
+ list: (conversationId, q) => this.request("GET", `/v1/conversations/${conversationId}/documents`, { query: q }),
199
+ get: (conversationId, docId) => this.request("GET", `/v1/conversations/${conversationId}/documents/${docId}`),
200
+ contentUrl: (conversationId, docId, disposition = "inline") => this.request("GET", `/v1/conversations/${conversationId}/documents/${docId}/content-url`, { query: { disposition } }),
201
+ delete: (conversationId, docId) => this.request("DELETE", `/v1/conversations/${conversationId}/documents/${docId}`),
202
+ /** Poll until the attachment is ready or failed (or timeout). */
203
+ waitReady: async (conversationId, docId, opts = {}) => {
204
+ const interval = opts.intervalMs ?? 3000;
205
+ const deadline = Date.now() + (opts.timeoutMs ?? 180_000);
206
+ for (;;) {
207
+ const d = await this.attachments.get(conversationId, docId);
208
+ if (d.status === "ready" || d.status === "failed" || Date.now() > deadline)
209
+ return d;
210
+ await sleep(interval);
211
+ }
212
+ },
213
+ };
214
+ // --- messages + feedback ---
215
+ messages = {
216
+ get: (id) => this.request("GET", `/v1/messages/${id}`),
217
+ createFeedback: (messageId, b) => this.request("POST", `/v1/messages/${messageId}/feedback`, { json: b }),
218
+ listFeedback: (messageId, q) => this.request("GET", `/v1/messages/${messageId}/feedback`, { query: q }),
219
+ };
220
+ feedback = {
221
+ get: (id) => this.request("GET", `/v1/feedback/${id}`),
222
+ delete: (id) => this.request("DELETE", `/v1/feedback/${id}`),
223
+ };
224
+ // --- parse jobs ---
225
+ parseJobs = {
226
+ list: (q) => this.request("GET", "/v1/parse_jobs", { query: q }),
227
+ get: (id) => this.request("GET", `/v1/parse_jobs/${id}`),
228
+ };
229
+ // --- agents (tenant read-only: choose an agent to query) ---
230
+ agents = {
231
+ list: (q) => this.request("GET", "/v1/agents", { query: q }),
232
+ listAll: () => this.pageAll("/v1/agents"),
233
+ get: (id) => this.request("GET", `/v1/agents/${id}`),
234
+ };
235
+ }
236
+ // ---------------------------------------------------------------------------
237
+ // Admin client (tenant + key + agent management) — use the ADMIN key
238
+ // ---------------------------------------------------------------------------
239
+ export class AdminClient extends HttpBase {
240
+ tenants = {
241
+ create: (b) => this.request("POST", "/v1/tenants", { json: b }),
242
+ list: (q) => this.request("GET", "/v1/tenants", { query: q }),
243
+ get: (id) => this.request("GET", `/v1/tenants/${id}`),
244
+ update: (id, b) => this.request("PATCH", `/v1/tenants/${id}`, { json: b }),
245
+ delete: (id, confirmName) => this.request("DELETE", `/v1/tenants/${id}`, { query: { confirm: confirmName } }),
246
+ /** Mint a tenant API key — the raw key is in the response ONCE; store it now. */
247
+ createApiKey: (tenantId, label) => this.request("POST", `/v1/tenants/${tenantId}/api-keys`, { json: { label } }),
248
+ listApiKeys: (tenantId, q) => this.request("GET", `/v1/tenants/${tenantId}/api-keys`, { query: q }),
249
+ revokeApiKey: (tenantId, keyId) => this.request("DELETE", `/v1/tenants/${tenantId}/api-keys/${keyId}`),
250
+ };
251
+ agents = {
252
+ /** tenant_id explicit, or null for a global/shared agent. */
253
+ create: (b) => this.request("POST", "/v1/agents", { json: b }),
254
+ update: (id, b) => this.request("PATCH", `/v1/agents/${id}`, { json: b }),
255
+ delete: (id) => this.request("DELETE", `/v1/agents/${id}`),
256
+ };
257
+ }
258
+ // ---------------------------------------------------------------------------
259
+ // helpers
260
+ // ---------------------------------------------------------------------------
261
+ function dispatchSse(frame, h) {
262
+ let event = "message";
263
+ const dataLines = [];
264
+ for (const line of frame.split("\n")) {
265
+ if (line.startsWith("event:"))
266
+ event = line.slice(6).trim();
267
+ else if (line.startsWith("data:"))
268
+ dataLines.push(line.slice(5).trim());
269
+ }
270
+ if (dataLines.length === 0)
271
+ return;
272
+ const data = safeJson(dataLines.join("\n"));
273
+ h.onEvent?.(event, data);
274
+ switch (event) {
275
+ case "hop":
276
+ h.onHop?.(data);
277
+ break;
278
+ case "retrieval_contents":
279
+ h.onSources?.(data);
280
+ break;
281
+ case "token":
282
+ h.onToken?.(data.text);
283
+ break;
284
+ case "final":
285
+ h.onFinal?.(data);
286
+ break;
287
+ case "error":
288
+ h.onError?.(data);
289
+ break;
290
+ case "done":
291
+ h.onDone?.();
292
+ break;
293
+ }
294
+ }
295
+ function sleep(ms) {
296
+ return new Promise((r) => setTimeout(r, ms));
297
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@babav/knowledge-core-client",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript client for the Babav Knowledge Core API (Deno + Node 18+, zero deps).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist", "src"],
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "registry": "https://registry.npmjs.org/"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/bkaponig/babav-knowledge-core.git",
22
+ "directory": "clients/typescript"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.6.0"
30
+ },
31
+ "engines": { "node": ">=18" },
32
+ "sideEffects": false,
33
+ "private": false,
34
+ "license": "UNLICENSED"
35
+ }