@memizy/host-sdk 0.0.0-stage → 1.0.0-rc.6
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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/frame.cjs +1 -0
- package/dist/frame.js +78 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +10 -0
- package/dist/learning.cjs +1 -0
- package/dist/learning.js +43 -0
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.js +52 -0
- package/dist/relay/host.cjs +1 -0
- package/dist/relay/host.js +542 -0
- package/dist/relay/messages.cjs +1 -0
- package/dist/relay/messages.js +27 -0
- package/dist/relay/player.cjs +1 -0
- package/dist/relay/player.js +327 -0
- package/dist/relay/socket.cjs +1 -0
- package/dist/relay/socket.js +74 -0
- package/dist/session.cjs +1 -0
- package/dist/session.js +548 -0
- package/dist/src/frame.d.ts +43 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/learning.d.ts +18 -0
- package/dist/src/plugin.d.ts +51 -0
- package/dist/src/relay/host.d.ts +156 -0
- package/dist/src/relay/messages.d.ts +137 -0
- package/dist/src/relay/player.d.ts +127 -0
- package/dist/src/relay/socket.d.ts +34 -0
- package/dist/src/session.d.ts +248 -0
- package/dist/src/storage.d.ts +27 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/storage.cjs +1 -0
- package/dist/storage.js +40 -0
- package/dist/version.cjs +1 -0
- package/dist/version.js +4 -0
- package/package.json +62 -4
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { AnswerRecord, HostApi, HostAs, Player, PluginApi, PluginRuntime, PluginView, SessionEndReason, SessionResult, HostConfig, SessionClock } from '@memizy/protocol';
|
|
2
|
+
import { OQSEFile, ProgressRecord } from '@memizy/oqse';
|
|
3
|
+
import { LearningAlgorithm } from './learning.js';
|
|
4
|
+
import { HostStorage } from './storage.js';
|
|
5
|
+
import { LoadedPlugin, PreparedSet } from './plugin.js';
|
|
6
|
+
export interface SessionPlayer {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
isHost?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SessionConfig {
|
|
12
|
+
plugin: LoadedPlugin;
|
|
13
|
+
/** The study set, loaded with `loadOQSEFile`. */
|
|
14
|
+
set: OQSEFile;
|
|
15
|
+
mode: 'solo' | 'multiplayer';
|
|
16
|
+
/** Multiplayer only: how the host takes part. */
|
|
17
|
+
hostAs?: HostAs;
|
|
18
|
+
/** Solo: exactly one player (default: a player called "Player"). Multiplayer: the roster. */
|
|
19
|
+
players?: SessionPlayer[];
|
|
20
|
+
/** Setting values (defaults are applied; invalid values throw). */
|
|
21
|
+
settings?: Record<string, unknown>;
|
|
22
|
+
storage?: HostStorage;
|
|
23
|
+
algorithm?: LearningAlgorithm;
|
|
24
|
+
/** Loader for assets that are not absolute URLs (e.g. files of an `.oqse` package). */
|
|
25
|
+
loadAsset?: (key: string, itemId: string | undefined) => Promise<Blob>;
|
|
26
|
+
/** Maps a player to the key of their stored progress and data (default: the player ID). */
|
|
27
|
+
userKeyFor?: (playerId: string) => string;
|
|
28
|
+
/** Language of the app (default `'en'`). */
|
|
29
|
+
locale?: string;
|
|
30
|
+
/** Theme of the app (default `'light'`). */
|
|
31
|
+
theme?: HostConfig['theme'];
|
|
32
|
+
sessionId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Seed of the display order of options, pairs… (SPEC 4.4; default: the session id).
|
|
35
|
+
* Pass the seed used for the relay bundle so all devices agree; `null` keeps the set's order.
|
|
36
|
+
*/
|
|
37
|
+
shuffleSeed?: string | null;
|
|
38
|
+
host?: {
|
|
39
|
+
name: string;
|
|
40
|
+
version: string;
|
|
41
|
+
};
|
|
42
|
+
/** Max wait for all instances to be ready before the countdown (default 15 s). */
|
|
43
|
+
readyTimeoutMs?: number;
|
|
44
|
+
/** Countdown before the game starts in multiplayer (default 3 s; 0 = none). */
|
|
45
|
+
countdownMs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* The session was already running (e.g. the host page was reloaded; pass the
|
|
48
|
+
* same `sessionId` and a persistent `storage`). The authority resumes from its
|
|
49
|
+
* last snapshot; without a snapshot the game starts again right away.
|
|
50
|
+
*/
|
|
51
|
+
restore?: boolean;
|
|
52
|
+
/** When resuming: the pauses so far (from the `paused` events), so timers stay right. */
|
|
53
|
+
clock?: {
|
|
54
|
+
pausedMs?: number;
|
|
55
|
+
pausedAt?: number | null;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Memizy services this host offers (SPEC 5.3), by name. A plugin may call those it
|
|
59
|
+
* declares in `services`; payloads and results are JSON. Throw a `ProtocolError` to
|
|
60
|
+
* reject a call with a specific code.
|
|
61
|
+
*/
|
|
62
|
+
services?: Record<string, ServiceHandler>;
|
|
63
|
+
}
|
|
64
|
+
/** Handles one service call. `playerId` is null on the board. */
|
|
65
|
+
export type ServiceHandler = (payload: unknown, context: {
|
|
66
|
+
playerId: string | null;
|
|
67
|
+
address: string;
|
|
68
|
+
pluginId: string;
|
|
69
|
+
setId: string;
|
|
70
|
+
}) => Promise<unknown>;
|
|
71
|
+
export type SessionEvent = {
|
|
72
|
+
type: 'ready';
|
|
73
|
+
address: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'countdown';
|
|
76
|
+
secondsLeft: number;
|
|
77
|
+
} | {
|
|
78
|
+
type: 'started';
|
|
79
|
+
} | {
|
|
80
|
+
type: 'settings';
|
|
81
|
+
values: Record<string, unknown>;
|
|
82
|
+
valid: boolean;
|
|
83
|
+
message?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A recorded answer. `record` is the updated learning progress (null for a generated item,
|
|
87
|
+
* which is not repeated); `skills` / `tags` come from the item (for statistics).
|
|
88
|
+
*/
|
|
89
|
+
| {
|
|
90
|
+
type: 'answer';
|
|
91
|
+
playerId: string;
|
|
92
|
+
record: ProgressRecord | null;
|
|
93
|
+
answer: AnswerRecord;
|
|
94
|
+
generated: boolean;
|
|
95
|
+
skills: string[];
|
|
96
|
+
tags: string[];
|
|
97
|
+
}
|
|
98
|
+
/** The game reported its result (`end(result)`); the session itself ends with `end()`. */
|
|
99
|
+
| {
|
|
100
|
+
type: 'result';
|
|
101
|
+
result: SessionResult;
|
|
102
|
+
} | {
|
|
103
|
+
type: 'players';
|
|
104
|
+
players: Player[];
|
|
105
|
+
} | {
|
|
106
|
+
type: 'authority';
|
|
107
|
+
connected: boolean;
|
|
108
|
+
} | {
|
|
109
|
+
type: 'traffic';
|
|
110
|
+
from: string;
|
|
111
|
+
to: string;
|
|
112
|
+
bytes: number;
|
|
113
|
+
data: unknown;
|
|
114
|
+
} | {
|
|
115
|
+
type: 'rejected';
|
|
116
|
+
address: string;
|
|
117
|
+
method: string;
|
|
118
|
+
code: string;
|
|
119
|
+
message: string;
|
|
120
|
+
} | {
|
|
121
|
+
type: 'pluginError';
|
|
122
|
+
address: string;
|
|
123
|
+
code: string;
|
|
124
|
+
message: string;
|
|
125
|
+
context?: Record<string, unknown>;
|
|
126
|
+
}
|
|
127
|
+
/** The host paused or resumed the game (store `clock` to resume a reloaded host). */
|
|
128
|
+
| {
|
|
129
|
+
type: 'paused';
|
|
130
|
+
paused: boolean;
|
|
131
|
+
clock: {
|
|
132
|
+
pausedMs: number;
|
|
133
|
+
pausedAt: number | null;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/** Creates the plugin side of an instance for a given host API (iframe + Penpal, or a direct connection in tests). */
|
|
137
|
+
export type PluginEndpoint = (hostApi: HostApi) => PluginApi | Promise<PluginApi>;
|
|
138
|
+
export interface SessionInstance {
|
|
139
|
+
readonly address: string;
|
|
140
|
+
readonly view: PluginView;
|
|
141
|
+
readonly hostApi: HostApi;
|
|
142
|
+
}
|
|
143
|
+
export declare class LocalSession {
|
|
144
|
+
readonly id: string;
|
|
145
|
+
readonly plugin: LoadedPlugin;
|
|
146
|
+
readonly runtime: PluginRuntime;
|
|
147
|
+
readonly mode: 'solo' | 'multiplayer';
|
|
148
|
+
readonly hostAs: HostAs | null;
|
|
149
|
+
readonly authority: string;
|
|
150
|
+
readonly prepared: PreparedSet;
|
|
151
|
+
private roster;
|
|
152
|
+
private settingValues;
|
|
153
|
+
private settingsOk;
|
|
154
|
+
private hostConfig;
|
|
155
|
+
private isStarted;
|
|
156
|
+
private isEnded;
|
|
157
|
+
/** Pauses of the game (game time = session time without them). */
|
|
158
|
+
private pauseState;
|
|
159
|
+
private readonly options;
|
|
160
|
+
private readonly storage;
|
|
161
|
+
private readonly algorithm;
|
|
162
|
+
private readonly instances;
|
|
163
|
+
private readonly listeners;
|
|
164
|
+
private authorityConnected;
|
|
165
|
+
private startPromise;
|
|
166
|
+
private resumeStartPending;
|
|
167
|
+
private pendingSnapshot;
|
|
168
|
+
/** The game has a result (`end(result)` or `end('finished')`): its snapshot is gone for good. */
|
|
169
|
+
private finished;
|
|
170
|
+
/** The authority called `end(result)`; later calls are ignored. */
|
|
171
|
+
private hasResult;
|
|
172
|
+
/** After the end, snapshots live only here: a reloaded authority still shows the end screen. */
|
|
173
|
+
private finishedSnapshot;
|
|
174
|
+
private snapshotTimer;
|
|
175
|
+
private readonly pendingData;
|
|
176
|
+
/** The players (the presenter is not one). Change them with `addPlayer`, `removePlayer`, `renamePlayer`, `setConnected`. */
|
|
177
|
+
get players(): readonly Player[];
|
|
178
|
+
/** Values of the settings (defaults applied; the plugin's own settings screen may change them in the lobby). */
|
|
179
|
+
get settings(): Readonly<Record<string, unknown>>;
|
|
180
|
+
/** Whether the settings are valid (the plugin's own settings screen reports it). */
|
|
181
|
+
get settingsValid(): boolean;
|
|
182
|
+
/** Language and theme of the app (change them with `setConfig`). */
|
|
183
|
+
get config(): Readonly<HostConfig>;
|
|
184
|
+
get started(): boolean;
|
|
185
|
+
get ended(): boolean;
|
|
186
|
+
constructor(config: SessionConfig);
|
|
187
|
+
on(listener: (event: SessionEvent) => void): () => void;
|
|
188
|
+
private emit;
|
|
189
|
+
/** The default view of an address in this session. */
|
|
190
|
+
viewOf(address: string): PluginView;
|
|
191
|
+
/** Addresses that have a game instance in this session (board and players). */
|
|
192
|
+
addresses(): string[];
|
|
193
|
+
/**
|
|
194
|
+
* Connects a plugin instance at `address` (a player ID, `"board"`, or `"settings"`
|
|
195
|
+
* for the lobby settings screen). An existing instance at the address is replaced.
|
|
196
|
+
*/
|
|
197
|
+
connect(address: string, endpoint: PluginEndpoint, options?: {
|
|
198
|
+
onDispose?: () => void;
|
|
199
|
+
}): Promise<SessionInstance>;
|
|
200
|
+
/** Recreates the instance at `address` (like a browser reload or a reconnect). */
|
|
201
|
+
reload(address: string): Promise<SessionInstance>;
|
|
202
|
+
/** Removes the instance at `address` (the player stays in the roster). */
|
|
203
|
+
disconnect(address: string): void;
|
|
204
|
+
private disposeInstance;
|
|
205
|
+
getInstance(address: string): SessionInstance | undefined;
|
|
206
|
+
/**
|
|
207
|
+
* Multiplayer: waits until all connected game instances are ready (or the
|
|
208
|
+
* ready timeout passes), runs the countdown and starts the game on the
|
|
209
|
+
* authority (SPEC 3.3). Solo games start automatically when ready.
|
|
210
|
+
*/
|
|
211
|
+
start(): Promise<void>;
|
|
212
|
+
private runStart;
|
|
213
|
+
addPlayer(player: SessionPlayer): void;
|
|
214
|
+
removePlayer(id: string): void;
|
|
215
|
+
renamePlayer(id: string, name: string): void;
|
|
216
|
+
/**
|
|
217
|
+
* Simulates a connection loss / recovery of an instance. Messages to and from a
|
|
218
|
+
* disconnected instance are dropped; for the authority, the others are told
|
|
219
|
+
* that the authority is unavailable (SPEC 6.1).
|
|
220
|
+
*/
|
|
221
|
+
setConnected(address: string, connected: boolean): void;
|
|
222
|
+
/** Services the plugin declares and this host offers (SPEC 5.3). */
|
|
223
|
+
get services(): string[];
|
|
224
|
+
/** Whether the game is paused (the clock stands still on every device). */
|
|
225
|
+
get paused(): boolean;
|
|
226
|
+
/** The session clock as sent to the instances (host time; the relay adds each device's skew). */
|
|
227
|
+
get clock(): SessionClock;
|
|
228
|
+
/** Pauses the game: timers and countdowns stop, players cannot act (the SDK shows "Paused"). */
|
|
229
|
+
pause(): void;
|
|
230
|
+
/** Continues a paused game. */
|
|
231
|
+
resume(): void;
|
|
232
|
+
private broadcastClock;
|
|
233
|
+
setConfig(config: Partial<HostConfig>): void;
|
|
234
|
+
/** Ends the session for all instances (they stay mounted until the app removes them). */
|
|
235
|
+
end(reason?: SessionEndReason): Promise<void>;
|
|
236
|
+
private createHostApi;
|
|
237
|
+
private buildInit;
|
|
238
|
+
private userKey;
|
|
239
|
+
private deliver;
|
|
240
|
+
private broadcastPlayers;
|
|
241
|
+
private setAuthorityConnected;
|
|
242
|
+
private callPlugin;
|
|
243
|
+
/** Drops the pending snapshot and deletes the saved one; later saves are ignored. */
|
|
244
|
+
private discardSnapshot;
|
|
245
|
+
private flushSnapshot;
|
|
246
|
+
private pendingDataValue;
|
|
247
|
+
private flushData;
|
|
248
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ProgressRecord } from '@memizy/oqse';
|
|
2
|
+
import { DataScope } from '@memizy/protocol';
|
|
3
|
+
export interface HostStorage {
|
|
4
|
+
loadProgress(userKey: string, setId: string): Promise<Record<string, ProgressRecord>>;
|
|
5
|
+
/** Upserts the given records (other records stay). */
|
|
6
|
+
saveProgress(userKey: string, setId: string, records: Record<string, ProgressRecord>): Promise<void>;
|
|
7
|
+
/** `setId` is ignored for scope `plugin`. Returns `null` when nothing is saved. */
|
|
8
|
+
loadData(userKey: string, pluginId: string, scope: DataScope, setId: string): Promise<unknown>;
|
|
9
|
+
saveData(userKey: string, pluginId: string, scope: DataScope, setId: string, value: unknown): Promise<void>;
|
|
10
|
+
loadSnapshot(sessionId: string): Promise<unknown | null>;
|
|
11
|
+
saveSnapshot(sessionId: string, snapshot: unknown): Promise<void>;
|
|
12
|
+
/** Removes the snapshot of a finished game (optional; without it the snapshot stays). */
|
|
13
|
+
deleteSnapshot?(sessionId: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** In-memory storage (values are copied, like a real store would). */
|
|
16
|
+
export declare class MemoryStorage implements HostStorage {
|
|
17
|
+
readonly progress: Map<string, Record<string, ProgressRecord>>;
|
|
18
|
+
readonly data: Map<string, unknown>;
|
|
19
|
+
readonly snapshots: Map<string, unknown>;
|
|
20
|
+
loadProgress(userKey: string, setId: string): Promise<Record<string, ProgressRecord>>;
|
|
21
|
+
saveProgress(userKey: string, setId: string, records: Record<string, ProgressRecord>): Promise<void>;
|
|
22
|
+
loadData(userKey: string, pluginId: string, scope: DataScope, setId: string): Promise<{} | null>;
|
|
23
|
+
saveData(userKey: string, pluginId: string, scope: DataScope, setId: string, value: unknown): Promise<void>;
|
|
24
|
+
loadSnapshot(sessionId: string): Promise<{} | null>;
|
|
25
|
+
saveSnapshot(sessionId: string, snapshot: unknown): Promise<void>;
|
|
26
|
+
deleteSnapshot(sessionId: string): Promise<void>;
|
|
27
|
+
}
|
package/dist/storage.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=(e,t,n,r)=>JSON.stringify([e,t,n,n===`set`?r:null]),t=class{progress=new Map;data=new Map;snapshots=new Map;async loadProgress(e,t){return structuredClone(this.progress.get(`${e}|${t}`)??{})}async saveProgress(e,t,n){let r=`${e}|${t}`;this.progress.set(r,{...this.progress.get(r)??{},...structuredClone(n)})}async loadData(t,n,r,i){let a=this.data.get(e(t,n,r,i));return a===void 0?null:structuredClone(a)}async saveData(t,n,r,i,a){this.data.set(e(t,n,r,i),structuredClone(a))}async loadSnapshot(e){let t=this.snapshots.get(e);return t===void 0?null:structuredClone(t)}async saveSnapshot(e,t){this.snapshots.set(e,structuredClone(t))}async deleteSnapshot(e){this.snapshots.delete(e)}};exports.MemoryStorage=t;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/storage.ts
|
|
2
|
+
var e = (e, t, n, r) => JSON.stringify([
|
|
3
|
+
e,
|
|
4
|
+
t,
|
|
5
|
+
n,
|
|
6
|
+
n === "set" ? r : null
|
|
7
|
+
]), t = class {
|
|
8
|
+
progress = /* @__PURE__ */ new Map();
|
|
9
|
+
data = /* @__PURE__ */ new Map();
|
|
10
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
11
|
+
async loadProgress(e, t) {
|
|
12
|
+
return structuredClone(this.progress.get(`${e}|${t}`) ?? {});
|
|
13
|
+
}
|
|
14
|
+
async saveProgress(e, t, n) {
|
|
15
|
+
let r = `${e}|${t}`;
|
|
16
|
+
this.progress.set(r, {
|
|
17
|
+
...this.progress.get(r) ?? {},
|
|
18
|
+
...structuredClone(n)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async loadData(t, n, r, i) {
|
|
22
|
+
let a = this.data.get(e(t, n, r, i));
|
|
23
|
+
return a === void 0 ? null : structuredClone(a);
|
|
24
|
+
}
|
|
25
|
+
async saveData(t, n, r, i, a) {
|
|
26
|
+
this.data.set(e(t, n, r, i), structuredClone(a));
|
|
27
|
+
}
|
|
28
|
+
async loadSnapshot(e) {
|
|
29
|
+
let t = this.snapshots.get(e);
|
|
30
|
+
return t === void 0 ? null : structuredClone(t);
|
|
31
|
+
}
|
|
32
|
+
async saveSnapshot(e, t) {
|
|
33
|
+
this.snapshots.set(e, structuredClone(t));
|
|
34
|
+
}
|
|
35
|
+
async deleteSnapshot(e) {
|
|
36
|
+
this.snapshots.delete(e);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { t as MemoryStorage };
|
package/dist/version.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=`1.0.0-rc.6`;exports.HOST_SDK_VERSION=e;
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memizy/host-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-rc.6",
|
|
4
|
+
"description": "Run Memizy plugins: sandboxed iframes, protocol validation, sessions, storage hooks.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/src/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/src/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc --noEmit && vite build && node ../../scripts/dts-extensions.mjs dist",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@memizy/oqse": "^1.0.0-rc.6",
|
|
32
|
+
"@memizy/protocol": "^1.0.0-rc.6",
|
|
33
|
+
"penpal": "^8.0.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@memizy/plugin-sdk": "^1.0.0-rc.6",
|
|
37
|
+
"@types/node": "^25.6.0",
|
|
38
|
+
"@types/ws": "^8",
|
|
39
|
+
"jsdom": "^30.1.2",
|
|
40
|
+
"typescript": "~6.0.2",
|
|
41
|
+
"vite": "^8.0.9",
|
|
42
|
+
"vite-plugin-dts": "^4.5.4",
|
|
43
|
+
"vitest": "^4.1.4",
|
|
44
|
+
"ws": "^8"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"author": "Memizy Team",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"memizy",
|
|
50
|
+
"plugin",
|
|
51
|
+
"host",
|
|
52
|
+
"sdk",
|
|
53
|
+
"multiplayer"
|
|
54
|
+
],
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/memizy/memizy.git",
|
|
58
|
+
"directory": "packages/host-sdk"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/memizy/memizy/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/memizy/memizy/tree/main/packages/host-sdk#readme"
|
|
64
|
+
}
|