@beakr/sdk 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,531 @@
1
- # Temporary Holding Version
1
+ # @beakr/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.