@tomorrowos/sdk 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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Persistent (or replaceable) data for pairing — scheme C.
3
+ * Live WebSocket handles stay in-memory on the TomorrowOS instance.
4
+ */
5
+ export {};
@@ -0,0 +1,62 @@
1
+ import { EventEmitter } from "events";
2
+ import http from "http";
3
+ import type { TomorrowOSStore } from "./store/types.js";
4
+ export interface TomorrowOSBrand {
5
+ name?: string;
6
+ cmsEndpoint?: string;
7
+ [key: string]: unknown;
8
+ }
9
+ export interface TomorrowOSOptions {
10
+ brand: TomorrowOSBrand;
11
+ /** Scheme C: inject Postgres/Redis-backed store. Defaults to MemoryStore. */
12
+ store?: TomorrowOSStore;
13
+ }
14
+ export interface ListenOptions {
15
+ port: number;
16
+ host?: string;
17
+ /**
18
+ * If set, GET requests serve files from this directory (e.g. CMS UI assets).
19
+ * `GET /` serves `staticIndex` (default `index.html`) under this root.
20
+ * WebSocket upgrades on `/` are unchanged.
21
+ */
22
+ staticRoot?: string;
23
+ /**
24
+ * Entry file under `staticRoot` for `GET /` (relative path only, no `..`).
25
+ * Default: `index.html`. Ignored when `staticRoot` is not set.
26
+ */
27
+ staticIndex?: string;
28
+ }
29
+ export declare class TomorrowOS extends EventEmitter {
30
+ readonly brand: TomorrowOSBrand;
31
+ private readonly store;
32
+ private readonly devices;
33
+ private httpServer;
34
+ private wss;
35
+ private staticRoot;
36
+ private staticIndexFile;
37
+ constructor(options: TomorrowOSOptions);
38
+ /** Verify a 6-digit pairing code (same as POST /pairing/verify). */
39
+ pairingVerify(code: string): Promise<{
40
+ deviceId: string;
41
+ }>;
42
+ pairing: {
43
+ verify: (code: string) => Promise<{
44
+ deviceId: string;
45
+ }>;
46
+ };
47
+ device(deviceId: string): {
48
+ sendCommand<T = unknown>(method: string, params?: Record<string, unknown>): Promise<{
49
+ status: string;
50
+ data?: T;
51
+ error?: string;
52
+ stack?: string;
53
+ }>;
54
+ };
55
+ private sendCommandToSocket;
56
+ listen(options: ListenOptions): http.Server;
57
+ private tryServeStatic;
58
+ private handleHttp;
59
+ private handleDeviceHttp;
60
+ private handleConnection;
61
+ }
62
+ //# sourceMappingURL=tomorrowos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAKxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAmDD,qBAAa,UAAW,SAAQ,YAAY;IAC1C,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,eAAe,CAAgB;gBAE3B,OAAO,EAAE,iBAAiB;IAMtC,oEAAoE;IAC9D,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAiChE,OAAO;uBACU,MAAM;sBAlCgC,MAAM;;MAmC3D;IAEF,MAAM,CAAC,QAAQ,EAAE,MAAM;oBAGD,CAAC,oBACT,MAAM,WACN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;;IAU5E,OAAO,CAAC,mBAAmB;IA6D3B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC,MAAM;YAgD7B,cAAc;YAmCd,UAAU;YAyCV,gBAAgB;IAgE9B,OAAO,CAAC,gBAAgB;CA8EzB"}
@@ -0,0 +1,371 @@
1
+ import { randomBytes, randomUUID } from "crypto";
2
+ import { EventEmitter } from "events";
3
+ import fs from "fs/promises";
4
+ import http from "http";
5
+ import path from "path";
6
+ import { WebSocket, WebSocketServer } from "ws";
7
+ import { MemoryStore } from "./store/memory-store.js";
8
+ function createSixDigitCode() {
9
+ return Math.floor(100000 + Math.random() * 900000).toString();
10
+ }
11
+ async function readJsonBody(req) {
12
+ const chunks = [];
13
+ for await (const chunk of req) {
14
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
15
+ }
16
+ const raw = Buffer.concat(chunks).toString("utf8");
17
+ if (!raw.trim())
18
+ return {};
19
+ try {
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ throw new Error("Invalid JSON body");
24
+ }
25
+ }
26
+ function sendJson(res, status, body) {
27
+ res.writeHead(status, { "Content-Type": "application/json" });
28
+ res.end(JSON.stringify(body));
29
+ }
30
+ function parseDevicePath(pathname) {
31
+ const base = /^\/device\/([^/]+)\/(.+)$/.exec(pathname);
32
+ if (!base)
33
+ return null;
34
+ const deviceId = decodeURIComponent(base[1]);
35
+ const action = base[2];
36
+ return { deviceId, action };
37
+ }
38
+ export class TomorrowOS extends EventEmitter {
39
+ brand;
40
+ store;
41
+ devices = new Map();
42
+ httpServer = null;
43
+ wss = null;
44
+ staticRoot = null;
45
+ staticIndexFile = "index.html";
46
+ constructor(options) {
47
+ super();
48
+ this.brand = options.brand;
49
+ this.store = options.store ?? new MemoryStore();
50
+ }
51
+ /** Verify a 6-digit pairing code (same as POST /pairing/verify). */
52
+ async pairingVerify(code) {
53
+ const record = await this.store.getPendingCode(code);
54
+ if (!record) {
55
+ const err = new Error("Invalid or expired code");
56
+ err.code = "PAIRING_INVALID";
57
+ throw err;
58
+ }
59
+ const pairingToken = randomBytes(32).toString("hex");
60
+ const pairedAt = new Date().toISOString();
61
+ await this.store.setPairedDevice(record.deviceId, {
62
+ pairingToken,
63
+ pairedAt
64
+ });
65
+ await this.store.deletePendingCode(code);
66
+ const ws = this.devices.get(record.deviceId);
67
+ if (ws && ws.readyState === WebSocket.OPEN) {
68
+ ws.send(JSON.stringify({
69
+ type: "pairing.verified",
70
+ method: "tomorrowos.pairing.verify",
71
+ deviceId: record.deviceId,
72
+ pairingToken
73
+ }));
74
+ }
75
+ this.emit("device.paired", { deviceId: record.deviceId });
76
+ return { deviceId: record.deviceId };
77
+ }
78
+ pairing = {
79
+ verify: (code) => this.pairingVerify(code)
80
+ };
81
+ device(deviceId) {
82
+ const self = this;
83
+ return {
84
+ async sendCommand(method, params = {}) {
85
+ const ws = self.devices.get(deviceId);
86
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
87
+ throw new Error("Device not connected");
88
+ }
89
+ return self.sendCommandToSocket(ws, deviceId, method, params);
90
+ }
91
+ };
92
+ }
93
+ sendCommandToSocket(ws, deviceId, method, params, timeoutMs = 60_000) {
94
+ const commandId = randomUUID();
95
+ return new Promise((resolve, reject) => {
96
+ const timer = setTimeout(() => {
97
+ ws.removeListener("message", onMessage);
98
+ reject(new Error(`Command timeout: ${method}`));
99
+ }, timeoutMs);
100
+ const onMessage = (data) => {
101
+ let msg;
102
+ try {
103
+ msg = JSON.parse(String(data));
104
+ }
105
+ catch {
106
+ return;
107
+ }
108
+ if (msg.type === "command.result" &&
109
+ msg.commandId === commandId &&
110
+ msg.method === method) {
111
+ clearTimeout(timer);
112
+ ws.removeListener("message", onMessage);
113
+ const status = String(msg.status ?? "unknown");
114
+ if (status === "failed") {
115
+ const error = new Error(String(msg.error ?? "Command failed"));
116
+ this.emit("command.failed", {
117
+ commandId,
118
+ method,
119
+ deviceId,
120
+ error
121
+ });
122
+ resolve({
123
+ status,
124
+ error: String(msg.error),
125
+ stack: msg.stack
126
+ });
127
+ return;
128
+ }
129
+ this.emit("command.verified", { commandId, method, deviceId });
130
+ resolve({ status, data: msg.data });
131
+ }
132
+ };
133
+ ws.on("message", onMessage);
134
+ ws.send(JSON.stringify({
135
+ type: "command",
136
+ commandId,
137
+ method,
138
+ params
139
+ }));
140
+ });
141
+ }
142
+ listen(options) {
143
+ const { port, host = "0.0.0.0", staticRoot, staticIndex } = options;
144
+ this.staticRoot = staticRoot ?? null;
145
+ if (this.staticRoot) {
146
+ const idx = (staticIndex ?? "index.html").trim().replace(/^[\\/]+/, "") || "index.html";
147
+ if (idx.includes("..")) {
148
+ throw new Error("staticIndex must not contain '..'");
149
+ }
150
+ this.staticIndexFile = idx;
151
+ }
152
+ else {
153
+ this.staticIndexFile = "index.html";
154
+ }
155
+ const server = http.createServer((req, res) => {
156
+ void this.handleHttp(req, res);
157
+ });
158
+ const wss = new WebSocketServer({ noServer: true });
159
+ server.on("upgrade", (request, socket, head) => {
160
+ if (!request.url) {
161
+ socket.destroy();
162
+ return;
163
+ }
164
+ const { pathname } = new URL(request.url, `http://${request.headers.host}`);
165
+ if (pathname === "/" || pathname === "") {
166
+ wss.handleUpgrade(request, socket, head, (ws) => {
167
+ wss.emit("connection", ws, request);
168
+ });
169
+ }
170
+ else {
171
+ socket.destroy();
172
+ }
173
+ });
174
+ wss.on("connection", (ws) => {
175
+ this.handleConnection(ws);
176
+ });
177
+ server.listen(port, host, () => {
178
+ // eslint-disable-next-line no-console
179
+ console.log(`[TomorrowOS] listening on http://${host}:${port}`);
180
+ });
181
+ this.httpServer = server;
182
+ this.wss = wss;
183
+ return server;
184
+ }
185
+ async tryServeStatic(pathname, res) {
186
+ if (!this.staticRoot)
187
+ return false;
188
+ const rel = pathname === "/" || pathname === "" ? this.staticIndexFile : pathname.slice(1);
189
+ if (!rel || rel.includes(".."))
190
+ return false;
191
+ const rootResolved = path.resolve(this.staticRoot);
192
+ const filePath = path.resolve(rootResolved, rel);
193
+ const relativeToRoot = path.relative(rootResolved, filePath);
194
+ if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) {
195
+ return false;
196
+ }
197
+ try {
198
+ const buf = await fs.readFile(filePath);
199
+ const ext = path.extname(rel).toLowerCase();
200
+ const types = {
201
+ ".html": "text/html; charset=utf-8",
202
+ ".js": "application/javascript; charset=utf-8",
203
+ ".css": "text/css; charset=utf-8",
204
+ ".json": "application/json; charset=utf-8"
205
+ };
206
+ const ctype = types[ext] ?? "application/octet-stream";
207
+ res.writeHead(200, { "Content-Type": ctype });
208
+ res.end(buf);
209
+ return true;
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ }
215
+ async handleHttp(req, res) {
216
+ try {
217
+ const url = new URL(req.url ?? "/", "http://localhost");
218
+ const pathname = url.pathname;
219
+ if (req.method === "GET" && this.staticRoot) {
220
+ const served = await this.tryServeStatic(pathname, res);
221
+ if (served)
222
+ return;
223
+ }
224
+ if (req.method === "POST" && pathname === "/pairing/verify") {
225
+ const body = (await readJsonBody(req));
226
+ const code = typeof body.code === "string" ? body.code : "";
227
+ try {
228
+ const { deviceId } = await this.pairingVerify(code);
229
+ sendJson(res, 200, { status: "success", deviceId });
230
+ }
231
+ catch (e) {
232
+ const msg = e instanceof Error ? e.message : "Verify failed";
233
+ sendJson(res, 400, { status: "failed", error: msg });
234
+ }
235
+ return;
236
+ }
237
+ if (req.method === "POST") {
238
+ const parsed = parseDevicePath(pathname);
239
+ if (parsed) {
240
+ await this.handleDeviceHttp(req, res, parsed);
241
+ return;
242
+ }
243
+ }
244
+ sendJson(res, 404, { status: "failed", error: "Not found" });
245
+ }
246
+ catch (e) {
247
+ const msg = e instanceof Error ? e.message : "Server error";
248
+ sendJson(res, 500, { status: "failed", error: msg });
249
+ }
250
+ }
251
+ async handleDeviceHttp(req, res, parsed) {
252
+ const { deviceId, action } = parsed;
253
+ const ws = this.devices.get(deviceId);
254
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
255
+ sendJson(res, 404, { status: "failed", error: "Device not connected" });
256
+ return;
257
+ }
258
+ let method;
259
+ let params = {};
260
+ switch (action) {
261
+ case "get-info":
262
+ method = "device.info.get";
263
+ break;
264
+ case "get-capabilities":
265
+ method = "device.info.getCapabilities";
266
+ break;
267
+ case "reboot":
268
+ method = "device.power.reboot";
269
+ break;
270
+ case "content/set-policy": {
271
+ method = "device.content.setPolicy";
272
+ const body = (await readJsonBody(req));
273
+ if (!body.policy || typeof body.policy !== "object") {
274
+ sendJson(res, 400, {
275
+ status: "failed",
276
+ error: "Invalid payload. Provide params.policy object."
277
+ });
278
+ return;
279
+ }
280
+ params = { policy: body.policy };
281
+ break;
282
+ }
283
+ case "content/clear":
284
+ method = "device.content.clear";
285
+ break;
286
+ default:
287
+ sendJson(res, 404, { status: "failed", error: "Unknown device action" });
288
+ return;
289
+ }
290
+ try {
291
+ const result = await this.sendCommandToSocket(ws, deviceId, method, params);
292
+ if (result.status === "failed") {
293
+ sendJson(res, 500, {
294
+ status: result.status,
295
+ error: result.error ?? "Command failed",
296
+ stack: result.stack
297
+ });
298
+ return;
299
+ }
300
+ sendJson(res, 200, { status: result.status, data: result.data });
301
+ }
302
+ catch (e) {
303
+ const msg = e instanceof Error ? e.message : "Command failed";
304
+ sendJson(res, 500, { status: "failed", error: msg });
305
+ }
306
+ }
307
+ handleConnection(ws) {
308
+ ws.on("message", (raw) => {
309
+ let msg;
310
+ try {
311
+ msg = JSON.parse(String(raw));
312
+ }
313
+ catch {
314
+ return;
315
+ }
316
+ const type = msg.type;
317
+ if (type === "device.hello") {
318
+ const deviceId = typeof msg.deviceId === "string" && msg.deviceId
319
+ ? msg.deviceId
320
+ : randomUUID();
321
+ const code = createSixDigitCode();
322
+ this.devices.set(deviceId, ws);
323
+ void this.store.setPendingCode(code, {
324
+ deviceId,
325
+ createdAt: Date.now()
326
+ });
327
+ ws.deviceId = deviceId;
328
+ ws.send(JSON.stringify({
329
+ type: "pairing.code",
330
+ method: "tomorrowos.pairing.createCode",
331
+ code,
332
+ deviceId
333
+ }));
334
+ this.emit("device.online", { deviceId });
335
+ return;
336
+ }
337
+ if (type === "device.resume") {
338
+ const deviceId = String(msg.deviceId ?? "");
339
+ const pairingToken = String(msg.pairingToken ?? "");
340
+ void (async () => {
341
+ const record = await this.store.getPairedDevice(deviceId);
342
+ if (!record || record.pairingToken !== pairingToken) {
343
+ ws.send(JSON.stringify({
344
+ type: "device.resume.failed",
345
+ reason: "Invalid pairing token"
346
+ }));
347
+ return;
348
+ }
349
+ this.devices.set(deviceId, ws);
350
+ ws.deviceId = deviceId;
351
+ ws.send(JSON.stringify({
352
+ type: "device.resumed",
353
+ method: "tomorrowos.pairing.resume",
354
+ deviceId
355
+ }));
356
+ this.emit("device.online", { deviceId });
357
+ })();
358
+ }
359
+ });
360
+ ws.on("close", () => {
361
+ const id = ws.deviceId;
362
+ if (id) {
363
+ this.devices.delete(id);
364
+ this.emit("device.offline", {
365
+ deviceId: id,
366
+ lastSeen: new Date().toISOString()
367
+ });
368
+ }
369
+ });
370
+ }
371
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@tomorrowos/sdk",
3
+ "version": "0.1.0",
4
+ "description": "TomorrowOS CMS server SDK — WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "tomorrowos": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "templates",
20
+ "README.md",
21
+ "LLM_PROMPT.md",
22
+ "BUILD_GUARDRAILS.md",
23
+ "PLAYER_INSTALL.md",
24
+ "brand.schema.json",
25
+ "brand.example.json"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "ws": "^8.18.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.19.41",
39
+ "@types/ws": "^8.5.10",
40
+ "typescript": "^5.5.0"
41
+ },
42
+ "license": "Apache-2.0"
43
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="160" height="40" viewBox="0 0 160 40" role="img" aria-label="Logo placeholder">
2
+ <rect width="160" height="40" rx="6" fill="#FF8A3D"/>
3
+ <text x="80" y="26" text-anchor="middle" fill="#ffffff" font-family="system-ui,sans-serif" font-size="14" font-weight="600">Your logo</text>
4
+ </svg>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "My Venue",
3
+ "tagline": "Digital signage",
4
+ "targetPlatforms": ["tizen"],
5
+ "primaryColor": "#FF8A3D",
6
+ "secondaryColor": "#F5F3EF",
7
+ "backgroundColor": "#FAFAF9",
8
+ "textColor": "#0A0908",
9
+ "logoPath": "./assets/logo.svg",
10
+ "fontFamily": "Inter",
11
+ "cmsEndpoint": "ws://localhost:3000",
12
+ "cms": {
13
+ "useCase": "other",
14
+ "hostingTarget": "self-hosted",
15
+ "expectedScreens": 5,
16
+ "features": {
17
+ "bulkCommands": false,
18
+ "proofOfPlay": false,
19
+ "contentScheduling": true,
20
+ "userManagement": false
21
+ }
22
+ },
23
+ "protocolVersion": "1.0"
24
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "my-tomorrowos-cms",
3
+ "version": "0.1.0",
4
+ "description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "tsx watch server.ts",
9
+ "start": "tsx server.ts",
10
+ "build-player": "tomorrowos build --platform tizen"
11
+ },
12
+ "dependencies": {
13
+ "@tomorrowos/sdk": "^1.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.0.0",
17
+ "tsx": "^4.19.0",
18
+ "typescript": "^5.5.0"
19
+ }
20
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * TomorrowOS CMS server — minimal starter.
3
+ * Add routes, database, or serve a React build via listen({ staticRoot }).
4
+ */
5
+
6
+ import { readFileSync } from "fs";
7
+ import { fileURLToPath } from "url";
8
+ import { dirname, join } from "path";
9
+ import { TomorrowOS } from "@tomorrowos/sdk";
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
13
+
14
+ const tomorrowos = new TomorrowOS({ brand });
15
+
16
+ tomorrowos.listen({
17
+ port: Number(process.env.PORT) || 3000,
18
+ host: "0.0.0.0"
19
+ });
20
+
21
+ tomorrowos.on("device.paired", (event) => {
22
+ console.log(`[TomorrowOS] device paired: ${event.deviceId}`);
23
+ });
24
+
25
+ tomorrowos.on("device.online", (event) => {
26
+ console.log(`[TomorrowOS] device online: ${event.deviceId}`);
27
+ });
28
+
29
+ tomorrowos.on("device.offline", (event) => {
30
+ console.log(
31
+ `[TomorrowOS] device offline: ${event.deviceId} (lastSeen: ${event.lastSeen})`
32
+ );
33
+ });
34
+
35
+ tomorrowos.on("command.verified", (event) => {
36
+ console.log(
37
+ `[TomorrowOS] command verified: ${event.commandId} (${event.method})`
38
+ );
39
+ });
40
+
41
+ tomorrowos.on("command.failed", (event) => {
42
+ console.error(
43
+ `[TomorrowOS] command failed: ${event.commandId} (${event.method}) — ${event.error.message}`
44
+ );
45
+ });
46
+
47
+ export default tomorrowos;
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "resolveJsonModule": true,
9
+ "noEmit": true
10
+ },
11
+ "include": ["server.ts"]
12
+ }