backprompter-server-sdk 0.1.1 → 0.1.3
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/README.md +118 -0
- package/index.js +271 -1
- package/package.json +23 -23
package/README.md
CHANGED
|
@@ -28,6 +28,124 @@ const result = await bp.run({
|
|
|
28
28
|
console.log(result.response);
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Attachments
|
|
32
|
+
|
|
33
|
+
`run()` takes `fileInputs` and `urlInputs`, and which key you use inside
|
|
34
|
+
`fileInputs` decides how an attachment is sent:
|
|
35
|
+
|
|
36
|
+
| Key | Sent as |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `url`, `dataUrl`, `base64` | image |
|
|
39
|
+
| `file_url`, `file_data` | document |
|
|
40
|
+
|
|
41
|
+
The backend infers the real type where it can — an explicit `mimeType`, the
|
|
42
|
+
data-URL prefix, the file extension, or the leading bytes of base64 — and falls
|
|
43
|
+
back to the key only when the type cannot be determined.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
await bp.run({
|
|
47
|
+
agentId: 'analyst',
|
|
48
|
+
userPrompt: 'What are the Q3 totals?',
|
|
49
|
+
fileInputs: [
|
|
50
|
+
{ file_url: 'https://example.com/q3.pdf', filename: 'q3.pdf' }, // document
|
|
51
|
+
{ url: 'https://example.com/chart.png' } // image
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Document support is per model: OpenAI models accept spreadsheets, while
|
|
57
|
+
Anthropic accepts PDF only and rejects other types outright. Pick a model that
|
|
58
|
+
handles what you send.
|
|
59
|
+
|
|
60
|
+
## Use your own login
|
|
61
|
+
|
|
62
|
+
Your app can keep its own login. Backprompter never has to see a password.
|
|
63
|
+
|
|
64
|
+
Your backend exchanges your user id for a session token. Your frontend signs in
|
|
65
|
+
with that token. The frontend then reaches every browser SDK function, including
|
|
66
|
+
`saveData`, `search`, file handling and voice.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// your backend, behind your own session check
|
|
70
|
+
app.post('/api/bp-token', requireMyAuth, async (req, res) => {
|
|
71
|
+
const { customToken } = await bp.createUserToken({
|
|
72
|
+
externalId: req.user.id, // your own user id
|
|
73
|
+
email: req.user.email // a label for your dashboard
|
|
74
|
+
});
|
|
75
|
+
res.json({ customToken });
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// your frontend, after your own login succeeds
|
|
81
|
+
const { customToken } = await fetch('/api/bp-token').then((r) => r.json());
|
|
82
|
+
await Backprompter.signInWithCustomToken(customToken);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Backprompter derives a storage id from your tenant id and your external id. The
|
|
86
|
+
same external id always returns to the same data. You cannot choose the id.
|
|
87
|
+
|
|
88
|
+
Keep the API key on your server. Only the token reaches the browser. A token is
|
|
89
|
+
valid for one user of one app.
|
|
90
|
+
|
|
91
|
+
## End-user data from your backend
|
|
92
|
+
|
|
93
|
+
You can also skip the browser session and do everything from your server. Call
|
|
94
|
+
`forUser()` with your own user id.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const user = bp.forUser('u_8821');
|
|
98
|
+
|
|
99
|
+
await user.saveData('preferences', { theme: 'dark' });
|
|
100
|
+
const prefs = await user.getData('preferences');
|
|
101
|
+
|
|
102
|
+
await user.saveConversation(conversationId, messages, { title: 'Support' });
|
|
103
|
+
const list = await user.listConversations();
|
|
104
|
+
|
|
105
|
+
await user.storeArtifacts([{ name: 'notes.md', content: text }]);
|
|
106
|
+
const hits = await user.search('last quarter revenue', { k: 5 });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Every call reads and writes that person's data only. The API key decides the app.
|
|
110
|
+
The external id decides the person.
|
|
111
|
+
|
|
112
|
+
The handle covers stored data. Agent calls stay on `bp.run()`, because a run is
|
|
113
|
+
not bound to a person. Pass what the agent should know about them in
|
|
114
|
+
`profileString`.
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
const profile = await user.getData('profile');
|
|
118
|
+
const result = await bp.run({
|
|
119
|
+
agentId: 'analyst',
|
|
120
|
+
userPrompt: 'What changed?',
|
|
121
|
+
profileString: JSON.stringify(profile.data)
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### API key scopes
|
|
126
|
+
|
|
127
|
+
New API keys carry three scopes: `run:public`, `enduser:data`, and
|
|
128
|
+
`identity:exchange`. Keys generated before these features existed carry only
|
|
129
|
+
`run:public`. To use `forUser()` or `createUserToken()`, generate a new key.
|
|
130
|
+
|
|
131
|
+
## Payments
|
|
132
|
+
|
|
133
|
+
An API key never triggers the Backprompter payment gate. Your backend is your
|
|
134
|
+
own integration, so you decide what a person may do.
|
|
135
|
+
|
|
136
|
+
Read the entitlement yourself when you want to enforce payment:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const { active, paymentRequired } = await bp.forUser('u_8821').getEntitlements();
|
|
140
|
+
if (paymentRequired && !active) return res.status(402).json({ error: 'Subscribe first' });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The gate applies only to end users signed in with a Backprompter token. That
|
|
144
|
+
includes users who signed in with a custom token from `createUserToken()`.
|
|
145
|
+
|
|
146
|
+
If you collect money outside Backprompter, leave the Backprompter payment
|
|
147
|
+
settings empty. No gate then applies to anyone.
|
|
148
|
+
|
|
31
149
|
## Notes
|
|
32
150
|
|
|
33
151
|
- Requires Node.js 18+ (uses built-in fetch).
|
package/index.js
CHANGED
|
@@ -65,7 +65,12 @@ export default class BackprompterServer {
|
|
|
65
65
|
baseText,
|
|
66
66
|
ragCharBudget,
|
|
67
67
|
tenantId,
|
|
68
|
-
apiKey
|
|
68
|
+
apiKey,
|
|
69
|
+
// Streaming: the Promise still resolves with the complete result; these
|
|
70
|
+
// callbacks see the answer as it is written. See _streamRun.
|
|
71
|
+
stream,
|
|
72
|
+
onDelta,
|
|
73
|
+
onStatus
|
|
69
74
|
} = {}) {
|
|
70
75
|
const resolvedApiKey = String(apiKey || this._apiKey || '').trim();
|
|
71
76
|
if (!resolvedApiKey) throw new Error('BackprompterServer: apiKey is required.');
|
|
@@ -88,12 +93,162 @@ export default class BackprompterServer {
|
|
|
88
93
|
if (baseText !== undefined) body.base_text = baseText;
|
|
89
94
|
if (ragCharBudget !== undefined) body.rag_char_budget = ragCharBudget;
|
|
90
95
|
|
|
96
|
+
if (stream) return this._streamRun(resolvedApiKey, body, { onDelta, onStatus });
|
|
91
97
|
return this._apiFetchWithApiKey('/public/run', resolvedApiKey, {
|
|
92
98
|
method: 'POST',
|
|
93
99
|
body: JSON.stringify(body)
|
|
94
100
|
});
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
// POST /public/run with "stream": true and read the server-sent events.
|
|
104
|
+
// Resolves with the same object as a non-streamed run (the `done` event).
|
|
105
|
+
// onDelta(text, fullTextSoFar) — each piece of the answer
|
|
106
|
+
// onStatus(event) — { type: 'meta', agentId, artifactType, streaming }
|
|
107
|
+
// { type: 'status', stage: 'routing' | 'retrieving' | 'tool', name? }
|
|
108
|
+
// { type: 'reset' } (discard the text so far)
|
|
109
|
+
// A plain JSON reply (a refusal, or a backend without streaming) is handled
|
|
110
|
+
// like an ordinary run.
|
|
111
|
+
async _streamRun(apiKey, body, { onDelta, onStatus } = {}) {
|
|
112
|
+
assertFetchAvailable();
|
|
113
|
+
const response = await fetch(`${this._backendUrl}/public/run`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'x-bp-api-key': apiKey },
|
|
116
|
+
body: JSON.stringify({ ...body, stream: true })
|
|
117
|
+
});
|
|
118
|
+
const type = response.headers.get('content-type') || '';
|
|
119
|
+
if (!type.startsWith('text/event-stream') || !response.body) {
|
|
120
|
+
const payload = await response.json().catch(() => ({}));
|
|
121
|
+
if (!response.ok) throw new Error(getErrorMessage(response.status, payload));
|
|
122
|
+
return payload;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const call = (fn, ...args) => {
|
|
126
|
+
if (typeof fn !== 'function') return;
|
|
127
|
+
try { fn(...args); } catch (e) { console.error('BackprompterServer stream callback error:', e); }
|
|
128
|
+
};
|
|
129
|
+
let text = '';
|
|
130
|
+
let result = null;
|
|
131
|
+
let failure = null;
|
|
132
|
+
const handle = (name, raw) => {
|
|
133
|
+
let data;
|
|
134
|
+
try { data = JSON.parse(raw); } catch (_) { return; }
|
|
135
|
+
if (name === 'delta') {
|
|
136
|
+
const piece = String((data && data.text) || '');
|
|
137
|
+
if (!piece) return;
|
|
138
|
+
text += piece;
|
|
139
|
+
call(onDelta, piece, text);
|
|
140
|
+
} else if (name === 'reset') {
|
|
141
|
+
text = '';
|
|
142
|
+
call(onStatus, { type: 'reset' });
|
|
143
|
+
} else if (name === 'meta' || name === 'status') {
|
|
144
|
+
call(onStatus, { type: name, ...data });
|
|
145
|
+
} else if (name === 'done') {
|
|
146
|
+
result = data;
|
|
147
|
+
} else if (name === 'error') {
|
|
148
|
+
failure = new Error(getErrorMessage(data && data.status, data));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let buffer = '';
|
|
153
|
+
let eventName = '';
|
|
154
|
+
let dataLines = [];
|
|
155
|
+
const line = (l) => {
|
|
156
|
+
if (l.endsWith('\r')) l = l.slice(0, -1);
|
|
157
|
+
if (l === '') {
|
|
158
|
+
if (dataLines.length) handle(eventName || 'message', dataLines.join('\n'));
|
|
159
|
+
eventName = '';
|
|
160
|
+
dataLines = [];
|
|
161
|
+
} else if (l.startsWith('event:')) {
|
|
162
|
+
eventName = l.slice(6).trim();
|
|
163
|
+
} else if (l.startsWith('data:')) {
|
|
164
|
+
dataLines.push(l.slice(5).replace(/^ /, ''));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const decoder = new TextDecoder();
|
|
168
|
+
for await (const chunk of response.body) {
|
|
169
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
170
|
+
let nl;
|
|
171
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
172
|
+
line(buffer.slice(0, nl));
|
|
173
|
+
buffer = buffer.slice(nl + 1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
buffer += decoder.decode();
|
|
177
|
+
if (buffer) line(buffer);
|
|
178
|
+
line('');
|
|
179
|
+
|
|
180
|
+
if (failure) throw failure;
|
|
181
|
+
if (!result) throw new Error('BackprompterServer: the reply stream ended before the answer was complete.');
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Identity ────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Exchange your own user id for a Backprompter session token.
|
|
189
|
+
*
|
|
190
|
+
* Call this from your backend after your own login succeeds. Send the token
|
|
191
|
+
* to your frontend and pass it to Backprompter.signInWithCustomToken(token).
|
|
192
|
+
* Your frontend then reaches every browser SDK function. Your API key stays
|
|
193
|
+
* on your server.
|
|
194
|
+
*
|
|
195
|
+
* The API key must carry the identity:exchange scope.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} opts
|
|
198
|
+
* @param {string} opts.externalId — your own user id, required
|
|
199
|
+
* @param {string} [opts.email] — label only, shown in your dashboard
|
|
200
|
+
* @param {string} [opts.displayName]
|
|
201
|
+
* @param {object} [opts.claims] — extra claims. Reserved names are dropped.
|
|
202
|
+
* @returns {Promise<{ customToken: string, uid: string, tenantId: string }>}
|
|
203
|
+
*/
|
|
204
|
+
async createUserToken({ externalId, email, displayName, claims, tenantId, apiKey } = {}) {
|
|
205
|
+
const resolvedExternalId = String(externalId || '').trim();
|
|
206
|
+
if (!resolvedExternalId) throw new Error('BackprompterServer: externalId is required.');
|
|
207
|
+
|
|
208
|
+
const body = { externalId: resolvedExternalId };
|
|
209
|
+
if (email) body.email = String(email).trim();
|
|
210
|
+
if (displayName) body.displayName = String(displayName).trim();
|
|
211
|
+
if (claims && typeof claims === 'object') body.claims = claims;
|
|
212
|
+
|
|
213
|
+
return this._endUserFetch('exchange', {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
body: JSON.stringify(body)
|
|
216
|
+
}, { tenantId, apiKey });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── End-user data ───────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A handle that acts for one of your users. Every call on it reads and writes
|
|
223
|
+
* that person's own data, and nobody else's.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} externalId — your own user id
|
|
226
|
+
* @returns {BackprompterUser}
|
|
227
|
+
*/
|
|
228
|
+
forUser(externalId) {
|
|
229
|
+
const resolved = String(externalId || '').trim();
|
|
230
|
+
if (!resolved) throw new Error('BackprompterServer: externalId is required.');
|
|
231
|
+
return new BackprompterUser(this, resolved);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_requireTenantId(tenantId) {
|
|
235
|
+
const resolved = String(tenantId || this._tenantId || '').trim();
|
|
236
|
+
if (!resolved) throw new Error('BackprompterServer: tenantId is required.');
|
|
237
|
+
return resolved;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async _endUserFetch(subPath, options = {}, { tenantId, apiKey, externalId } = {}) {
|
|
241
|
+
const resolvedApiKey = String(apiKey || this._apiKey || '').trim();
|
|
242
|
+
if (!resolvedApiKey) throw new Error('BackprompterServer: apiKey is required.');
|
|
243
|
+
const resolvedTenantId = this._requireTenantId(tenantId);
|
|
244
|
+
|
|
245
|
+
const headers = { ...(options.headers || {}) };
|
|
246
|
+
if (externalId) headers['x-bp-end-user'] = String(externalId);
|
|
247
|
+
|
|
248
|
+
const path = `/api/app/${encodeURIComponent(resolvedTenantId)}/enduser/${subPath}`;
|
|
249
|
+
return this._apiFetchWithApiKey(path, resolvedApiKey, { ...options, headers });
|
|
250
|
+
}
|
|
251
|
+
|
|
97
252
|
async _apiFetchWithApiKey(path, apiKey, options = {}) {
|
|
98
253
|
assertFetchAvailable();
|
|
99
254
|
|
|
@@ -117,3 +272,118 @@ export default class BackprompterServer {
|
|
|
117
272
|
return payload;
|
|
118
273
|
}
|
|
119
274
|
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Acts for one of your users. Create it with backprompterServer.forUser(id).
|
|
278
|
+
*
|
|
279
|
+
* The API key decides which app the calls reach. The external id decides which
|
|
280
|
+
* person inside that app. Backprompter derives a stable storage id from the two,
|
|
281
|
+
* so the same external id always returns to the same data.
|
|
282
|
+
*/
|
|
283
|
+
export class BackprompterUser {
|
|
284
|
+
constructor(server, externalId) {
|
|
285
|
+
this._server = server;
|
|
286
|
+
this._externalId = externalId;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
get externalId() {
|
|
290
|
+
return this._externalId;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_fetch(subPath, options = {}) {
|
|
294
|
+
return this._server._endUserFetch(subPath, options, { externalId: this._externalId });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Mint a browser session token for this user. */
|
|
298
|
+
createToken(opts = {}) {
|
|
299
|
+
return this._server.createUserToken({ ...opts, externalId: this._externalId });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Custom data ───────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
saveData(docId, data) {
|
|
305
|
+
return this._fetch(`data/${encodeURIComponent(docId)}`, {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
body: JSON.stringify({ data })
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getData(docId) {
|
|
312
|
+
return this._fetch(`data/${encodeURIComponent(docId)}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
deleteData(docId) {
|
|
316
|
+
return this._fetch(`data/${encodeURIComponent(docId)}`, { method: 'DELETE' });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Conversations ─────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
saveConversation(conversationId, messages, metadata = {}) {
|
|
322
|
+
return this._fetch(`conversations/${encodeURIComponent(conversationId)}`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
body: JSON.stringify({ messages, metadata })
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
listConversations() {
|
|
329
|
+
return this._fetch('conversations');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
getConversation(conversationId) {
|
|
333
|
+
return this._fetch(`conversations/${encodeURIComponent(conversationId)}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
deleteConversation(conversationId) {
|
|
337
|
+
return this._fetch(`conversations/${encodeURIComponent(conversationId)}`, { method: 'DELETE' });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Artifacts ─────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
storeArtifacts(files) {
|
|
343
|
+
return this._fetch('artifacts', {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
body: JSON.stringify({ files })
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
listArtifacts() {
|
|
350
|
+
return this._fetch('artifacts');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getArtifact(docId) {
|
|
354
|
+
return this._fetch(`artifacts/${encodeURIComponent(docId)}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
deleteArtifact(docId) {
|
|
358
|
+
return this._fetch(`artifacts/${encodeURIComponent(docId)}`, { method: 'DELETE' });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Search ────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
search(query, { searchIn = ['conversations', 'artifacts'], k = 5 } = {}) {
|
|
364
|
+
return this._fetch('search', {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
body: JSON.stringify({ query, searchIn, k })
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Payments ──────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
getEntitlements() {
|
|
373
|
+
return this._fetch('entitlements');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
createCheckoutSession({ successUrl, cancelUrl } = {}) {
|
|
377
|
+
return this._fetch('checkout-session', {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
body: JSON.stringify({ successUrl, cancelUrl })
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
createBillingPortalSession({ returnUrl } = {}) {
|
|
384
|
+
return this._fetch('billing-portal', {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
body: JSON.stringify({ returnUrl })
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "backprompter-server-sdk",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Backprompter server-side SDK for Node.js backends",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.js"
|
|
9
|
-
},
|
|
10
|
-
"publishConfig": {
|
|
11
|
-
"access": "public"
|
|
12
|
-
},
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=18"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"backprompter",
|
|
18
|
-
"sdk",
|
|
19
|
-
"server",
|
|
20
|
-
"node"
|
|
21
|
-
],
|
|
22
|
-
"license": "MIT"
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "backprompter-server-sdk",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Backprompter server-side SDK for Node.js backends",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"backprompter",
|
|
18
|
+
"sdk",
|
|
19
|
+
"server",
|
|
20
|
+
"node"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT"
|
|
23
|
+
}
|