@beakr/sdk 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +530 -2
- package/dist/index.cjs +5316 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +24940 -0
- package/dist/index.d.ts +24940 -0
- package/dist/index.js +5293 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -4
package/README.md
CHANGED
|
@@ -1,3 +1,531 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @beakr/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The TypeScript SDK for the Beakr public API. No runtime dependencies: it uses
|
|
4
|
+
`fetch` and Web Crypto, so it runs in Node 18+, Deno, Bun, browsers and edge
|
|
5
|
+
runtimes. Ships ESM and CommonJS with type definitions generated from the
|
|
6
|
+
published API contract.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @beakr/sdk # not on npm yet; see "Installing before the first release"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The [Python SDK](https://github.com/BeakrHub/beakr-python) (`beakr-sdk`) has the
|
|
13
|
+
same resources, and the [Beakr CLI](https://github.com/BeakrHub/beakr-cli) is
|
|
14
|
+
built on it.
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
Create a key in the Beakr web app (Settings, API keys; keys are created only
|
|
19
|
+
there) and keep it in `BEAKR_API_KEY`.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { Beakr } from "@beakr/sdk";
|
|
23
|
+
|
|
24
|
+
const beakr = new Beakr(); // reads BEAKR_API_KEY, or pass { apiKey }
|
|
25
|
+
// The API: `baseUrl`, else BEAKR_BASE_URL, else BEAKR_API_URL, else production.
|
|
26
|
+
|
|
27
|
+
const me = await beakr.me.get();
|
|
28
|
+
const projects = await beakr.projects.list(); // one page; beakr.projects.iter() follows every page
|
|
29
|
+
|
|
30
|
+
// Read Knowledge through the same door Beakr's own agents use.
|
|
31
|
+
const page = await beakr.knowledge.command("cat", { page: "Pump calibration" }, { scope: "Assay Lab" });
|
|
32
|
+
|
|
33
|
+
// Context for an agent starting work, ready to drop into CLAUDE.md.
|
|
34
|
+
const context = await beakr.engine.context({ projectId: "...", format: "claude_md" });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Where the credential comes from
|
|
38
|
+
|
|
39
|
+
The same order as the Python SDK, first match wins:
|
|
40
|
+
|
|
41
|
+
1. `apiKey` or `tokens` passed to the client;
|
|
42
|
+
2. `BEAKR_API_KEY`;
|
|
43
|
+
3. `serviceAccount` passed to the client (a parsed file or its path);
|
|
44
|
+
4. in Node.js, the Beakr CLI's sign-in for this API (`beakr auth login`): read
|
|
45
|
+
from `~/.beakr/credentials` (`$BEAKR_HOME/credentials`) when that holds a
|
|
46
|
+
current one, otherwise asked of the installed CLI (`beakr auth token --json`);
|
|
47
|
+
5. in Node.js, the key saved by `beakr auth login --with-api-key`, when it was saved
|
|
48
|
+
for this API;
|
|
49
|
+
6. the service account file named by `BEAKR_SERVICE_ACCOUNT_FILE`, else
|
|
50
|
+
`~/.beakr/service-account.json` when it exists.
|
|
51
|
+
|
|
52
|
+
A stored sign-in or saved key is only used by a client pointed at the API that
|
|
53
|
+
issued it. Pass `cliLogin: false` to skip links 4 and 5. Links 4 to 6 are
|
|
54
|
+
resolved at the client's first request and work on every supported Node
|
|
55
|
+
version (18 and later).
|
|
56
|
+
|
|
57
|
+
The CLI keeps its sign-in in the OS keychain when one is available (the
|
|
58
|
+
default on macOS), and refreshes it under a lock that only the CLI takes:
|
|
59
|
+
refreshing rotates the pair, and presenting a spent refresh token revokes the
|
|
60
|
+
login everywhere. So the SDK never reads the keychain or refreshes the sign-in
|
|
61
|
+
itself. When the credentials file has no current sign-in for the API -- it is
|
|
62
|
+
in the keychain, or its access token has expired -- the SDK runs the CLI
|
|
63
|
+
(`BEAKR_CLI_PATH`, else `beakr` on the `PATH`), which reads it the way every
|
|
64
|
+
`beakr` command does and refreshes it if needed, and keeps the access token it
|
|
65
|
+
prints in memory until it expires. After a 401 on that token the SDK asks the
|
|
66
|
+
CLI once more (naming the refused token by its SHA-256, never on the command
|
|
67
|
+
line) and resends once. The refresh token never leaves the CLI. Without the
|
|
68
|
+
CLI installed, an expired stored sign-in fails with `cli_login_expired`, and a
|
|
69
|
+
keychain sign-in is not found; pass a credential instead.
|
|
70
|
+
|
|
71
|
+
### Use a token pair you already hold
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const signedIn = new Beakr({
|
|
75
|
+
// An OAuth pair from Beakr's sign-in issuer, with where it refreshes.
|
|
76
|
+
tokens, // { accessToken, refreshToken, expiresAt, tokenEndpoint, clientId }
|
|
77
|
+
onTokens: (rotated) => saveSomewhere(rotated), // a refresh may rotate; persist each pair
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The pair is refreshed at its issuer's `tokenEndpoint` (as the public client
|
|
82
|
+
`clientId`) before it expires and on a 401; a pair without a `tokenEndpoint` is
|
|
83
|
+
used until it expires and is not refreshed. To sign a person in, use the CLI
|
|
84
|
+
(`beakr auth login`); the SDK then finds that sign-in on its own (above).
|
|
85
|
+
|
|
86
|
+
### Upload a folder, download a file
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// Node: walk a directory; sub-directories become folders, dotfiles are skipped.
|
|
90
|
+
const uploaded = await beakr.uploads.uploadFolder("./docs", { projectId });
|
|
91
|
+
|
|
92
|
+
// Browser or edge: the same, from entries (e.g. an <input webkitdirectory> list).
|
|
93
|
+
await beakr.uploads.uploadEntries(
|
|
94
|
+
[...input.files].map((file) => ({ path: file.webkitRelativePath, data: file })),
|
|
95
|
+
{ projectId, ingest: false }, // ingest: false keeps the files out of Knowledge
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const { data, filename } = await beakr.files.download(artifactId); // any runtime
|
|
99
|
+
await beakr.files.downloadTo(artifactId, "./downloads/"); // Node; a trailing / means a folder, created if missing
|
|
100
|
+
|
|
101
|
+
// Search the original documents by meaning, then download a result in one step.
|
|
102
|
+
const found = await beakr.knowledge.documentsSearch("who covers losses from defects");
|
|
103
|
+
const link = await beakr.files.downloadLink(found.results![0]!.source_ref!);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Many files or whole directories go up as one batch with one indexing job:
|
|
107
|
+
`const batch = await beakr.uploads.uploadMany(["results", "summary.pdf"], { projectId })`,
|
|
108
|
+
then `await beakr.jobs.wait(batch)` (or pass `wait: true`).
|
|
109
|
+
|
|
110
|
+
### Tell Knowledge what a project is about
|
|
111
|
+
|
|
112
|
+
A project's Knowledge profile steers how Beakr compiles its files: what the
|
|
113
|
+
project is for, the people, organizations and topics that matter, and the kinds
|
|
114
|
+
of things to build pages for. Only the project's owners may set it.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
await beakr.projects.setKnowledgeProfile(projectId, {
|
|
118
|
+
description: "Batch records, deviations and CAPAs for the Basel site",
|
|
119
|
+
goals: ["Track deviations to closure"],
|
|
120
|
+
entity_types: ["batch", "deviation", "capa", "supplier"],
|
|
121
|
+
core_people: [{ name: "Priya Natarajan", role: "Batch owner" }],
|
|
122
|
+
});
|
|
123
|
+
await beakr.projects.updateKnowledgeProfile(projectId, { focus_areas: ["pH excursions"] });
|
|
124
|
+
const profile = await beakr.projects.knowledgeProfile(projectId);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`knowledgeProfile` throws `NotFoundError` (`code === "knowledge_profile_not_found"`)
|
|
128
|
+
while the project has none.
|
|
129
|
+
|
|
130
|
+
### Skills
|
|
131
|
+
|
|
132
|
+
A skill is a SKILL.md folder your agent loads on demand. `skills.list` covers
|
|
133
|
+
your own skills, your organization's, the built-in ones and public skills from
|
|
134
|
+
anywhere; each carries `visibility`, `owner` (`me`, `org`, `system`, `anyone`),
|
|
135
|
+
`origin` (`created`, `mined`, `imported`, `system`) and `installed`, and each
|
|
136
|
+
is also a filter.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const mine = await beakr.skills.list({ owner: "me" });
|
|
140
|
+
const toInstall = await beakr.skills.list({ visibility: "public", installed: false });
|
|
141
|
+
for await (const skill of beakr.skills.iter({ owner: ["org", "system"] })) console.log(skill.slug);
|
|
142
|
+
|
|
143
|
+
const skill = await beakr.skills.createFromFolder("skills/plate-reader"); // or a .zip (Node)
|
|
144
|
+
await beakr.skills.download(skill.id, "exported/"); // exported/plate-reader/ (created if missing)
|
|
145
|
+
const [installable] = toInstall.data;
|
|
146
|
+
if (installable) await beakr.skills.install(installable.id);
|
|
147
|
+
for (const proposal of (await beakr.skills.proposals.list()).data) {
|
|
148
|
+
await beakr.skills.proposals.accept(proposal.id);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`skills.get` takes an id or a slug; a slug resolves to your own skill first,
|
|
153
|
+
then a built-in, then one you installed, then your organization's.
|
|
154
|
+
`uploadBundle(skillId, files, { expectedSha256: bundle.files_sha256 })` throws
|
|
155
|
+
`ConflictError` (`code === "skill_conflict"`) when the skill changed since you
|
|
156
|
+
read it. The digest is the bundle's `files_sha256`; passing `expectedSha256:
|
|
157
|
+
undefined` throws a `TypeError` rather than silently writing without the check.
|
|
158
|
+
|
|
159
|
+
### Know when Knowledge is ready
|
|
160
|
+
|
|
161
|
+
Uploaded files reach a project's Knowledge in the background after indexing. Wait for it
|
|
162
|
+
before asking questions that depend on those files, and retry what failed:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { KnowledgeFailedError } from "@beakr/sdk";
|
|
166
|
+
|
|
167
|
+
// The batch's own files: indexing, then Knowledge.
|
|
168
|
+
const result = await beakr.uploads.uploadMany("./docs", {
|
|
169
|
+
projectId,
|
|
170
|
+
wait: "knowledge",
|
|
171
|
+
knowledgeWait: { timeoutMs: 30 * 60_000 },
|
|
172
|
+
});
|
|
173
|
+
console.log(result.batch?.knowledge); // { queued: 0, running: 0, created: 41, failed: 1, ... }
|
|
174
|
+
|
|
175
|
+
// The whole project.
|
|
176
|
+
const status = await beakr.projects.waitUntilReady(projectId, { timeoutMs: 15 * 60_000 });
|
|
177
|
+
for (const failed of status.failed_files ?? []) {
|
|
178
|
+
console.log(failed.path, failed.error_code, failed.reason); // knowledgeFailures() lists all
|
|
179
|
+
}
|
|
180
|
+
if (status.failed) await beakr.projects.retryFailed(projectId); // or retryFailed(projectId, [id])
|
|
181
|
+
|
|
182
|
+
await beakr.projects.waitUntilReady(projectId, { raiseOnFailure: true }).catch((error) => {
|
|
183
|
+
if (error instanceof KnowledgeFailedError) console.log(error.status.failed_files);
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`ready` is true when nothing is queued or running (`KnowledgeTimeoutError` at the timeout).
|
|
188
|
+
Failed files never end a wait early; `error_code` is one of `processing_failed`, `timed_out`,
|
|
189
|
+
`concurrent_update` or `compile_failed`, and a retry is safe to repeat.
|
|
190
|
+
|
|
191
|
+
Every file goes through the resumable upload (a small file is one part); an interrupted one
|
|
192
|
+
rejects with `UploadInterruptedError`, whose `uploadId` resumes it with
|
|
193
|
+
`beakr.uploads.resume`. Folder uploads do not follow symbolic links. Node-only
|
|
194
|
+
helpers (`uploadFolder`, `downloadTo`) load `node:` modules lazily, so the SDK still imports
|
|
195
|
+
in edge and browser runtimes.
|
|
196
|
+
|
|
197
|
+
### Import a coding-agent session and compound it
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const result = await beakr.conversations.import({
|
|
201
|
+
client: "claude-code",
|
|
202
|
+
sessionId: "session-123",
|
|
203
|
+
baseIndex: alreadySent, // send only new messages, and say where they start
|
|
204
|
+
messages: [{ role: "user", content: "Use the April calibration from now on." }],
|
|
205
|
+
compound: true, // import and queue compounding in one request
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Message text is kept only by opt-in. `compound: true` holds it, for a bounded
|
|
210
|
+
time, until the capture reads it; `holdText: true` holds it the same way for a
|
|
211
|
+
later `beakr.engine.compound(...)` without queuing one now; `storeFullText:
|
|
212
|
+
true` keeps it. A plain import keeps the summary and metadata only, and a later
|
|
213
|
+
compound answers `nothing_to_compound`.
|
|
214
|
+
|
|
215
|
+
`beakr.engine.cancelCompound(conversationId)` stops a queued or running
|
|
216
|
+
compound: a batch already being read finishes and its proposals stay, no further
|
|
217
|
+
batch starts, and the conversation's `compound_status` reads `cancelled`.
|
|
218
|
+
`engine.compound` resumes from where it stopped.
|
|
219
|
+
|
|
220
|
+
Without `baseIndex` a batch is read as the whole session from the start. A
|
|
221
|
+
`baseIndex` past the number of messages already stored (a gap) rejects with a
|
|
222
|
+
`ConflictError` whose `expectedBaseIndex` says where to resend from. A
|
|
223
|
+
`baseIndex` below that count is not an error: the stored session is replaced
|
|
224
|
+
from that position on, by design for clients that compact their transcripts,
|
|
225
|
+
so a stale resend rewrites history rather than failing. Track what you sent.
|
|
226
|
+
|
|
227
|
+
### Query what the caller has connected
|
|
228
|
+
|
|
229
|
+
The same read-only tools Beakr's own agent uses on Slack, Drive, Jira and the
|
|
230
|
+
rest, against the integrations the caller connected:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const { data: tools } = await beakr.integrations.tools({ provider: "slack" });
|
|
234
|
+
const result = await beakr.integrations.callTool("slack_search_messages", { query: "calibration" });
|
|
235
|
+
console.log(result.content, result.sources);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Tools that change data in the connected account are refused.
|
|
239
|
+
|
|
240
|
+
### Send an interview, read what came back
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const link = await beakr.interviews.createLink("Assay onboarding", {
|
|
244
|
+
goal: "How the team runs the plate reader",
|
|
245
|
+
recipient: { name: "Ada", email: "ada@example.com" }, // recorded only; Beakr sends nothing
|
|
246
|
+
});
|
|
247
|
+
sendItYourself(link.url); // the link is shown once
|
|
248
|
+
const result = await beakr.interviews.linkResult(link.link_id);
|
|
249
|
+
if (result.status === "completed") await beakr.interviews.sendToProjects(link.link_id, [projectId]);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Tokens for the users of your own product
|
|
253
|
+
|
|
254
|
+
With a service account's key, mirror your users and mint each a short-lived
|
|
255
|
+
token that can never do more than the key. The service account and its key are
|
|
256
|
+
made in the web app (Settings > Organization > Developer); creating either from
|
|
257
|
+
an API key or token is refused with `session_required`. Add the service account
|
|
258
|
+
to the projects it manages before attaching labels to them.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// Acts as the external user: its label and project memberships decide what it reaches.
|
|
262
|
+
const user = await beakr.externalUsers.create({ external_id: "acme-42", name: "Ada" });
|
|
263
|
+
const label = await beakr.labels.create({ name: "acme" });
|
|
264
|
+
await beakr.labels.addMember(label.id, { user_id: user.user_id });
|
|
265
|
+
await beakr.labels.attachProject(label.id, { project_id: projectId }); // viewer
|
|
266
|
+
const { token } = await beakr.endUserTokens.mint({
|
|
267
|
+
externalUserId: user.user_id,
|
|
268
|
+
scopes: ["kb:read", "agent:run"],
|
|
269
|
+
ttlSeconds: 900,
|
|
270
|
+
});
|
|
271
|
+
const asUser = new Beakr({ apiKey: token });
|
|
272
|
+
|
|
273
|
+
// Or: acts as your service account, confined to the projects you name.
|
|
274
|
+
const scoped = await beakr.endUserTokens.mint({
|
|
275
|
+
subject: "acme-42",
|
|
276
|
+
projectIds: [projectId],
|
|
277
|
+
scopes: ["kb:read"],
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Give either `externalUserId`, or `subject` with `projectIds`; the server refuses both.
|
|
282
|
+
|
|
283
|
+
### What this installation serves: `beakr.meta.capabilities()`
|
|
284
|
+
|
|
285
|
+
A self-hosted Beakr installation serves only part of the documented contract
|
|
286
|
+
(no organization administration, external users, end-user tokens, service
|
|
287
|
+
accounts or integrations) and answers a bare 404 for the rest. Each server
|
|
288
|
+
lists what it serves in its own `GET /v1/public/openapi.json`;
|
|
289
|
+
`capabilities()` reads it once per base URL (cached for five minutes) so a
|
|
290
|
+
client can hide what is not there instead of failing on it:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const caps = await beakr.meta.capabilities();
|
|
294
|
+
if (!caps.serves("GET", "/v1/integrations")) {
|
|
295
|
+
console.log("This Beakr installation does not provide integrations");
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`serves` takes a concrete path or a contract template. A server that publishes
|
|
300
|
+
no document is assumed to serve everything, and a route outside the documented
|
|
301
|
+
contract is always assumed served: the document cannot speak for it.
|
|
302
|
+
|
|
303
|
+
## Errors
|
|
304
|
+
|
|
305
|
+
Every failure is a `BeakrError` with `status`, `code` (from the error
|
|
306
|
+
envelope's `error.code`), `message` and `requestId`. Subclasses:
|
|
307
|
+
`AuthenticationError` (401), `PermissionDeniedError` (403, with
|
|
308
|
+
`requiredScope`; `PersonReviewRequiredError` for `person_review_required`,
|
|
309
|
+
where only a person or a key holding `proposals:accept` may accept proposals
|
|
310
|
+
or commit Knowledge), `NotFoundError`, `ConflictError`, `ValidationError`,
|
|
311
|
+
`RateLimitError` (with `retryAfter`), `ServerError`, `APIConnectionError`,
|
|
312
|
+
`KnowledgeCommandError` (a refused Knowledge command), `UnexpectedResponseError`
|
|
313
|
+
(a success whose body is an HTML page where JSON was expected) and
|
|
314
|
+
`DeviceLoginError`; `runs.answer()` throws `RunFailedError` for a run
|
|
315
|
+
that ended `error`, `failed` or `cancelled`. An error from the API ends its
|
|
316
|
+
message with `(called <baseUrl>)` and carries `baseUrl`, so a key sent to the
|
|
317
|
+
wrong host reads as that. A body that is not JSON is never the message: an HTML
|
|
318
|
+
page (a base URL that is not a Beakr API, or a proxy in front of it) is named by
|
|
319
|
+
its title, with a `fix` saying to check the base URL, and other text is cut to a
|
|
320
|
+
short excerpt. Branch on `code`, never on the message.
|
|
321
|
+
|
|
322
|
+
Codes a program should expect from sensitive actions: `session_required`
|
|
323
|
+
(do it in the web app), `mfa_required` (verify a second factor and retry),
|
|
324
|
+
`client_not_allowed`, `insufficient_scope`. Web app only, always refused to API
|
|
325
|
+
keys and tokens: creating API keys, creating a service account, minting or
|
|
326
|
+
disabling its keys. Deleting a project and changing a member's role are refused
|
|
327
|
+
to keys and tokens wherever the organization protects sensitive actions (SSO or
|
|
328
|
+
step-up required).
|
|
329
|
+
|
|
330
|
+
`KnowledgeCommandError` carries the response's `status` and `requestId` like
|
|
331
|
+
every other error, the door's `outcome.error_code` as `code` when it named one
|
|
332
|
+
(`invalid_arguments`, `unknown_command`, `scope_not_found`,
|
|
333
|
+
`citation_not_citeable`, ...; otherwise `knowledge_command_error`), and the
|
|
334
|
+
whole reply as `result`.
|
|
335
|
+
|
|
336
|
+
## Retries and idempotency
|
|
337
|
+
|
|
338
|
+
- 429 is retried for every request, waiting the server's `Retry-After`. The
|
|
339
|
+
header may be seconds or an HTTP-date (IMF-fixdate, or the obsolete RFC 850
|
|
340
|
+
and asctime forms); a date already past retries at once, and one that is not
|
|
341
|
+
a valid date is ignored in favor of the normal backoff. A wait beyond 60
|
|
342
|
+
seconds is not taken: the error is thrown instead, with
|
|
343
|
+
`RateLimitError.retryAfter` in seconds.
|
|
344
|
+
- 500, 502, 503, 504 and connection errors are retried only where a resend
|
|
345
|
+
cannot duplicate an effect: reads, `PUT`, `DELETE`, starting an agent run
|
|
346
|
+
(the route deduplicates), and every public create.
|
|
347
|
+
- Creates send an `Idempotency-Key` that stays the same across retries, and
|
|
348
|
+
the engine replays a create resent with the same key and body: the retry
|
|
349
|
+
returns the first result, never a second resource (`REPLAYING_CREATES` in
|
|
350
|
+
`src/replay.ts`). A resend that meets an earlier attempt still holding the
|
|
351
|
+
key (409 `idempotency_in_progress`) waits its `Retry-After` and tries again.
|
|
352
|
+
- Other POSTs (decisions such as accept, token exchanges, reads with a body)
|
|
353
|
+
are never resent after an ambiguous failure; 501, 505 and other 4xx are
|
|
354
|
+
never retried.
|
|
355
|
+
|
|
356
|
+
`maxRetries` (default 2) and `timeoutMs` (default 60 000) are client options.
|
|
357
|
+
|
|
358
|
+
## Resources
|
|
359
|
+
|
|
360
|
+
The client covers the documented API only:
|
|
361
|
+
|
|
362
|
+
`me`, `meta` (`openapi`, `capabilities`), `projects`, `members`, `org` (`serviceAccounts`),
|
|
363
|
+
`externalUsers`, `endUserTokens`, `labels`, `uploads` (one file, a folder,
|
|
364
|
+
resume), `files` (downloads), `jobs` (`wait`), `knowledge` (with
|
|
365
|
+
`proposals` and `pages.sources()`), `conversations`, `engine` (`context`,
|
|
366
|
+
`compound`, `cancelCompound`), `runs`, `interviews`, `skills` (with `proposals`) and
|
|
367
|
+
`integrations` (with `tools` / `callTool`).
|
|
368
|
+
|
|
369
|
+
### Ask the agent
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
import { sourceWhere } from "@beakr/sdk";
|
|
373
|
+
|
|
374
|
+
const { text, sources, run_id } = await beakr.runs.answer("How long is a compile lease?", { projectId });
|
|
375
|
+
console.log(text); // "... lasts 300 seconds (compile-lease.md › line 4)."
|
|
376
|
+
for (const source of sources) console.log(sourceWhere(source));
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`text` shows each citation as the readable reference in parentheses, as the
|
|
380
|
+
engine renders it (`(memo.pdf › p. 7)`, `(Page Title › Section)`), and
|
|
381
|
+
`sources` lists the cited documents in order of first use, one public source
|
|
382
|
+
record each: `filename`, `provider`, `location` (`url`/`display_path` of the
|
|
383
|
+
original, or `file_id` for Beakr's copy), and the cited `evidence`.
|
|
384
|
+
`knowledge.pages.sources(pageId)` returns the same records. To show the answer
|
|
385
|
+
as it is written, `for await (const chunk of beakr.runs.streamText(runId))`
|
|
386
|
+
yields the visible text with each citation key numbered `[n]` in order of
|
|
387
|
+
first use; those numbers are for display while streaming and do not index
|
|
388
|
+
`runs.answer()`'s `sources`. `runs.stream()` yields the raw server-sent
|
|
389
|
+
events.
|
|
390
|
+
|
|
391
|
+
A run reads the organization's Knowledge unless `knowledge: false` is passed
|
|
392
|
+
to `runs.start` or `runs.answer`; the server treats a run that does not ask for
|
|
393
|
+
Knowledge as one with it turned off, so the SDK always sends the toggle.
|
|
394
|
+
|
|
395
|
+
Uploads use the resumable flow (`/v1/uploads`) and declare the file's
|
|
396
|
+
SHA-256 (`content_hash`), hashed as a stream so no runtime holds the whole file.
|
|
397
|
+
|
|
398
|
+
Every operation in the documented contract also has a typed method on
|
|
399
|
+
`beakr.api`, grouped by area and generated from the contract:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const project = await beakr.api.projects.createProject(
|
|
403
|
+
{ body: { name: "Assays" } },
|
|
404
|
+
{ idempotencyKey: "create-assays" }, // a retry with the same key replays, never duplicates
|
|
405
|
+
);
|
|
406
|
+
await beakr.api.knowledge.getWikiPage({ path: { page_id: pageId } });
|
|
407
|
+
for await (const conversation of beakr.api.conversations.listConversationsAll()) {
|
|
408
|
+
console.log(conversation);
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Creates send an `Idempotency-Key` (pass your own to make retries across processes
|
|
413
|
+
safe); operations that page with a cursor also get an `...All()` iterator.
|
|
414
|
+
`OPERATIONS` lists every method's HTTP method and path, and
|
|
415
|
+
`test/contract-coverage.test.ts` fails if any contract operation lacks one.
|
|
416
|
+
|
|
417
|
+
## Development
|
|
418
|
+
|
|
419
|
+
```sh
|
|
420
|
+
npm install
|
|
421
|
+
npm run regen # after an engine contract change: generate, typecheck, build
|
|
422
|
+
npm run generate # regenerate src/generated/ from the documented contract
|
|
423
|
+
npm run build
|
|
424
|
+
npm test
|
|
425
|
+
npm run typecheck
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Edge support is tested in Vercel's Edge Runtime context (`@edge-runtime/vm`,
|
|
429
|
+
`test/edge-runtime.test.ts`): no `process`, `require` or `Buffer`, requests
|
|
430
|
+
through a mocked `fetch`.
|
|
431
|
+
|
|
432
|
+
`npm run generate` reads, in order: a path or URL argument, `BEAKR_OPENAPI`, or
|
|
433
|
+
the committed `docs/api/public-openapi.json` of the engine checkout
|
|
434
|
+
(`BEAKR_ENGINE_PATH`, else a sibling `beakr-engine`), else this repository's
|
|
435
|
+
snapshot of it.
|
|
436
|
+
|
|
437
|
+
CI reads no other repository. The files it checks against are committed under
|
|
438
|
+
`snapshots/`: `snapshots/beakr-engine` (the documented contract and the API
|
|
439
|
+
changelog; never the engine's internal contract) and `snapshots/beakr-python`
|
|
440
|
+
(the release scripts every client repository shares, which must match ours).
|
|
441
|
+
`snapshots/manifest.json` pins the commit each came from (with
|
|
442
|
+
`uncommitted_changes` when taken from a working tree) and each file's SHA-256.
|
|
443
|
+
After an engine or beakr-python change, refresh them from checkouts beside this
|
|
444
|
+
one (or named with `--engine` / `--python`), then regenerate:
|
|
445
|
+
|
|
446
|
+
```sh
|
|
447
|
+
python3 scripts/refresh_snapshots.py refresh # rewrite snapshots/ and the manifest
|
|
448
|
+
python3 scripts/refresh_snapshots.py diff # exit 1 when stale against the checkouts
|
|
449
|
+
python3 scripts/refresh_snapshots.py verify # offline: the files are the manifest's (CI)
|
|
450
|
+
BEAKR_ENGINE_PATH=snapshots/beakr-engine npm run regen
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The engine's CI checks this repository and beakr-python out on the branch named
|
|
454
|
+
like its own and runs these checks against the real repositories, so drift
|
|
455
|
+
shows up there before a refresh here.
|
|
456
|
+
Resource methods take their request and response types from that schema, so a
|
|
457
|
+
contract change that breaks a method fails `npm run typecheck`.
|
|
458
|
+
|
|
459
|
+
The documented contract is the only one this package is built from: it carries
|
|
460
|
+
no code for routes the API serves without documenting them (the Beakr CLI keeps
|
|
461
|
+
its own private client for the few it calls). `test/shipped-surface.test.ts`
|
|
462
|
+
packs the package as it would be published and fails on any route in it outside
|
|
463
|
+
that contract. `CHANGELOG.md` is not in the package for that reason: it repeats
|
|
464
|
+
the API changelog's history, which names routes that are now unlisted.
|
|
465
|
+
|
|
466
|
+
## Installing before the first release
|
|
467
|
+
|
|
468
|
+
Until `@beakr/sdk` is on npm, install a packed build of this repository:
|
|
469
|
+
|
|
470
|
+
```sh
|
|
471
|
+
git clone https://github.com/BeakrHub/beakr-typescript && cd beakr-typescript
|
|
472
|
+
npm ci && npm run build && npm pack # beakr-sdk-<version>.tgz
|
|
473
|
+
npm install /path/to/beakr-sdk-<version>.tgz # in your project
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Release
|
|
477
|
+
|
|
478
|
+
Nothing publishes automatically yet. `.github/workflows/publish.yml` is
|
|
479
|
+
prepared but runs only when a GitHub release is published and the repository
|
|
480
|
+
variable `NPM_PUBLISH_ENABLED` is `true`.
|
|
481
|
+
|
|
482
|
+
To enable it once:
|
|
483
|
+
|
|
484
|
+
1. Create the `@beakr` organization (scope) on npm, if it does not exist yet,
|
|
485
|
+
and publish nothing by hand.
|
|
486
|
+
2. On npmjs.com, add a Trusted Publisher for `@beakr/sdk` (package settings,
|
|
487
|
+
Trusted publishing; for a package that has never been published, follow
|
|
488
|
+
npm's instructions for a first trusted publish): GitHub Actions, owner
|
|
489
|
+
`BeakrHub`, repository `beakr-typescript`, workflow `publish.yml`,
|
|
490
|
+
environment `npm`.
|
|
491
|
+
3. In this repository, create the `npm` environment (Settings, Environments),
|
|
492
|
+
ideally with required reviewers, and set the repository variable
|
|
493
|
+
`NPM_PUBLISH_ENABLED` to `true`.
|
|
494
|
+
|
|
495
|
+
The workflow authenticates with GitHub's OIDC token (no npm token is stored)
|
|
496
|
+
and publishes with provenance.
|
|
497
|
+
|
|
498
|
+
### Versions and release notes come from the API contract
|
|
499
|
+
|
|
500
|
+
`scripts/prepare_release.py` (Python 3, standard library only; identical in
|
|
501
|
+
every Beakr client repository, and tested in beakr-python) compares the
|
|
502
|
+
engine's contract at the last release (`release/released.json`) with the current
|
|
503
|
+
one, using the engine's compatibility rules (`scripts/contract_compat.py`). A
|
|
504
|
+
breaking change needs a major bump (below 1.0, a minor one), an added or changed
|
|
505
|
+
operation a minor bump, a wording change a patch. `package.json`,
|
|
506
|
+
`package-lock.json` and `src/version.ts` are kept in step
|
|
507
|
+
(`release/config.json`). The API changelog entries added since that release
|
|
508
|
+
become the release's section in `CHANGELOG.md`; `### Client changes` is
|
|
509
|
+
hand-written and kept across reapplies.
|
|
510
|
+
|
|
511
|
+
```sh
|
|
512
|
+
python3 scripts/prepare_release.py plan # what changed, which bump it needs
|
|
513
|
+
python3 scripts/prepare_release.py apply # bump versions, write CHANGELOG.md
|
|
514
|
+
python3 scripts/prepare_release.py check # what CI and publishing run
|
|
515
|
+
# After publishing: record the engine commit it was built from, then refresh
|
|
516
|
+
# the snapshot so it carries that commit's contract and changelog
|
|
517
|
+
# (snapshots/beakr-engine/release-base/<sha>), which the offline release check reads.
|
|
518
|
+
python3 scripts/prepare_release.py record --engine-commit <engine sha>
|
|
519
|
+
python3 scripts/refresh_snapshots.py refresh --engine <engine checkout>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## Storing tokens
|
|
523
|
+
|
|
524
|
+
The SDK keeps tokens in memory and hands every rotated pair to `onTokens`; it
|
|
525
|
+
never writes them to disk (it reads the CLI's stored sign-in, above, but never
|
|
526
|
+
writes or refreshes it). If your application persists them and runs more than
|
|
527
|
+
one process with the same login, coordinate refresh yourself: hold a lock while
|
|
528
|
+
refreshing, reload the stored pair under it, and use a pair another process
|
|
529
|
+
already rotated instead of presenting the old refresh token. Beakr revokes a
|
|
530
|
+
whole sign-in when a used refresh token is presented again. Store each login
|
|
531
|
+
with the API origin that issued it and only send it back to that origin.
|