@seclai/sdk 1.0.7 → 1.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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Seclai JavaScript SDK
2
2
 
3
- This is the official Seclai JavaScript SDK with TypeScript typings.
3
+ The official JavaScript/TypeScript SDK for the [Seclai](https://seclai.com) API. Provides full typed coverage of all API endpoints, file uploads, SSE streaming, polling helpers, and automatic pagination.
4
+
5
+ Works in Node.js 18+, Deno, Bun, Cloudflare Workers, and any runtime with a `fetch` implementation.
4
6
 
5
7
  ## Install
6
8
 
@@ -8,138 +10,508 @@ This is the official Seclai JavaScript SDK with TypeScript typings.
8
10
  npm install @seclai/sdk
9
11
  ```
10
12
 
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { Seclai } from "@seclai/sdk";
17
+
18
+ const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });
19
+
20
+ // List all sources
21
+ const sources = await client.listSources();
22
+
23
+ // Run an agent and stream the result
24
+ const result = await client.runStreamingAgentAndWait(
25
+ "agent_id",
26
+ { input: "Summarize the latest uploads", metadata: {} },
27
+ { timeoutMs: 120_000 },
28
+ );
29
+ console.log(result);
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ | Option | Environment variable | Default |
35
+ | --- | --- | --- |
36
+ | `apiKey` | `SECLAI_API_KEY` | — |
37
+ | `accessToken` | — | — |
38
+ | `profile` | `SECLAI_PROFILE` | `"default"` |
39
+ | `configDir` | `SECLAI_CONFIG_DIR` | `~/.seclai` |
40
+ | `autoRefresh` | — | `true` |
41
+ | `accountId` | — | — |
42
+ | `baseUrl` | `SECLAI_API_URL` | `https://api.seclai.com` |
43
+ | `apiKeyHeader` | — | `x-api-key` |
44
+ | `defaultHeaders` | — | `{}` |
45
+ | `fetch` | — | `globalThis.fetch` |
46
+
47
+ ### Authentication
48
+
49
+ Credentials are resolved via a chain (first match wins):
50
+
51
+ 1. Explicit `apiKey` option
52
+ 2. Explicit `accessToken` option (string or `() => string | Promise<string>`)
53
+ 3. `SECLAI_API_KEY` environment variable
54
+ 4. SSO profile from `~/.seclai/config` with cached tokens in `~/.seclai/sso/cache/`
55
+
56
+ ```ts
57
+ // API key
58
+ const client = new Seclai({ apiKey: "sk-..." });
59
+ ```
60
+
61
+ ```ts
62
+ // Static bearer token
63
+ const client = new Seclai({ accessToken: "eyJhbGciOi..." });
64
+ ```
65
+
66
+ ```ts
67
+ // Dynamic bearer token provider (called per request)
68
+ const client = new Seclai({
69
+ accessToken: async () => fetchTokenFromVault(),
70
+ });
71
+ ```
72
+
73
+ ```ts
74
+ // SSO profile (uses cached tokens, auto-refreshes)
75
+ const client = new Seclai({ profile: "my-profile" });
76
+ ```
77
+
78
+ ```ts
79
+ // Environment variable (no options needed)
80
+ // export SECLAI_API_KEY="sk-..."
81
+ const client = new Seclai();
82
+ ```
83
+
84
+ To set up SSO authentication, install the [Seclai CLI](https://www.npmjs.com/package/seclai) and run:
85
+
86
+ ```bash
87
+ seclai configure sso # set up an SSO profile
88
+ seclai auth login # authenticate via browser
89
+ ```
90
+
11
91
  ## API documentation
12
92
 
13
93
  Online API documentation (latest):
14
94
 
15
- https://seclai.github.io/seclai-javascript/1.0.7/
95
+ https://seclai.github.io/seclai-javascript/1.1.1/
96
+
97
+ ## Resources
16
98
 
17
- ## Usage
99
+ ### Agents
18
100
 
19
101
  ```ts
20
- import { Seclai } from "@seclai/sdk";
102
+ // CRUD
103
+ const agents = await client.listAgents({ page: 1, limit: 20 });
104
+ const agent = await client.createAgent({ name: "My Agent", description: "..." });
105
+ const fetched = await client.getAgent("agent_id");
106
+ const updated = await client.updateAgent("agent_id", { name: "Renamed" });
107
+ await client.deleteAgent("agent_id");
108
+
109
+ // Definition (step workflow)
110
+ const def = await client.getAgentDefinition("agent_id");
111
+ await client.updateAgentDefinition("agent_id", { steps: [...], change_id: def.change_id });
112
+ ```
21
113
 
22
- const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });
114
+ ### Agent runs
23
115
 
24
- const sources = await client.listSources();
25
- console.log(sources.pagination, sources.data);
116
+ ```ts
117
+ // Start a run
118
+ const run = await client.runAgent("agent_id", { input: "Hello" });
119
+
120
+ // List & search runs
121
+ const runs = await client.listAgentRuns("agent_id", { status: "completed" });
122
+ const search = await client.searchAgentRuns({ agent_id: "...", status: ["completed"] });
123
+
124
+ // Fetch run details (optionally with step outputs)
125
+ const detail = await client.getAgentRun("run_id", { includeStepOutputs: true });
126
+
127
+ // Cancel or delete
128
+ await client.cancelAgentRun("run_id");
129
+ await client.deleteAgentRun("run_id");
26
130
  ```
27
131
 
28
- ### Run an agent with SSE streaming (wait for final result)
132
+ ### Streaming
29
133
 
30
- Use the SSE streaming endpoint and block until the final `done` event is received.
134
+ The SDK provides two streaming patterns over the SSE `/runs/stream` endpoint:
31
135
 
32
- If the stream ends before `done` or the timeout is reached, this method throws.
136
+ **Block until done** returns the final `done` payload or throws on timeout:
33
137
 
34
138
  ```ts
35
- import { Seclai } from "@seclai/sdk";
139
+ const result = await client.runStreamingAgentAndWait(
140
+ "agent_id",
141
+ { input: "Hello", metadata: {} },
142
+ { timeoutMs: 60_000 },
143
+ );
144
+ ```
36
145
 
37
- const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });
146
+ **Async iterator** yields every SSE event as `{ event, data }`:
147
+
148
+ ```ts
149
+ for await (const event of client.runStreamingAgent(
150
+ "agent_id",
151
+ { input: "Hello" },
152
+ { timeoutMs: 120_000 },
153
+ )) {
154
+ console.log(event.event, event.data);
155
+ if (event.event === "done") break;
156
+ }
157
+ ```
158
+
159
+ ### Polling
38
160
 
39
- const run = await client.runStreamingAgentAndWait(
161
+ For environments where SSE is not practical, poll for a completed run:
162
+
163
+ ```ts
164
+ const result = await client.runAgentAndPoll(
40
165
  "agent_id",
41
- {
42
- input: "Hello from streaming",
43
- metadata: { app: "My App" },
44
- },
45
- { timeoutMs: 60_000 }
166
+ { input: "Hello" },
167
+ { pollIntervalMs: 2_000, timeoutMs: 120_000 },
46
168
  );
169
+ ```
170
+
171
+ ### Agent input uploads
47
172
 
48
- console.log(run);
173
+ ```ts
174
+ const upload = await client.uploadAgentInput("agent_id", {
175
+ file: new Uint8Array([...]),
176
+ fileName: "input.pdf",
177
+ });
178
+ const status = await client.getAgentInputUploadStatus("agent_id", upload.upload_id);
49
179
  ```
50
180
 
51
- ### Get agent run details
181
+ ### Agent AI assistant
182
+
183
+ ```ts
184
+ const steps = await client.generateAgentSteps("agent_id", { user_input: "Build a RAG pipeline" });
185
+ const config = await client.generateStepConfig("agent_id", { step_type: "llm", user_input: "..." });
186
+
187
+ // Conversation history
188
+ const history = await client.getAgentAiConversationHistory("agent_id");
189
+ await client.markAgentAiSuggestion("agent_id", "conversation_id", { accepted: true });
190
+ ```
52
191
 
53
- Fetch details for a specific agent run, optionally including per-step outputs:
192
+ ### Agent evaluations
54
193
 
55
194
  ```ts
56
- import { Seclai } from "@seclai/sdk";
195
+ const criteria = await client.listEvaluationCriteria("agent_id");
196
+ const created = await client.createEvaluationCriteria("agent_id", { name: "Accuracy", ... });
197
+ const detail = await client.getEvaluationCriteria("criteria_id");
198
+ await client.updateEvaluationCriteria("criteria_id", { ... });
199
+ await client.deleteEvaluationCriteria("criteria_id");
57
200
 
58
- const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });
201
+ // Test a draft
202
+ await client.testDraftEvaluation("agent_id", { criteria: { ... }, run_id: "..." });
59
203
 
60
- // Basic details
61
- const run = await client.getAgentRun("run_id");
62
- console.log(run);
204
+ // Results by criteria
205
+ const results = await client.listEvaluationResults("criteria_id");
206
+ const summary = await client.getEvaluationCriteriaSummary("criteria_id");
207
+ await client.createEvaluationResult("criteria_id", { ... });
63
208
 
64
- // Include per-step outputs with timing, durations, and credits
65
- const runWithSteps = await client.getAgentRun("run_id", {
66
- includeStepOutputs: true,
67
- });
68
- console.log(runWithSteps);
209
+ // Results by run
210
+ const runResults = await client.listRunEvaluationResults("agent_id", "run_id");
211
+
212
+ // Non-manual evaluation summary
213
+ const nonManual = await client.getNonManualEvaluationSummary("agent_id");
214
+
215
+ // Compatible runs for a criteria
216
+ const compatible = await client.listCompatibleRuns("criteria_id");
69
217
  ```
70
218
 
71
- ### Upload a file
219
+ ### Knowledge bases
72
220
 
73
- **Max file size:** 200 MiB.
221
+ ```ts
222
+ const kbs = await client.listKnowledgeBases();
223
+ const kb = await client.createKnowledgeBase({ name: "Docs KB" });
224
+ const fetched = await client.getKnowledgeBase("kb_id");
225
+ await client.updateKnowledgeBase("kb_id", { name: "Renamed KB" });
226
+ await client.deleteKnowledgeBase("kb_id");
227
+ ```
74
228
 
75
- **Supported MIME types:**
76
- - `application/epub+zip`
77
- - `application/json`
78
- - `application/msword`
79
- - `application/pdf`
80
- - `application/vnd.ms-excel`
81
- - `application/vnd.ms-outlook`
82
- - `application/vnd.ms-powerpoint`
83
- - `application/vnd.openxmlformats-officedocument.presentationml.presentation`
84
- - `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
85
- - `application/vnd.openxmlformats-officedocument.wordprocessingml.document`
86
- - `application/xml`
87
- - `application/zip`
88
- - `audio/flac`, `audio/mp4`, `audio/mpeg`, `audio/ogg`, `audio/wav`
89
- - `image/bmp`, `image/gif`, `image/jpeg`, `image/png`, `image/tiff`, `image/webp`
90
- - `text/csv`, `text/html`, `text/markdown`, `text/x-markdown`, `text/plain`, `text/xml`
91
- - `video/mp4`, `video/quicktime`, `video/x-msvideo`
229
+ ### Memory banks
92
230
 
93
- If the upload is sent as `application/octet-stream`, the server attempts to infer the type from the file extension, so pass `fileName` with a meaningful extension.
231
+ ```ts
232
+ const banks = await client.listMemoryBanks();
233
+ const bank = await client.createMemoryBank({ name: "Chat Memory", type: "conversation" });
234
+ const fetched = await client.getMemoryBank("mb_id");
235
+ await client.updateMemoryBank("mb_id", { name: "Renamed" });
236
+ await client.deleteMemoryBank("mb_id");
237
+
238
+ // Stats & compaction
239
+ const stats = await client.getMemoryBankStats("mb_id");
240
+ await client.compactMemoryBank("mb_id");
241
+
242
+ // Test compaction
243
+ const test = await client.testMemoryBankCompaction("mb_id", { ... });
244
+ const standalone = await client.testCompactionPromptStandalone({ ... });
245
+
246
+ // Templates & agents
247
+ const templates = await client.listMemoryBankTemplates();
248
+ const agents = await client.getAgentsUsingMemoryBank("mb_id");
249
+
250
+ // AI assistant
251
+ const suggestion = await client.generateMemoryBankConfig({ user_input: "..." });
252
+ const history = await client.getMemoryBankAiLastConversation();
253
+ await client.acceptMemoryBankAiSuggestion("conv_id", { ... });
254
+
255
+ // Source management
256
+ await client.deleteMemoryBankSource("mb_id");
257
+ ```
258
+
259
+ ### Sources
94
260
 
95
261
  ```ts
96
- import { Seclai } from "@seclai/sdk";
262
+ const sources = await client.listSources({ page: 1, limit: 20, order: "asc" });
263
+ const source = await client.createSource({ name: "My Source", ... });
264
+ const fetched = await client.getSource("source_id");
265
+ await client.updateSource("source_id", { name: "Renamed" });
266
+ await client.deleteSource("source_id");
267
+ ```
97
268
 
98
- const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });
269
+ ### File uploads
270
+
271
+ Upload a file to a source (max 200 MiB). The SDK infers MIME type from the file extension when `mimeType` is not provided.
272
+
273
+ ```ts
274
+ import { readFile } from "node:fs/promises";
99
275
 
100
- const bytes = new TextEncoder().encode("hello");
101
- const upload = await client.uploadFileToSource("source_connection_id", {
102
- file: bytes,
103
- fileName: "hello.txt",
104
- mimeType: "text/plain",
105
- title: "Hello",
106
- metadata: { category: "docs", author: "Ada" },
276
+ await client.uploadFileToSource("source_id", {
277
+ file: await readFile("document.pdf"),
278
+ fileName: "document.pdf",
279
+ title: "Q4 Report",
280
+ metadata: { department: "finance" },
107
281
  });
108
- console.log(upload);
109
282
  ```
110
283
 
111
- ### Replace an existing content version with a new upload
112
-
113
- If you need to correct or update an uploaded document while keeping references stable,
114
- use `uploadFileToContent` to upload a new file and replace the content behind an existing
115
- `source_connection_content_version`.
284
+ Upload inline text:
116
285
 
117
286
  ```ts
118
- import { Seclai } from "@seclai/sdk";
287
+ await client.uploadInlineTextToSource("source_id", {
288
+ text: "Hello, world!",
289
+ title: "Greeting",
290
+ });
291
+ ```
119
292
 
120
- const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });
293
+ Replace a content version with a new file:
121
294
 
122
- const replace = await client.uploadFileToContent("content_version_id", {
123
- file: await fetch("https://example.invalid/updated.pdf").then((r) => r.arrayBuffer()),
295
+ ```ts
296
+ await client.uploadFileToContent("content_version_id", {
297
+ file: await readFile("updated.pdf"),
124
298
  fileName: "updated.pdf",
125
299
  mimeType: "application/pdf",
126
- title: "Updated document",
127
- metadata: { revision: 2 },
128
300
  });
301
+ ```
302
+
303
+ ### Source exports
129
304
 
130
- console.log(replace);
305
+ ```ts
306
+ const exports = await client.listSourceExports("source_id");
307
+ const exp = await client.createSourceExport("source_id", { format: "json" });
308
+ const status = await client.getSourceExport("source_id", exp.id);
309
+ const estimate = await client.estimateSourceExport("source_id", {});
310
+ const response = await client.downloadSourceExport("source_id", exp.id);
311
+ await client.deleteSourceExport("source_id", exp.id);
312
+ await client.cancelSourceExport("source_id", exp.id);
131
313
  ```
132
314
 
133
- ## Development
315
+ ### Source embedding migrations
134
316
 
135
- ### Base URL
317
+ ```ts
318
+ const migration = await client.getSourceEmbeddingMigration("source_id");
319
+ await client.startSourceEmbeddingMigration("source_id", { target_model: "..." });
320
+ await client.cancelSourceEmbeddingMigration("source_id");
321
+ ```
136
322
 
137
- Set `SECLAI_API_URL` to point at a different API host (e.g., staging):
323
+ ### Content
138
324
 
139
- ```bash
140
- export SECLAI_API_URL="https://example.invalid"
325
+ ```ts
326
+ const detail = await client.getContentDetail("content_id", { start: 0, end: 1000 });
327
+ const embeddings = await client.listContentEmbeddings("content_id");
328
+ await client.deleteContent("content_id");
329
+
330
+ // Replace content with inline text
331
+ await client.replaceContentWithInlineText("content_id", { text: "Updated text", title: "Updated" });
332
+
333
+ // Upload a replacement file
334
+ await client.uploadFileToContent("content_id", {
335
+ file: await readFile("updated.pdf"),
336
+ fileName: "updated.pdf",
337
+ });
338
+ ```
339
+
340
+ ### Solutions
341
+
342
+ ```ts
343
+ const solutions = await client.listSolutions();
344
+ const sol = await client.createSolution({ name: "My Solution" });
345
+ const fetched = await client.getSolution("solution_id");
346
+ await client.updateSolution("solution_id", { name: "Renamed" });
347
+ await client.deleteSolution("solution_id");
348
+
349
+ // Link / unlink resources
350
+ await client.linkAgentsToSolution("solution_id", { ids: ["agent_id"] });
351
+ await client.unlinkAgentsFromSolution("solution_id", { ids: ["agent_id"] });
352
+ await client.linkKnowledgeBasesToSolution("solution_id", { ids: ["kb_id"] });
353
+ await client.unlinkKnowledgeBasesFromSolution("solution_id", { ids: ["kb_id"] });
354
+ await client.linkSourceConnectionsToSolution("solution_id", { ids: ["source_id"] });
355
+ await client.unlinkSourceConnectionsFromSolution("solution_id", { ids: ["source_id"] });
356
+
357
+ // AI assistant
358
+ const plan = await client.generateSolutionAiPlan("solution_id", { user_input: "Set up a RAG pipeline" });
359
+ await client.acceptSolutionAiPlan("solution_id", "conversation_id", {});
360
+ await client.declineSolutionAiPlan("solution_id", "conversation_id");
361
+
362
+ // AI-generated knowledge base / source within the solution
363
+ await client.generateSolutionAiKnowledgeBase("solution_id", { user_input: "..." });
364
+ await client.generateSolutionAiSource("solution_id", { user_input: "..." });
365
+
366
+ // Conversations
367
+ const convs = await client.listSolutionConversations("solution_id");
368
+ await client.addSolutionConversationTurn("solution_id", { user_input: "..." });
369
+ await client.markSolutionConversationTurn("solution_id", "conversation_id", { ... });
370
+ ```
371
+
372
+ ### Governance AI
373
+
374
+ ```ts
375
+ const plan = await client.generateGovernanceAiPlan({ user_input: "Add a toxicity policy" });
376
+ const convs = await client.listGovernanceAiConversations();
377
+ await client.acceptGovernanceAiPlan("conversation_id");
378
+ await client.declineGovernanceAiPlan("conversation_id");
379
+ ```
380
+
381
+ ### Alerts
382
+
383
+ ```ts
384
+ const alerts = await client.listAlerts({ status: "active" });
385
+ const alert = await client.getAlert("alert_id");
386
+ await client.changeAlertStatus("alert_id", { status: "resolved" });
387
+ await client.addAlertComment("alert_id", { text: "Investigating" });
388
+
389
+ // Subscriptions
390
+ await client.subscribeToAlert("alert_id");
391
+ await client.unsubscribeFromAlert("alert_id");
392
+
393
+ // Alert configs
394
+ const configs = await client.listAlertConfigs();
395
+ await client.createAlertConfig({ ... });
396
+ await client.getAlertConfig("config_id");
397
+ await client.updateAlertConfig("config_id", { ... });
398
+ await client.deleteAlertConfig("config_id");
399
+
400
+ // Organization preferences
401
+ const prefs = await client.listOrganizationAlertPreferences();
402
+ await client.updateOrganizationAlertPreference("org_id", "alert_type", { ... });
403
+ ```
404
+
405
+ ### Models
406
+
407
+ ```ts
408
+ const alerts = await client.listModelAlerts();
409
+ await client.markModelAlertRead("alert_id");
410
+ await client.markAllModelAlertsRead();
411
+ const unread = await client.getUnreadModelAlertCount();
412
+ const recs = await client.getModelRecommendations("model_id");
141
413
  ```
142
414
 
415
+ ### Search
416
+
417
+ ```ts
418
+ const results = await client.search({ query: "quarterly report" });
419
+ const filtered = await client.search({ query: "my agent", entityType: "agent", limit: 5 });
420
+ ```
421
+
422
+ ### Top-level AI assistant
423
+
424
+ ```ts
425
+ // Generate plans for different resource types
426
+ const kb = await client.aiAssistantKnowledgeBase({ user_input: "Create a docs KB" });
427
+ const src = await client.aiAssistantSource({ user_input: "Add a web source" });
428
+ const sol = await client.aiAssistantSolution({ user_input: "Set up monitoring" });
429
+ const mb = await client.aiAssistantMemoryBank({ user_input: "Create a chat memory" });
430
+
431
+ // Accept or decline the generated plan
432
+ await client.acceptAiAssistantPlan("conversation_id", { confirm_deletions: true });
433
+ await client.declineAiAssistantPlan("conversation_id");
434
+
435
+ // Memory bank conversation history
436
+ const history = await client.getAiAssistantMemoryBankHistory();
437
+ await client.acceptAiMemoryBankSuggestion("conversation_id", { ... });
438
+
439
+ // Feedback
440
+ await client.submitAiFeedback({ ... });
441
+ ```
442
+
443
+ ## Pagination helper
444
+
445
+ Automatically iterate through all pages:
446
+
447
+ ```ts
448
+ for await (const source of client.paginate(
449
+ (opts) => client.listSources(opts),
450
+ { limit: 50 },
451
+ )) {
452
+ console.log(source.name);
453
+ }
454
+ ```
455
+
456
+ ## Error handling
457
+
458
+ All errors extend `SeclaiError`:
459
+
460
+ ```ts
461
+ import {
462
+ SeclaiError,
463
+ SeclaiConfigurationError,
464
+ SeclaiAPIStatusError,
465
+ SeclaiAPIValidationError,
466
+ SeclaiStreamingError,
467
+ } from "@seclai/sdk";
468
+
469
+ try {
470
+ await client.getAgent("bad_id");
471
+ } catch (err) {
472
+ if (err instanceof SeclaiAPIValidationError) {
473
+ console.error("Validation:", err.validationError);
474
+ } else if (err instanceof SeclaiAPIStatusError) {
475
+ console.error(`HTTP ${err.statusCode}:`, err.responseText);
476
+ } else if (err instanceof SeclaiStreamingError) {
477
+ console.error("Stream failed for run:", err.runId);
478
+ }
479
+ }
480
+ ```
481
+
482
+ ## Cancellation (AbortSignal)
483
+
484
+ All low-level methods support an `AbortSignal` for request cancellation:
485
+
486
+ ```ts
487
+ const controller = new AbortController();
488
+
489
+ // Cancel after 5 seconds
490
+ setTimeout(() => controller.abort(), 5_000);
491
+
492
+ const data = await client.request("GET", "/agents", {
493
+ signal: controller.signal,
494
+ });
495
+ ```
496
+
497
+ ## Low-level access
498
+
499
+ For endpoints not yet covered by a convenience method, use `request` or `requestRaw`:
500
+
501
+ ```ts
502
+ // JSON request/response
503
+ const data = await client.request("POST", "/custom/endpoint", {
504
+ json: { key: "value" },
505
+ query: { filter: "active" },
506
+ });
507
+
508
+ // Raw Response (e.g. binary downloads)
509
+ const response = await client.requestRaw("GET", "/files/download/123");
510
+ const blob = await response.blob();
511
+ ```
512
+
513
+ ## Development
514
+
143
515
  ### Install dependencies
144
516
 
145
517
  ```bash
@@ -160,6 +532,12 @@ npm run build
160
532
 
161
533
  This also regenerates `src/openapi.ts` from `openapi/seclai.openapi.json`.
162
534
 
535
+ ### Run tests
536
+
537
+ ```bash
538
+ npm test
539
+ ```
540
+
163
541
  ### Generate docs
164
542
 
165
543
  Generate HTML docs into `build/docs/`: