@perstack/api-client 0.0.44 → 0.0.47

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,126 +1,738 @@
1
1
  # @perstack/api-client
2
2
 
3
- The official TypeScript/JavaScript API client for Perstack.
4
-
5
- For API reference, see [Registry API](https://github.com/perstack-ai/perstack/blob/main/docs/references/registry-api.md).
3
+ Official TypeScript API client for the Perstack platform.
6
4
 
7
5
  ## Installation
8
6
 
9
7
  ```bash
10
8
  npm install @perstack/api-client
11
- # or
12
- pnpm add @perstack/api-client
13
- # or
14
- yarn add @perstack/api-client
15
9
  ```
16
10
 
17
- ## Usage
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createApiClient } from "@perstack/api-client"
15
+
16
+ const client = createApiClient({
17
+ apiKey: "your-api-key",
18
+ })
19
+
20
+ // List applications
21
+ const result = await client.applications.list()
22
+ if (result.ok) {
23
+ console.log(result.data.data.applications)
24
+ } else {
25
+ console.error(result.error.message)
26
+ }
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ The `createApiClient` function accepts a configuration object:
32
+
33
+ ```typescript
34
+ interface ApiClientConfig {
35
+ apiKey: string // Required: Your Perstack API key
36
+ baseUrl?: string // Optional: API base URL (default: "https://api.perstack.ai")
37
+ timeout?: number // Optional: Request timeout in milliseconds (default: 30000)
38
+ }
39
+ ```
40
+
41
+ ## API Reference
18
42
 
19
- ### Initialization
43
+ The client provides access to four main API modules:
44
+
45
+ - `client.applications` - Application management
46
+ - `client.env` - Environment configuration (secrets and variables)
47
+ - `client.jobs` - Job execution and monitoring
48
+ - `client.experts` - Expert definitions and versioning
49
+
50
+ ### Result Type
51
+
52
+ All API methods return a discriminated union type for type-safe error handling:
20
53
 
21
54
  ```typescript
22
- import { ApiV1Client } from "@perstack/api-client";
55
+ type ApiResult<T> = { ok: true; data: T } | { ok: false; error: ApiError }
23
56
 
24
- const client = new ApiV1Client({
25
- baseUrl: "https://api.perstack.ai", // Optional, defaults to https://api.perstack.ai
26
- apiKey: "YOUR_API_KEY", // Required for authenticated requests
27
- });
57
+ interface ApiError {
58
+ code: number // HTTP status code (0 for network/validation errors)
59
+ message: string // Error message
60
+ reason?: unknown // Additional error details
61
+ aborted?: boolean // True if request was aborted
62
+ }
28
63
  ```
29
64
 
30
- ### Registry
65
+ ### Request Options
31
66
 
32
- Interact with the Expert Registry.
67
+ All API methods accept an optional `RequestOptions` object:
33
68
 
34
69
  ```typescript
35
- // Get all experts in the registry
36
- const experts = await client.registry.experts.getMany({});
70
+ interface RequestOptions {
71
+ signal?: AbortSignal // AbortController signal for cancellation
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ### Applications API
78
+
79
+ Manage applications within your organization.
37
80
 
38
- // Get a specific expert
39
- const expert = await client.registry.experts.get({
40
- owner: "perstack",
41
- slug: "software-engineer",
42
- });
81
+ #### `client.applications.list(params?, options?)`
43
82
 
44
- // Get expert versions
45
- const versions = await client.registry.experts.getVersions({
46
- owner: "perstack",
47
- slug: "software-engineer",
48
- });
83
+ List all applications with optional filtering and pagination.
84
+
85
+ ```typescript
86
+ const result = await client.applications.list({
87
+ name: "my-app", // Filter by name
88
+ sort: "createdAt", // Sort by: "name" | "createdAt" | "updatedAt"
89
+ order: "desc", // Order: "asc" | "desc"
90
+ take: 10, // Number of results
91
+ skip: 0, // Offset for pagination
92
+ })
49
93
  ```
50
94
 
51
- ### Studio
95
+ #### `client.applications.get(id, options?)`
96
+
97
+ Get a single application by ID.
98
+
99
+ ```typescript
100
+ const result = await client.applications.get("app-id")
101
+ if (result.ok) {
102
+ console.log(result.data.data.application)
103
+ }
104
+ ```
52
105
 
53
- Interact with the Studio (Experts, Jobs, Workspace).
106
+ #### `client.applications.create(input, options?)`
54
107
 
55
- #### Experts
108
+ Create a new application.
56
109
 
57
110
  ```typescript
58
- // Create a studio expert
59
- const newExpert = await client.studio.experts.create({
60
- slug: "my-custom-expert",
61
- description: "A custom expert for my needs",
62
- });
111
+ const result = await client.applications.create({
112
+ name: "My Application",
113
+ applicationGroupId: "group-id", // Optional
114
+ })
115
+ ```
116
+
117
+ #### `client.applications.update(id, input, options?)`
63
118
 
64
- // Get studio experts
65
- const myExperts = await client.studio.experts.getMany({});
119
+ Update an existing application.
120
+
121
+ ```typescript
122
+ const result = await client.applications.update("app-id", {
123
+ name: "Updated Name",
124
+ status: "active", // "active" | "inactive"
125
+ })
66
126
  ```
67
127
 
68
- #### Expert Jobs
128
+ #### `client.applications.delete(id, options?)`
69
129
 
70
- Manage expert execution jobs.
130
+ Delete an application.
71
131
 
72
132
  ```typescript
73
- // Start a job
74
- const job = await client.studio.expertJobs.start({
75
- expertId: "expert-id",
76
- input: {
77
- // ... input data
78
- },
79
- });
80
-
81
- // Get job status
82
- const jobStatus = await client.studio.expertJobs.get({
83
- id: job.id,
84
- });
85
-
86
- // Continue a job (if waiting for input)
87
- await client.studio.expertJobs.continue({
88
- id: job.id,
89
- input: {
90
- // ... user input
91
- },
92
- });
133
+ const result = await client.applications.delete("app-id")
134
+ ```
135
+
136
+ ---
137
+
138
+ ### Environment API
139
+
140
+ Manage secrets and environment variables.
141
+
142
+ #### Secrets
143
+
144
+ ##### `client.env.secrets.list(options?)`
145
+
146
+ List all secrets.
147
+
148
+ ```typescript
149
+ const result = await client.env.secrets.list()
150
+ if (result.ok) {
151
+ for (const secret of result.data.data.secrets) {
152
+ console.log(secret.name) // Secret values are not returned
153
+ }
154
+ }
155
+ ```
156
+
157
+ ##### `client.env.secrets.get(name, options?)`
158
+
159
+ Get a secret by name.
160
+
161
+ ```typescript
162
+ const result = await client.env.secrets.get("API_KEY")
163
+ ```
164
+
165
+ ##### `client.env.secrets.create(input, options?)`
166
+
167
+ Create a new secret.
168
+
169
+ ```typescript
170
+ const result = await client.env.secrets.create({
171
+ name: "API_KEY",
172
+ value: "secret-value",
173
+ })
174
+ ```
175
+
176
+ ##### `client.env.secrets.update(name, input, options?)`
177
+
178
+ Update an existing secret.
179
+
180
+ ```typescript
181
+ const result = await client.env.secrets.update("API_KEY", {
182
+ value: "new-secret-value",
183
+ })
184
+ ```
185
+
186
+ ##### `client.env.secrets.delete(name, options?)`
187
+
188
+ Delete a secret.
189
+
190
+ ```typescript
191
+ const result = await client.env.secrets.delete("API_KEY")
192
+ ```
193
+
194
+ #### Variables
195
+
196
+ ##### `client.env.variables.list(options?)`
197
+
198
+ List all environment variables.
199
+
200
+ ```typescript
201
+ const result = await client.env.variables.list()
202
+ ```
203
+
204
+ ##### `client.env.variables.get(name, options?)`
205
+
206
+ Get a variable by name.
207
+
208
+ ```typescript
209
+ const result = await client.env.variables.get("DATABASE_URL")
210
+ ```
211
+
212
+ ##### `client.env.variables.create(input, options?)`
213
+
214
+ Create a new variable.
215
+
216
+ ```typescript
217
+ const result = await client.env.variables.create({
218
+ name: "DATABASE_URL",
219
+ value: "postgres://...",
220
+ })
221
+ ```
222
+
223
+ ##### `client.env.variables.update(name, input, options?)`
224
+
225
+ Update an existing variable.
226
+
227
+ ```typescript
228
+ const result = await client.env.variables.update("DATABASE_URL", {
229
+ value: "postgres://new-url...",
230
+ })
231
+ ```
232
+
233
+ ##### `client.env.variables.delete(name, options?)`
234
+
235
+ Delete a variable.
236
+
237
+ ```typescript
238
+ const result = await client.env.variables.delete("DATABASE_URL")
239
+ ```
240
+
241
+ ---
242
+
243
+ ### Jobs API
244
+
245
+ Execute and monitor expert jobs.
246
+
247
+ #### `client.jobs.list(params?, options?)`
248
+
249
+ List jobs with optional filtering and pagination.
250
+
251
+ ```typescript
252
+ const result = await client.jobs.list({
253
+ take: 20,
254
+ skip: 0,
255
+ sort: "createdAt",
256
+ order: "desc",
257
+ filter: "status:running",
258
+ })
259
+ ```
260
+
261
+ #### `client.jobs.get(id, options?)`
262
+
263
+ Get a job by ID.
264
+
265
+ ```typescript
266
+ const result = await client.jobs.get("job-id")
267
+ if (result.ok) {
268
+ console.log(result.data.data.job.status)
269
+ }
270
+ ```
271
+
272
+ #### `client.jobs.start(input, options?)`
273
+
274
+ Start a new job.
275
+
276
+ ```typescript
277
+ const result = await client.jobs.start({
278
+ applicationId: "app-id",
279
+ expertKey: "@org/expert@1.0.0",
280
+ query: "Help me with this task",
281
+ files: ["file1.txt", "file2.txt"], // Optional
282
+ provider: "anthropic",
283
+ model: "claude-sonnet-4-20250514", // Optional
284
+ reasoningBudget: "medium", // Optional: "low" | "medium" | "high"
285
+ maxSteps: 50, // Optional
286
+ maxRetries: 3, // Optional
287
+ })
288
+ ```
289
+
290
+ #### `client.jobs.update(id, input, options?)`
291
+
292
+ Update a job's status.
293
+
294
+ ```typescript
295
+ const result = await client.jobs.update("job-id", {
296
+ status: "paused",
297
+ })
298
+ ```
299
+
300
+ #### `client.jobs.continue(id, input, options?)`
301
+
302
+ Continue a paused job with additional input.
303
+
304
+ ```typescript
305
+ const result = await client.jobs.continue("job-id", {
306
+ query: "Continue with this additional context",
307
+ files: ["additional-file.txt"],
308
+ interactiveToolCallResult: true, // Optional: for tool call responses
309
+ provider: "anthropic", // Optional: override provider
310
+ model: "claude-sonnet-4-20250514", // Optional: override model
311
+ maxSteps: 10, // Optional: additional steps limit
312
+ })
313
+ ```
314
+
315
+ #### `client.jobs.cancel(id, options?)`
316
+
317
+ Cancel a running job.
318
+
319
+ ```typescript
320
+ const result = await client.jobs.cancel("job-id")
321
+ ```
322
+
323
+ #### Checkpoints
324
+
325
+ Track job progress through checkpoints.
326
+
327
+ ##### `client.jobs.checkpoints.list(jobId, params?, options?)`
328
+
329
+ List checkpoints for a job.
330
+
331
+ ```typescript
332
+ const result = await client.jobs.checkpoints.list("job-id", {
333
+ take: 50,
334
+ skip: 0,
335
+ sort: "createdAt",
336
+ order: "asc",
337
+ })
338
+ ```
339
+
340
+ ##### `client.jobs.checkpoints.get(jobId, checkpointId, options?)`
341
+
342
+ Get a specific checkpoint.
343
+
344
+ ```typescript
345
+ const result = await client.jobs.checkpoints.get("job-id", "checkpoint-id")
346
+ ```
347
+
348
+ ##### `client.jobs.checkpoints.stream(jobId, options?)`
349
+
350
+ Stream checkpoints in real-time using Server-Sent Events (SSE).
351
+
352
+ ```typescript
353
+ for await (const checkpoint of client.jobs.checkpoints.stream("job-id")) {
354
+ console.log("Activity:", checkpoint.activity)
355
+ }
93
356
  ```
94
357
 
95
358
  #### Workspace
96
359
 
97
- Manage workspace resources (Items, Variables, Secrets).
360
+ Access the job's workspace files.
361
+
362
+ ##### `client.jobs.workspace.get(jobId, options?)`
363
+
364
+ Get workspace metadata.
365
+
366
+ ```typescript
367
+ const result = await client.jobs.workspace.get("job-id")
368
+ if (result.ok) {
369
+ console.log("Branch:", result.data.data.workspace.branch)
370
+ console.log("Stats:", result.data.data.workspace.stats)
371
+ }
372
+ ```
373
+
374
+ ##### `client.jobs.workspace.tree(jobId, params?, options?)`
375
+
376
+ List files in the workspace.
377
+
378
+ ```typescript
379
+ const result = await client.jobs.workspace.tree("job-id", {
380
+ path: "src/", // Optional: subdirectory path
381
+ recursive: true, // Optional: include subdirectories
382
+ })
383
+
384
+ if (result.ok) {
385
+ for (const item of result.data.data.items) {
386
+ console.log(`${item.type}: ${item.path}`)
387
+ }
388
+ }
389
+ ```
390
+
391
+ ##### `client.jobs.workspace.blob(jobId, path, options?)`
392
+
393
+ Download a file from the workspace.
394
+
395
+ ```typescript
396
+ const result = await client.jobs.workspace.blob("job-id", "src/main.ts")
397
+ if (result.ok) {
398
+ const content = await result.data.text()
399
+ console.log(content)
400
+ }
401
+ ```
402
+
403
+ ---
404
+
405
+ ### Experts API
406
+
407
+ Manage expert definitions and versions.
408
+
409
+ #### `client.experts.list(params?, options?)`
410
+
411
+ List available experts.
412
+
413
+ ```typescript
414
+ const result = await client.experts.list({
415
+ filter: "search term",
416
+ category: "coding", // "general" | "coding" | "research" | "writing" | "data" | "automation"
417
+ includeDrafts: false,
418
+ limit: 20,
419
+ offset: 0,
420
+ })
421
+ ```
422
+
423
+ #### `client.experts.get(key, options?)`
424
+
425
+ Get an expert definition by key.
98
426
 
99
427
  ```typescript
100
- // Get workspace details
101
- const workspace = await client.studio.workspace.get();
428
+ // Get latest version
429
+ const result = await client.experts.get("@org/expert")
102
430
 
103
- // Create a workspace item (file)
104
- await client.studio.workspace.items.create({
105
- path: "/path/to/file.txt",
106
- content: "Hello, World!",
107
- });
431
+ // Get specific version
432
+ const result = await client.experts.get("@org/expert@1.0.0")
433
+
434
+ // Get by tag
435
+ const result = await client.experts.get("@org/expert@latest")
436
+ ```
108
437
 
109
- // Create a secret
110
- await client.studio.workspace.secrets.create({
111
- key: "OPENAI_API_KEY",
112
- value: "sk-...",
113
- });
438
+ #### `client.experts.getMeta(key, options?)`
439
+
440
+ Get expert metadata without the full definition.
441
+
442
+ ```typescript
443
+ const result = await client.experts.getMeta("@org/expert@1.0.0")
444
+ if (result.ok) {
445
+ console.log("Scope:", result.data.data.scope)
446
+ console.log("Version:", result.data.data.version)
447
+ }
114
448
  ```
115
449
 
450
+ #### `client.experts.publish(scopeName, options?)`
451
+
452
+ Publish an expert scope (make it publicly visible).
453
+
454
+ ```typescript
455
+ const result = await client.experts.publish("@org/expert")
456
+ ```
457
+
458
+ #### `client.experts.unpublish(scopeName, options?)`
459
+
460
+ Unpublish an expert scope (make it private).
461
+
462
+ ```typescript
463
+ const result = await client.experts.unpublish("@org/expert")
464
+ ```
465
+
466
+ #### `client.experts.yank(key, options?)`
467
+
468
+ Yank (deprecate) a specific version.
469
+
470
+ ```typescript
471
+ const result = await client.experts.yank("@org/expert@1.0.0")
472
+ if (result.ok) {
473
+ console.log("Yanked:", result.data.data.yanked)
474
+ console.log("Latest tag updated:", result.data.data.latestTagUpdated)
475
+ }
476
+ ```
477
+
478
+ #### Drafts
479
+
480
+ Manage expert draft versions before publishing.
481
+
482
+ ##### `client.experts.drafts.list(scopeName, params?, options?)`
483
+
484
+ List drafts for a scope.
485
+
486
+ ```typescript
487
+ const result = await client.experts.drafts.list("@org/expert", {
488
+ limit: 10,
489
+ offset: 0,
490
+ })
491
+ ```
492
+
493
+ ##### `client.experts.drafts.get(scopeName, draftRef, options?)`
494
+
495
+ Get a specific draft.
496
+
497
+ ```typescript
498
+ const result = await client.experts.drafts.get("@org/expert", "draft-ref")
499
+ ```
500
+
501
+ ##### `client.experts.drafts.create(scopeName, input, options?)`
502
+
503
+ Create a new draft.
504
+
505
+ ```typescript
506
+ const result = await client.experts.drafts.create("@org/expert", {
507
+ applicationId: "app-id",
508
+ experts: [
509
+ {
510
+ key: "main",
511
+ name: "Main Expert",
512
+ description: "A helpful assistant",
513
+ instruction: "You are a helpful assistant...",
514
+ skills: {},
515
+ delegates: [],
516
+ },
517
+ ],
518
+ })
519
+ ```
520
+
521
+ ##### `client.experts.drafts.update(scopeName, draftRef, input, options?)`
522
+
523
+ Update an existing draft.
524
+
525
+ ```typescript
526
+ const result = await client.experts.drafts.update("@org/expert", "draft-ref", {
527
+ experts: [
528
+ {
529
+ key: "main",
530
+ name: "Updated Expert",
531
+ instruction: "Updated instructions...",
532
+ },
533
+ ],
534
+ })
535
+ ```
536
+
537
+ ##### `client.experts.drafts.delete(scopeName, draftRef, options?)`
538
+
539
+ Delete a draft.
540
+
541
+ ```typescript
542
+ const result = await client.experts.drafts.delete("@org/expert", "draft-ref")
543
+ ```
544
+
545
+ ##### `client.experts.drafts.assignVersion(scopeName, draftRef, input, options?)`
546
+
547
+ Promote a draft to a versioned release.
548
+
549
+ ```typescript
550
+ const result = await client.experts.drafts.assignVersion("@org/expert", "draft-ref", {
551
+ version: "1.0.0",
552
+ tag: "latest", // Optional: assign a tag
553
+ })
554
+ ```
555
+
556
+ #### Versions
557
+
558
+ List expert versions.
559
+
560
+ ##### `client.experts.versions.list(scopeName, options?)`
561
+
562
+ List all versions for a scope.
563
+
564
+ ```typescript
565
+ const result = await client.experts.versions.list("@org/expert")
566
+ if (result.ok) {
567
+ for (const version of result.data.data.versions) {
568
+ console.log(`${version.version}: ${version.tag || "(no tag)"}`)
569
+ }
570
+ }
571
+ ```
572
+
573
+ ---
574
+
116
575
  ## Error Handling
117
576
 
118
- The client throws errors for failed requests.
577
+ The client uses a result type pattern for predictable error handling:
578
+
579
+ ```typescript
580
+ const result = await client.applications.get("app-id")
581
+
582
+ if (!result.ok) {
583
+ switch (result.error.code) {
584
+ case 401:
585
+ console.error("Authentication failed")
586
+ break
587
+ case 404:
588
+ console.error("Application not found")
589
+ break
590
+ case 0:
591
+ // Network error, timeout, or validation error
592
+ if (result.error.aborted) {
593
+ console.error("Request was cancelled")
594
+ } else {
595
+ console.error("Network error:", result.error.message)
596
+ }
597
+ break
598
+ default:
599
+ console.error(`Error ${result.error.code}: ${result.error.message}`)
600
+ }
601
+ return
602
+ }
603
+
604
+ // TypeScript knows result.data exists here
605
+ console.log(result.data.data.application)
606
+ ```
607
+
608
+ ### Request Cancellation
609
+
610
+ Use `AbortController` to cancel requests:
611
+
612
+ ```typescript
613
+ const controller = new AbortController()
614
+
615
+ // Cancel after 5 seconds
616
+ setTimeout(() => controller.abort(), 5000)
617
+
618
+ const result = await client.jobs.start(
619
+ { applicationId: "app-id", expertKey: "@org/expert", provider: "anthropic" },
620
+ { signal: controller.signal }
621
+ )
622
+
623
+ if (!result.ok && result.error.aborted) {
624
+ console.log("Request was cancelled")
625
+ }
626
+ ```
627
+
628
+ ---
629
+
630
+ ## SSE Streaming
631
+
632
+ For real-time checkpoint streaming, the client provides an async generator:
119
633
 
120
634
  ```typescript
635
+ import { createApiClient } from "@perstack/api-client"
636
+
637
+ const client = createApiClient({ apiKey: "your-api-key" })
638
+
639
+ // Start a job
640
+ const jobResult = await client.jobs.start({
641
+ applicationId: "app-id",
642
+ expertKey: "@org/expert",
643
+ provider: "anthropic",
644
+ })
645
+
646
+ if (!jobResult.ok) {
647
+ throw new Error(jobResult.error.message)
648
+ }
649
+
650
+ const jobId = jobResult.data.data.job.id
651
+
652
+ // Stream checkpoints
121
653
  try {
122
- await client.registry.experts.get({ owner: "invalid", slug: "expert" });
654
+ for await (const checkpoint of client.jobs.checkpoints.stream(jobId)) {
655
+ console.log("Checkpoint:", checkpoint.id)
656
+ console.log("Activity:", checkpoint.activity)
657
+ }
123
658
  } catch (error) {
124
- console.error("Failed to fetch expert:", error);
659
+ console.error("Stream error:", error)
125
660
  }
126
661
  ```
662
+
663
+ ### Advanced SSE Parsing
664
+
665
+ For custom SSE parsing, the package exports utility functions:
666
+
667
+ ```typescript
668
+ import { parseSSE, parseCheckpointSSE, parseSSEWithSchema } from "@perstack/api-client"
669
+ import { z } from "zod"
670
+
671
+ // Generic SSE parser
672
+ const reader = response.body.getReader()
673
+ for await (const event of parseSSE<MyEventType>(reader)) {
674
+ console.log(event)
675
+ }
676
+
677
+ // Checkpoint-specific parser with validation
678
+ for await (const event of parseCheckpointSSE(reader)) {
679
+ if (event.type === "checkpoint") {
680
+ console.log("Checkpoint:", event.data)
681
+ } else if (event.type === "done") {
682
+ console.log("Stream complete:", event.data.status)
683
+ } else if (event.type === "error") {
684
+ console.error("Parse error:", event.data.message)
685
+ }
686
+ }
687
+
688
+ // Custom schema validation
689
+ const mySchema = z.object({ id: z.string(), value: z.number() })
690
+ for await (const event of parseSSEWithSchema(reader, mySchema)) {
691
+ console.log(event.id, event.value)
692
+ }
693
+ ```
694
+
695
+ ---
696
+
697
+ ## TypeScript Support
698
+
699
+ The package is written in TypeScript and exports all types:
700
+
701
+ ```typescript
702
+ import type {
703
+ // Client types
704
+ ApiClient,
705
+ ApiClientConfig,
706
+ ApiResult,
707
+ ApiError,
708
+ RequestOptions,
709
+
710
+ // Application types
711
+ Application,
712
+ ApplicationStatus,
713
+ CreateApplicationInput,
714
+ UpdateApplicationInput,
715
+ ListApplicationsParams,
716
+
717
+ // Job types
718
+ Job,
719
+ JobStatus,
720
+ StartJobInput,
721
+ UpdateJobInput,
722
+ ContinueJobInput,
723
+ Checkpoint,
724
+ CheckpointStatus,
725
+
726
+ // Expert types
727
+ Expert,
728
+ ExpertDefinition,
729
+ ExpertVersion,
730
+ ExpertScope,
731
+ } from "@perstack/api-client"
732
+ ```
733
+
734
+ ---
735
+
736
+ ## License
737
+
738
+ MIT