@layla-network/sdk 0.1.0 → 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/dist/index.cjs +297 -59
- package/dist/index.d.cts +307 -77
- package/dist/index.d.ts +307 -77
- package/dist/index.js +295 -60
- package/package.json +43 -32
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/errors.ts
|
|
2
2
|
var LaylaError = class extends Error {
|
|
3
3
|
constructor(message) {
|
|
4
4
|
super(message);
|
|
@@ -19,14 +19,8 @@ var LaylaBridgeUnavailableError = class extends LaylaError {
|
|
|
19
19
|
this.name = "LaylaBridgeUnavailableError";
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.promise = new Promise((res, rej) => {
|
|
25
|
-
this.resolve = res;
|
|
26
|
-
this.reject = rej;
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
};
|
|
22
|
+
|
|
23
|
+
// src/internal/bridge.ts
|
|
30
24
|
var _LaylaBridge = class _LaylaBridge {
|
|
31
25
|
constructor() {
|
|
32
26
|
this.queue = [];
|
|
@@ -44,27 +38,13 @@ var _LaylaBridge = class _LaylaBridge {
|
|
|
44
38
|
if (!parsed || typeof parsed.event !== "string") return;
|
|
45
39
|
const job = this.active;
|
|
46
40
|
if (!job) return;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
case "on_message_end": {
|
|
55
|
-
sink.handleEnd();
|
|
56
|
-
this.finishActive();
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
case "on_error": {
|
|
60
|
-
const data = parsed.data;
|
|
61
|
-
sink.handleError(new LaylaError(data?.message || "Layla model error"));
|
|
62
|
-
this.finishActive();
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
default:
|
|
66
|
-
break;
|
|
41
|
+
if (parsed.event === "on_error") {
|
|
42
|
+
const data = parsed.data;
|
|
43
|
+
job.sink.fail(new LaylaError(data?.message || "Layla model error"));
|
|
44
|
+
this.finishActive();
|
|
45
|
+
return;
|
|
67
46
|
}
|
|
47
|
+
if (job.sink.accept(parsed)) this.finishActive();
|
|
68
48
|
};
|
|
69
49
|
}
|
|
70
50
|
static shared() {
|
|
@@ -76,14 +56,33 @@ var _LaylaBridge = class _LaylaBridge {
|
|
|
76
56
|
window.addEventListener("message", this.onWindowMessage);
|
|
77
57
|
this.listening = true;
|
|
78
58
|
}
|
|
79
|
-
enqueue(
|
|
59
|
+
enqueue(job) {
|
|
80
60
|
this.ensureListening();
|
|
81
|
-
this.queue.push(
|
|
61
|
+
this.queue.push(job);
|
|
82
62
|
this.pump();
|
|
83
63
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Cancel a request.
|
|
66
|
+
*
|
|
67
|
+
* - If still queued (not yet sent), drop it; the host never sees it.
|
|
68
|
+
* - If it is the active (in-flight) request, ask the sink for a stop message
|
|
69
|
+
* and post it (streaming generation -> `{ cmd: 'cancel' }`; one-shot -> none).
|
|
70
|
+
* We deliberately do NOT free the active slot here: it frees when the host
|
|
71
|
+
* sends the request's terminating event (`on_message_end` /
|
|
72
|
+
* `on_get_characters_response` / `on_error`). Holding the slot until then
|
|
73
|
+
* keeps any trailing event attributed to the (already-closed) sink — which
|
|
74
|
+
* swallows it — instead of leaking into the next request.
|
|
75
|
+
*/
|
|
76
|
+
cancel(sink) {
|
|
77
|
+
const remaining = this.queue.filter((job) => job.sink !== sink);
|
|
78
|
+
if (remaining.length !== this.queue.length) {
|
|
79
|
+
this.queue = remaining;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.active && this.active.sink === sink) {
|
|
83
|
+
const stop = sink.cancelMessage?.();
|
|
84
|
+
if (stop) this.post(stop);
|
|
85
|
+
}
|
|
87
86
|
}
|
|
88
87
|
finishActive() {
|
|
89
88
|
this.active = null;
|
|
@@ -101,17 +100,33 @@ var _LaylaBridge = class _LaylaBridge {
|
|
|
101
100
|
this.send(next);
|
|
102
101
|
}
|
|
103
102
|
send(job) {
|
|
104
|
-
if (
|
|
103
|
+
if (!this.post(job.message)) {
|
|
105
104
|
this.active = null;
|
|
106
|
-
job.sink.
|
|
105
|
+
job.sink.fail(new LaylaBridgeUnavailableError());
|
|
107
106
|
this.pump();
|
|
108
|
-
return;
|
|
109
107
|
}
|
|
110
|
-
|
|
108
|
+
}
|
|
109
|
+
/** Post a message to the host. Returns false if the bridge isn't present. */
|
|
110
|
+
post(message) {
|
|
111
|
+
if (typeof window === "undefined" || !window.ReactNativeWebView) return false;
|
|
112
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
113
|
+
return true;
|
|
111
114
|
}
|
|
112
115
|
};
|
|
113
116
|
_LaylaBridge.instance = null;
|
|
114
117
|
var LaylaBridge = _LaylaBridge;
|
|
118
|
+
|
|
119
|
+
// src/internal/deferred.ts
|
|
120
|
+
var Deferred = class {
|
|
121
|
+
constructor() {
|
|
122
|
+
this.promise = new Promise((res, rej) => {
|
|
123
|
+
this.resolve = res;
|
|
124
|
+
this.reject = rej;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/resources/chat/stream.ts
|
|
115
130
|
var ChatCompletionStream = class {
|
|
116
131
|
constructor(model) {
|
|
117
132
|
this.id = `chatcmpl-layla-${Date.now()}-${Math.floor(
|
|
@@ -150,7 +165,43 @@ var ChatCompletionStream = class {
|
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
167
|
}
|
|
153
|
-
/* ----
|
|
168
|
+
/* ---- BridgeSink: driven by the bridge --------------------------------- */
|
|
169
|
+
accept(event) {
|
|
170
|
+
switch (event.event) {
|
|
171
|
+
case "on_message": {
|
|
172
|
+
const data = event.data ?? { msg: "", delta: "" };
|
|
173
|
+
this.handleDelta(data.delta ?? "", data.msg ?? "");
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
case "on_message_end":
|
|
177
|
+
this.handleEnd();
|
|
178
|
+
return true;
|
|
179
|
+
// terminal
|
|
180
|
+
default:
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
fail(err) {
|
|
185
|
+
if (this.closed) return;
|
|
186
|
+
this.failure = err;
|
|
187
|
+
this.closed = true;
|
|
188
|
+
this.drainError(err);
|
|
189
|
+
this.emit("error", err);
|
|
190
|
+
this.finalDeferred.reject(err);
|
|
191
|
+
}
|
|
192
|
+
isClosed() {
|
|
193
|
+
return this.closed;
|
|
194
|
+
}
|
|
195
|
+
cancelMessage() {
|
|
196
|
+
return { cmd: "cancel" };
|
|
197
|
+
}
|
|
198
|
+
/** Abort from the consumer side. */
|
|
199
|
+
abort(reason) {
|
|
200
|
+
if (this.closed) return;
|
|
201
|
+
const err = reason instanceof Error ? reason : new LaylaAbortError();
|
|
202
|
+
this.fail(err);
|
|
203
|
+
LaylaBridge.shared().cancel(this);
|
|
204
|
+
}
|
|
154
205
|
handleDelta(delta, snapshot) {
|
|
155
206
|
if (this.closed) return;
|
|
156
207
|
this.snapshot = snapshot || this.snapshot + delta;
|
|
@@ -171,24 +222,6 @@ var ChatCompletionStream = class {
|
|
|
171
222
|
this.emit("end");
|
|
172
223
|
this.finalDeferred.resolve(completion);
|
|
173
224
|
}
|
|
174
|
-
handleError(err) {
|
|
175
|
-
if (this.closed) return;
|
|
176
|
-
this.failure = err;
|
|
177
|
-
this.closed = true;
|
|
178
|
-
this.drainError(err);
|
|
179
|
-
this.emit("error", err);
|
|
180
|
-
this.finalDeferred.reject(err);
|
|
181
|
-
}
|
|
182
|
-
isClosed() {
|
|
183
|
-
return this.closed;
|
|
184
|
-
}
|
|
185
|
-
/** Abort from the consumer side. */
|
|
186
|
-
abort(reason) {
|
|
187
|
-
if (this.closed) return;
|
|
188
|
-
const err = reason instanceof Error ? reason : new LaylaAbortError();
|
|
189
|
-
LaylaBridge.shared().cancelQueued(this);
|
|
190
|
-
this.handleError(err);
|
|
191
|
-
}
|
|
192
225
|
/* ---- async iteration -------------------------------------------------- */
|
|
193
226
|
next() {
|
|
194
227
|
if (this.buffer.length) {
|
|
@@ -265,6 +298,8 @@ var ChatCompletionStream = class {
|
|
|
265
298
|
};
|
|
266
299
|
}
|
|
267
300
|
};
|
|
301
|
+
|
|
302
|
+
// src/resources/chat/index.ts
|
|
268
303
|
var Completions = class {
|
|
269
304
|
create(body) {
|
|
270
305
|
const stream = this.startStream(
|
|
@@ -295,7 +330,10 @@ var Completions = class {
|
|
|
295
330
|
{ once: true }
|
|
296
331
|
);
|
|
297
332
|
}
|
|
298
|
-
LaylaBridge.shared().enqueue({
|
|
333
|
+
LaylaBridge.shared().enqueue({
|
|
334
|
+
message: { cmd: "send_message", data: messages },
|
|
335
|
+
sink: stream
|
|
336
|
+
});
|
|
299
337
|
return stream;
|
|
300
338
|
}
|
|
301
339
|
};
|
|
@@ -304,19 +342,216 @@ var Chat = class {
|
|
|
304
342
|
this.completions = new Completions();
|
|
305
343
|
}
|
|
306
344
|
};
|
|
345
|
+
|
|
346
|
+
// src/resources/images.ts
|
|
347
|
+
var ImagesBridgeSink = class {
|
|
348
|
+
constructor() {
|
|
349
|
+
this.listeners = {};
|
|
350
|
+
this.closed = false;
|
|
351
|
+
}
|
|
352
|
+
on(event, listener) {
|
|
353
|
+
var _a;
|
|
354
|
+
((_a = this.listeners)[event] || (_a[event] = [])).push(listener);
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
off(event, listener) {
|
|
358
|
+
const ls = this.listeners[event];
|
|
359
|
+
if (ls) this.listeners[event] = ls.filter((l) => l !== listener);
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
accept(event) {
|
|
363
|
+
switch (event.event) {
|
|
364
|
+
case "on_generate_image_progress":
|
|
365
|
+
for (const l of this.listeners["on_generate_image_progress"] ?? []) {
|
|
366
|
+
try {
|
|
367
|
+
l(event.data.status, event.data.steps, event.data.total_steps);
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
// not terminal
|
|
373
|
+
case "on_generate_image_response":
|
|
374
|
+
for (const l of this.listeners["on_generate_image_response"] ?? []) {
|
|
375
|
+
try {
|
|
376
|
+
l(event.data?.image_data_base64 || null);
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
381
|
+
// terminal
|
|
382
|
+
default:
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
fail(_) {
|
|
387
|
+
if (this.closed) return;
|
|
388
|
+
this.closed = true;
|
|
389
|
+
}
|
|
390
|
+
isClosed() {
|
|
391
|
+
return this.closed;
|
|
392
|
+
}
|
|
393
|
+
cancelMessage() {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
/** Abort from the consumer side. */
|
|
397
|
+
abort(reason) {
|
|
398
|
+
const err = reason instanceof Error ? reason : new LaylaAbortError();
|
|
399
|
+
this.fail(err);
|
|
400
|
+
LaylaBridge.shared().cancel(this);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
var Images = class {
|
|
404
|
+
/**
|
|
405
|
+
* Ask the native host to generate an image. Resolves to a ready-to-use base64 image src string, or null if the character has no image
|
|
406
|
+
*/
|
|
407
|
+
generateImage(prompt, onProgress, options = {}) {
|
|
408
|
+
const setupSince = () => {
|
|
409
|
+
const sink = new ImagesBridgeSink();
|
|
410
|
+
if (options.signal?.aborted) {
|
|
411
|
+
queueMicrotask(() => sink.abort(new LaylaAbortError()));
|
|
412
|
+
return sink;
|
|
413
|
+
}
|
|
414
|
+
if (options.signal) {
|
|
415
|
+
options.signal.addEventListener(
|
|
416
|
+
"abort",
|
|
417
|
+
() => sink.abort(new LaylaAbortError()),
|
|
418
|
+
{ once: true }
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
LaylaBridge.shared().enqueue({
|
|
422
|
+
message: { cmd: "generate_image", data: { prompt } },
|
|
423
|
+
sink
|
|
424
|
+
});
|
|
425
|
+
return sink;
|
|
426
|
+
};
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
const sink = setupSince();
|
|
429
|
+
sink.on("on_generate_image_response", (image_data_base64) => {
|
|
430
|
+
resolve(image_data_base64);
|
|
431
|
+
});
|
|
432
|
+
sink.on("on_generate_image_progress", (status, step, total_step) => {
|
|
433
|
+
onProgress(status, step, total_step);
|
|
434
|
+
});
|
|
435
|
+
const onFailure = (err) => {
|
|
436
|
+
reject(err);
|
|
437
|
+
};
|
|
438
|
+
options.signal?.addEventListener("abort", () => {
|
|
439
|
+
onFailure(new LaylaAbortError());
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/internal/one-shot.ts
|
|
446
|
+
var OneShotRequest = class {
|
|
447
|
+
constructor(command, responseEvent, extract) {
|
|
448
|
+
this.responseEvent = responseEvent;
|
|
449
|
+
this.extract = extract;
|
|
450
|
+
this.closed = false;
|
|
451
|
+
this.deferred = new Deferred();
|
|
452
|
+
this.deferred.promise.catch(() => void 0);
|
|
453
|
+
this.job = { message: command, sink: this };
|
|
454
|
+
}
|
|
455
|
+
get promise() {
|
|
456
|
+
return this.deferred.promise;
|
|
457
|
+
}
|
|
458
|
+
accept(event) {
|
|
459
|
+
if (event.event !== this.responseEvent) return false;
|
|
460
|
+
if (!this.closed) {
|
|
461
|
+
this.closed = true;
|
|
462
|
+
try {
|
|
463
|
+
this.deferred.resolve(this.extract(event));
|
|
464
|
+
} catch (err) {
|
|
465
|
+
this.deferred.reject(
|
|
466
|
+
err instanceof Error ? err : new LaylaError(String(err))
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
fail(err) {
|
|
473
|
+
if (this.closed) return;
|
|
474
|
+
this.closed = true;
|
|
475
|
+
this.deferred.reject(err);
|
|
476
|
+
}
|
|
477
|
+
isClosed() {
|
|
478
|
+
return this.closed;
|
|
479
|
+
}
|
|
480
|
+
// No cancelMessage(): a one-shot has no generation to stop on the host side.
|
|
481
|
+
/** Abort from the consumer side. */
|
|
482
|
+
abort(reason) {
|
|
483
|
+
if (this.closed) return;
|
|
484
|
+
const err = reason instanceof Error ? reason : new LaylaAbortError();
|
|
485
|
+
this.fail(err);
|
|
486
|
+
LaylaBridge.shared().cancel(this);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
function oneShot(command, responseEvent, extract, signal) {
|
|
490
|
+
const req = new OneShotRequest(command, responseEvent, extract);
|
|
491
|
+
if (signal?.aborted) {
|
|
492
|
+
queueMicrotask(() => req.abort(new LaylaAbortError()));
|
|
493
|
+
return req.promise;
|
|
494
|
+
}
|
|
495
|
+
if (signal) {
|
|
496
|
+
signal.addEventListener("abort", () => req.abort(new LaylaAbortError()), {
|
|
497
|
+
once: true
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
LaylaBridge.shared().enqueue(req.job);
|
|
501
|
+
return req.promise;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/resources/characters.ts
|
|
505
|
+
var Characters = class {
|
|
506
|
+
/**
|
|
507
|
+
* Ask the native host for the available character cards. Resolves once with
|
|
508
|
+
* the host's `on_get_characters_response` payload, or rejects on error/abort.
|
|
509
|
+
*/
|
|
510
|
+
list(options = {}) {
|
|
511
|
+
return oneShot(
|
|
512
|
+
{ cmd: "get_characters" },
|
|
513
|
+
"on_get_characters_response",
|
|
514
|
+
(event) => {
|
|
515
|
+
const data = event.data;
|
|
516
|
+
return Array.isArray(data) ? data : [];
|
|
517
|
+
},
|
|
518
|
+
options.signal
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Ask the native host for a character portrait. Resolves to a ready-to-use
|
|
523
|
+
* image src string, or null when the character has no image.
|
|
524
|
+
*/
|
|
525
|
+
getImage(characterId, options = {}) {
|
|
526
|
+
return oneShot(
|
|
527
|
+
{ cmd: "get_character_image", data: { character_id: characterId } },
|
|
528
|
+
"on_get_character_image_response",
|
|
529
|
+
(event) => {
|
|
530
|
+
const data = event.data;
|
|
531
|
+
return data?.image_data_base64 ?? null;
|
|
532
|
+
},
|
|
533
|
+
options.signal
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/client.ts
|
|
307
539
|
var LaylaSDK = class {
|
|
308
540
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
309
541
|
constructor(_options = {}) {
|
|
310
542
|
this.chat = new Chat();
|
|
543
|
+
this.characters = new Characters();
|
|
544
|
+
this.images = new Images();
|
|
311
545
|
}
|
|
312
546
|
};
|
|
313
|
-
var
|
|
547
|
+
var client_default = LaylaSDK;
|
|
314
548
|
export {
|
|
315
549
|
ChatCompletionStream,
|
|
550
|
+
Images,
|
|
316
551
|
LaylaSDK as Layla,
|
|
317
552
|
LaylaAbortError,
|
|
318
553
|
LaylaBridgeUnavailableError,
|
|
319
554
|
LaylaError,
|
|
320
555
|
LaylaSDK,
|
|
321
|
-
|
|
556
|
+
client_default as default
|
|
322
557
|
};
|
package/package.json
CHANGED
|
@@ -1,32 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@layla-network/sdk",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Layla custom mini-apps SDK",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.cjs",
|
|
7
|
-
"module": "./dist/index.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js",
|
|
13
|
-
"require": "./dist/index.cjs"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist"
|
|
18
|
-
],
|
|
19
|
-
"sideEffects": false,
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
22
|
-
"prepublishOnly": "npm run build"
|
|
23
|
-
},
|
|
24
|
-
"license": "MIT",
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"tsup": "^8.0.0",
|
|
27
|
-
"typescript": "^5.4.0"
|
|
28
|
-
},
|
|
29
|
-
"publishConfig": {
|
|
30
|
-
"access": "public"
|
|
31
|
-
}
|
|
32
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@layla-network/sdk",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Layla custom mini-apps SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.4.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"vite-plugin-singlefile": "^2.3.3"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/l3utterfly/layla-sdk.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/l3utterfly/layla-sdk#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/l3utterfly/layla-sdk/issues"
|
|
42
|
+
}
|
|
43
|
+
}
|