@telemetryos/cli 1.2.0 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @telemetryos/cli
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - This change updates the CLI package to properly host applications with
8
+ full support for all SDK features.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies
13
+ - @telemetryos/development-application-host-ui@1.4.0
14
+
15
+ ## 1.3.0
16
+
17
+ ### Minor Changes
18
+
19
+ - Add Weather API
20
+
21
+ Adds a weather API for applications that wish to use it. It can be found on
22
+ client instances under .weather.
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies
27
+ - @telemetryos/development-application-host-ui@1.3.0
28
+
3
29
  ## 1.2.0
4
30
 
5
31
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const initCommand: Command;
@@ -0,0 +1,79 @@
1
+ import { Command } from 'commander';
2
+ import { generateApplication } from '../generate-application.js';
3
+ import inquirer from 'inquirer';
4
+ import path from 'path';
5
+ export const initCommand = new Command('init')
6
+ .description('Initializes a new telemetryOS application')
7
+ .option('-n, --name <string>', 'The name of the application', '')
8
+ .option('-d, --description <string>', 'The description of the application', 'A telemetryOS application')
9
+ .option('-a, --author <string>', 'The author of the application', '')
10
+ .option('-v, --version <string>', 'The version of the application', '0.1.0')
11
+ .option('-t, --template <string>', 'The template to use (vite-react-typescript)', '')
12
+ .argument('[project-path]', 'Path to create the new telemetry application project. Defaults to a folder current working directory with the same name as your project', '')
13
+ .action(handleInitCommand);
14
+ async function handleInitCommand(projectPath, options) {
15
+ let name = options.name;
16
+ let description = options.description;
17
+ let author = options.author;
18
+ let version = options.version;
19
+ let template = options.template;
20
+ const questions = [];
21
+ if (!name)
22
+ questions.push({
23
+ type: 'input',
24
+ name: 'name',
25
+ message: 'What is the name of your application?',
26
+ validate: (input) => input.length > 0 || 'Application name cannot be empty'
27
+ });
28
+ if (!description)
29
+ questions.push({
30
+ type: 'input',
31
+ name: 'description',
32
+ message: 'What is the description of your application?',
33
+ default: ''
34
+ });
35
+ if (!author)
36
+ questions.push({
37
+ type: 'input',
38
+ name: 'author',
39
+ message: 'Who is the author of your application?',
40
+ default: ''
41
+ });
42
+ if (!version)
43
+ questions.push({
44
+ type: 'input',
45
+ name: 'version',
46
+ message: 'What is the version of your application?',
47
+ default: '0.1.0',
48
+ validate: (input) => /^\d+\.\d+\.\d+(-.+)?$/.test(input) || 'Version must be in semver format (e.g. 1.0.0)'
49
+ });
50
+ if (!template)
51
+ questions.push({
52
+ type: 'list',
53
+ name: 'template',
54
+ message: 'Which template would you like to use?',
55
+ choices: [
56
+ { name: 'Vite + React + TypeScript', value: 'vite-react-typescript' }
57
+ ]
58
+ });
59
+ if (questions.length !== 0) {
60
+ const answers = await inquirer.prompt(questions);
61
+ if (answers.name)
62
+ name = answers.name;
63
+ if (answers.template)
64
+ template = answers.template;
65
+ }
66
+ if (!projectPath)
67
+ projectPath = path.join(process.cwd(), name);
68
+ await generateApplication({
69
+ name,
70
+ description,
71
+ author,
72
+ version,
73
+ template,
74
+ projectPath,
75
+ progressFn: (createdFilePath) => {
76
+ console.log(`.${path.sep}${path.relative(process.cwd(), createdFilePath)}`);
77
+ }
78
+ });
79
+ }
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
2
  import pkg from '../../package.json' with { type: 'json' };
3
- export const rootCommand = new Command('tx')
4
- .description('TelemetryX Application CLI')
3
+ export const rootCommand = new Command('tos')
4
+ .description('TelemetryOS Application CLI')
5
5
  .version(pkg.version);
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { runServer } from '../run-server.js';
3
3
  export const serveCommand = new Command('serve')
4
- .description('Serves a telemetryX application locally for development')
4
+ .description('Serves a telemetryOS application locally for development')
5
5
  .option('-p, --port <number>', 'Port to run the development ui on', '6969')
6
6
  .argument('[project-path]', 'Path to the telemetry application project. Defaults to current working directory', process.cwd())
7
7
  .action(runServer);
@@ -0,0 +1,10 @@
1
+ export type GenerateApplicationOptions = {
2
+ name: string;
3
+ description: string;
4
+ author: string;
5
+ version: string;
6
+ template: string;
7
+ projectPath: string;
8
+ progressFn: (createdFilePath: string) => void;
9
+ };
10
+ export declare function generateApplication(options: GenerateApplicationOptions): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ const ignoredTelemplateFiles = [
4
+ '.DS_Store',
5
+ 'thumbs.db',
6
+ 'node_modules',
7
+ '.git',
8
+ 'dist'
9
+ ];
10
+ const templatesDir = path.join(import.meta.dirname, '../templates');
11
+ export async function generateApplication(options) {
12
+ const { name, description, author, version, template, projectPath, progressFn } = options;
13
+ await fs.mkdir(projectPath, { recursive: true });
14
+ await copyDir(path.join(templatesDir, template), projectPath, {
15
+ name,
16
+ description,
17
+ author,
18
+ version
19
+ }, progressFn);
20
+ }
21
+ async function copyDir(source, destination, replacements, progressFn) {
22
+ const dirListing = await fs.readdir(source);
23
+ for (const dirEntry of dirListing) {
24
+ if (ignoredTelemplateFiles.includes(dirEntry))
25
+ continue;
26
+ const sourcePath = path.join(source, dirEntry);
27
+ const destinationPath = path.join(destination, dirEntry);
28
+ const stats = await fs.stat(sourcePath);
29
+ if (stats.isDirectory()) {
30
+ await fs.mkdir(destinationPath, { recursive: true });
31
+ await copyDir(sourcePath, destinationPath, replacements, progressFn);
32
+ }
33
+ else if (stats.isFile()) {
34
+ await copyFile(sourcePath, destinationPath, replacements, progressFn);
35
+ }
36
+ }
37
+ }
38
+ async function copyFile(source, destination, replacements, progressFn) {
39
+ let contents = await fs.readFile(source, 'utf-8');
40
+ for (const [key, value] of Object.entries(replacements)) {
41
+ contents = contents.replace(new RegExp(`{{${key}}}`, 'g'), value);
42
+ }
43
+ await fs.writeFile(destination, contents, 'utf-8');
44
+ progressFn(destination);
45
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { initCommand } from './commands/init.js';
3
+ import { rootCommand } from './commands/root.js';
4
+ import { serveCommand } from './commands/serve.js';
5
+ rootCommand.addCommand(serveCommand);
6
+ rootCommand.addCommand(initCommand);
7
+ rootCommand.parse(process.argv);
@@ -0,0 +1,5 @@
1
+ type Flags = {
2
+ port: number;
3
+ };
4
+ export declare function runServer(projectPath: string, flags: Flags): Promise<void>;
5
+ export {};
@@ -0,0 +1,100 @@
1
+ import { spawn } from 'child_process';
2
+ import { readFile } from 'fs/promises';
3
+ import http from 'http';
4
+ import path from 'path';
5
+ import readable from 'readline/promises';
6
+ import serveHandler from 'serve-handler';
7
+ const ansiWhite = '\u001b[37m';
8
+ const ansiYellow = '\u001b[33m';
9
+ const ansiCyan = '\u001b[36m';
10
+ const ansiBold = '\u001b[1m';
11
+ const ansiReset = '\u001b[0m';
12
+ export async function runServer(projectPath, flags) {
13
+ printSplashScreen();
14
+ projectPath = path.resolve(process.cwd(), projectPath);
15
+ const telemetryConfig = await loadConfigFile(projectPath);
16
+ if (!telemetryConfig) {
17
+ console.error('No telemetry configuration found. Are you in the right directory?');
18
+ process.exit(1);
19
+ }
20
+ await serveDevelopmentApplicationHostUI(flags.port, telemetryConfig);
21
+ await serveTelemetryApplication(projectPath, telemetryConfig);
22
+ }
23
+ async function serveDevelopmentApplicationHostUI(port, telemetryConfig) {
24
+ const hostUiPath = await import.meta.resolve('@telemetryos/development-application-host-ui/dist');
25
+ const serveConfig = { public: hostUiPath.replace('file://', '') };
26
+ const server = http.createServer();
27
+ server.on('request', (req, res) => {
28
+ const url = new URL(req.url, `http://${req.headers.origin}`);
29
+ if (url.pathname === '/__tos-config__') {
30
+ res.setHeader('Content-Type', 'application/json');
31
+ res.end(JSON.stringify(telemetryConfig));
32
+ return;
33
+ }
34
+ serveHandler(req, res, serveConfig).catch((err) => {
35
+ console.error('Error handling request:', err);
36
+ res.statusCode = 500;
37
+ res.end('Internal Server Error');
38
+ });
39
+ });
40
+ printServerInfo(port);
41
+ server.listen(port);
42
+ }
43
+ async function serveTelemetryApplication(rootPath, telemetryConfig) {
44
+ var _a;
45
+ if (!((_a = telemetryConfig === null || telemetryConfig === void 0 ? void 0 : telemetryConfig.devServer) === null || _a === void 0 ? void 0 : _a.runCommand))
46
+ return;
47
+ const runCommand = telemetryConfig.devServer.runCommand;
48
+ const binPath = path.join(rootPath, 'node_modules', '.bin');
49
+ const childProcess = spawn(runCommand, {
50
+ shell: true,
51
+ env: { ...process.env, FORCE_COLOR: '1', PATH: `${binPath}:${process.env.PATH}` },
52
+ stdio: ['ignore', 'pipe', 'pipe'],
53
+ cwd: rootPath,
54
+ });
55
+ const stdoutReadline = readable.createInterface({
56
+ input: childProcess.stdout,
57
+ crlfDelay: Infinity,
58
+ });
59
+ const stderrReadline = readable.createInterface({
60
+ input: childProcess.stderr,
61
+ crlfDelay: Infinity,
62
+ });
63
+ stdoutReadline.on('line', (line) => {
64
+ console.log(`[application]: ${line}`);
65
+ });
66
+ stderrReadline.on('line', (line) => {
67
+ console.error(`[application]: ${line}`);
68
+ });
69
+ process.on('exit', () => {
70
+ childProcess.kill();
71
+ });
72
+ }
73
+ async function loadConfigFile(rootPath) {
74
+ const configFilePath = path.join(rootPath, 'telemetry.config.json');
75
+ try {
76
+ const fileContent = await readFile(configFilePath, 'utf-8');
77
+ const config = JSON.parse(fileContent);
78
+ return config;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ function printSplashScreen() {
85
+ console.log(`
86
+
87
+ ${ansiWhite} █ █ █ ${ansiYellow}▄▀▀▀▄ ▄▀▀▀▄
88
+ ${ansiWhite} █ █ █ ${ansiYellow}█ █ █
89
+ ${ansiWhite}▀█▀ ▄▀▀▄ █ ▄▀▀▄ █▀▄▀▄ ▄▀▀▄ ▀█▀ █▄▀ █ █ ${ansiYellow}█ █ ▀▀▀▄
90
+ ${ansiWhite} █ █▀▀▀ █ █▀▀▀ █ █ █ █▀▀▀ █ █ █ █ ${ansiYellow}█ █ █
91
+ ${ansiWhite} ▀▄ ▀▄▄▀ █ ▀▄▄▀ █ █ █ ▀▄▄▀ ▀▄ █ █ ${ansiYellow}▀▄▄▄▀ ▀▄▄▄▀
92
+ ${ansiWhite} ▄▀ ${ansiReset}`);
93
+ }
94
+ function printServerInfo(port) {
95
+ console.log(`
96
+ ╔═══════════════════════════════════════════════════════════╗
97
+ ║ ${ansiBold}Development environment running at: ${ansiCyan}http://localhost:${port}${ansiReset} ║
98
+ ╚═══════════════════════════════════════════════════════════╝
99
+ `);
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetryos/cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "The official TelemetryOS application CLI package. Use it to build applications that run on the TelemetryOS platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "license": "",
26
26
  "repository": "github:TelemetryTV/Application-API",
27
27
  "dependencies": {
28
- "@telemetryos/development-application-host-ui": "^1.2.0",
28
+ "@telemetryos/development-application-host-ui": "^1.4.0",
29
29
  "@types/serve-handler": "^6.1.4",
30
30
  "commander": "^14.0.0",
31
31
  "inquirer": "^12.9.6",
@@ -0,0 +1,7 @@
1
+ # Agent Guidelines
2
+
3
+ This document outlines guidelines for interacting with and developing for AI agents within this project.
4
+
5
+ ## Important Notes
6
+
7
+ * **Claude-specific Guidelines:** If you are working with Claude models, please refer to `CLAUDE.md` for specific guidelines and best practices.
@@ -0,0 +1,625 @@
1
+ # TelemetryOS SDK Reference
2
+
3
+ **Application:** [Your App Name]
4
+ **Purpose:** [What this application does]
5
+
6
+ ## Platform Architecture
7
+
8
+ TelemetryOS applications are web apps that run on digital signage devices. Applications have up to 4 components:
9
+
10
+ 1. **Render** (`/render`) - Content displayed on devices (runs on device in Chrome/iframe)
11
+ 2. **Settings** (`/settings`) - Config UI in Studio admin portal (runs in Studio browser)
12
+ 3. **Workers** (optional) - Background JavaScript (runs on device, no DOM)
13
+ 4. **Containers** (optional) - Docker containers for backend services (runs on device)
14
+
15
+ **Runtime Environment:**
16
+ - Chrome browser (platform-controlled version)
17
+ - Iframe sandbox execution
18
+ - Client-side only (no SSR, no Node.js APIs)
19
+ - Modern web APIs available (Fetch, WebSockets, WebGL, Canvas)
20
+ - External APIs require CORS proxy
21
+
22
+ **Communication:**
23
+ - Settings and Render share instance storage
24
+ - Settings saves config → Render subscribes to config
25
+ - Device storage only available in Render (not Settings)
26
+
27
+ ## Project Structure
28
+
29
+ ```
30
+ project-root/
31
+ ├── telemetry.config.json # Platform configuration
32
+ ├── package.json
33
+ ├── tsconfig.json
34
+ ├── vite.config.ts
35
+ ├── index.html
36
+ └── src/
37
+ ├── main.tsx # Entry point (configure SDK here)
38
+ ├── App.tsx # Routing logic
39
+ ├── views/
40
+ │ ├── Settings.tsx # /settings mount point
41
+ │ └── Render.tsx # /render mount point
42
+ ├── components/ # Reusable components
43
+ ├── hooks/ # Custom React hooks
44
+ ├── types/ # TypeScript interfaces
45
+ └── utils/ # Helper functions
46
+ ```
47
+
48
+ ## Configuration Files
49
+
50
+ ### telemetry.config.json (project root)
51
+ ```json
52
+ {
53
+ "name": "app-name",
54
+ "version": "1.0.0",
55
+ "mountPoints": {
56
+ "render": "/render",
57
+ "settings": "/settings"
58
+ },
59
+ "devServer": {
60
+ "runCommand": "vite --port 3000",
61
+ "url": "http://localhost:3000"
62
+ }
63
+ }
64
+ ```
65
+
66
+ ### package.json scripts
67
+ ```json
68
+ {
69
+ "scripts": {
70
+ "dev": "vite",
71
+ "build": "tsc && vite build",
72
+ "preview": "vite preview"
73
+ },
74
+ "dependencies": {
75
+ "@telemetryos/sdk": "latest",
76
+ "react": "latest",
77
+ "react-dom": "latest"
78
+ },
79
+ "devDependencies": {
80
+ "@types/react": "latest",
81
+ "@types/react-dom": "latest",
82
+ "@vitejs/plugin-react": "latest",
83
+ "typescript": "latest",
84
+ "vite": "latest"
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## Complete File Implementations
90
+
91
+ ### src/main.tsx (Entry Point)
92
+ ```typescript
93
+ import { configure } from '@telemetryos/sdk';
94
+ import React from 'react';
95
+ import ReactDOM from 'react-dom/client';
96
+ import App from './App';
97
+ import './index.css';
98
+
99
+ // Configure SDK ONCE before React renders
100
+ // Name must match telemetry.config.json
101
+ configure('app-name');
102
+
103
+ ReactDOM.createRoot(document.getElementById('root')!).render(
104
+ <React.StrictMode>
105
+ <App />
106
+ </React.StrictMode>
107
+ );
108
+ ```
109
+
110
+ ### src/App.tsx (Routing)
111
+ ```typescript
112
+ import Settings from './views/Settings';
113
+ import Render from './views/Render';
114
+
115
+ export default function App() {
116
+ const path = window.location.pathname;
117
+
118
+ if (path === '/settings') return <Settings />;
119
+ if (path === '/render') return <Render />;
120
+
121
+ return <div>Invalid mount point: {path}</div>;
122
+ }
123
+ ```
124
+
125
+ ### src/views/Settings.tsx (Complete Reference)
126
+ ```typescript
127
+ import { useEffect, useState, FormEvent } from 'react';
128
+ import { store } from '@telemetryos/sdk';
129
+
130
+ interface Config {
131
+ city: string;
132
+ units: 'celsius' | 'fahrenheit';
133
+ }
134
+
135
+ export default function Settings() {
136
+ const [config, setConfig] = useState<Config>({ city: '', units: 'celsius' });
137
+ const [loading, setLoading] = useState(false);
138
+ const [error, setError] = useState<string | null>(null);
139
+
140
+ // Load existing config on mount
141
+ useEffect(() => {
142
+ store().instance.get<Config>('config')
143
+ .then(saved => { if (saved) setConfig(saved); })
144
+ .catch(err => setError(err.message));
145
+ }, []);
146
+
147
+ const handleSave = async (e: FormEvent) => {
148
+ e.preventDefault();
149
+ setLoading(true);
150
+ setError(null);
151
+
152
+ try {
153
+ const success = await store().instance.set('config', config);
154
+ if (!success) throw new Error('Storage operation failed');
155
+ } catch (err) {
156
+ setError(err instanceof Error ? err.message : 'Unknown error');
157
+ } finally {
158
+ setLoading(false);
159
+ }
160
+ };
161
+
162
+ return (
163
+ <div>
164
+ <h2>Settings</h2>
165
+ {error && <div style={{ color: 'red' }}>{error}</div>}
166
+ <form onSubmit={handleSave}>
167
+ <div>
168
+ <label htmlFor="city">City:</label>
169
+ <input
170
+ id="city"
171
+ value={config.city}
172
+ onChange={(e) => setConfig({ ...config, city: e.target.value })}
173
+ required
174
+ />
175
+ </div>
176
+ <div>
177
+ <label htmlFor="units">Units:</label>
178
+ <select
179
+ id="units"
180
+ value={config.units}
181
+ onChange={(e) => setConfig({ ...config, units: e.target.value as Config['units'] })}
182
+ >
183
+ <option value="celsius">Celsius</option>
184
+ <option value="fahrenheit">Fahrenheit</option>
185
+ </select>
186
+ </div>
187
+ <button type="submit" disabled={loading}>
188
+ {loading ? 'Saving...' : 'Save'}
189
+ </button>
190
+ </form>
191
+ </div>
192
+ );
193
+ }
194
+ ```
195
+
196
+ ### src/views/Render.tsx (Complete Reference)
197
+ ```typescript
198
+ import { useEffect, useState } from 'react';
199
+ import { store, proxy } from '@telemetryos/sdk';
200
+
201
+ interface Config {
202
+ city: string;
203
+ units: 'celsius' | 'fahrenheit';
204
+ }
205
+
206
+ interface WeatherData {
207
+ temperature: number;
208
+ conditions: string;
209
+ }
210
+
211
+ export default function Render() {
212
+ const [config, setConfig] = useState<Config | null>(null);
213
+ const [weather, setWeather] = useState<WeatherData | null>(null);
214
+ const [loading, setLoading] = useState(false);
215
+ const [error, setError] = useState<string | null>(null);
216
+
217
+ // Subscribe to config changes from Settings
218
+ useEffect(() => {
219
+ store().instance.get<Config>('config').then(setConfig);
220
+
221
+ const unsubscribe = store().instance.subscribe('config', (newConfig: Config) => {
222
+ setConfig(newConfig);
223
+ });
224
+
225
+ return () => unsubscribe();
226
+ }, []);
227
+
228
+ // Fetch weather when config changes
229
+ useEffect(() => {
230
+ if (!config?.city) return;
231
+
232
+ const fetchWeather = async () => {
233
+ setLoading(true);
234
+ setError(null);
235
+
236
+ try {
237
+ const response = await proxy().fetch(
238
+ `https://api.example.com/weather?city=${config.city}&units=${config.units}`
239
+ );
240
+
241
+ if (!response.ok) throw new Error(`API error: ${response.status}`);
242
+
243
+ const data = await response.json();
244
+ setWeather({ temperature: data.temp, conditions: data.conditions });
245
+
246
+ // Cache for offline
247
+ await store().device.set('cached', { data, timestamp: Date.now() });
248
+ } catch (err) {
249
+ setError(err instanceof Error ? err.message : 'Unknown error');
250
+
251
+ // Try cached data
252
+ const cached = await store().device.get<any>('cached');
253
+ if (cached) setWeather(cached.data);
254
+ } finally {
255
+ setLoading(false);
256
+ }
257
+ };
258
+
259
+ fetchWeather();
260
+ }, [config]);
261
+
262
+ // States
263
+ if (!config) return <div>Configure in Settings</div>;
264
+ if (loading && !weather) return <div>Loading...</div>;
265
+ if (error && !weather) return <div>Error: {error}</div>;
266
+
267
+ return (
268
+ <div>
269
+ <h1>{config.city}</h1>
270
+ <div>{weather?.temperature}°{config.units === 'celsius' ? 'C' : 'F'}</div>
271
+ <div>{weather?.conditions}</div>
272
+ {error && <div style={{ color: 'orange' }}>Showing cached data</div>}
273
+ </div>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ## SDK API Reference
279
+
280
+ Import from `@telemetryos/sdk`.
281
+
282
+ ### Initialization
283
+
284
+ ```typescript
285
+ configure(applicationName: string): void
286
+ ```
287
+ - Call once in main.tsx before React renders
288
+ - Name must match telemetry.config.json
289
+ - Throws if called multiple times
290
+
291
+ ### Storage API
292
+
293
+ **Type Signatures:**
294
+ ```typescript
295
+ store().application.set(key: string, value: any): Promise<boolean>
296
+ store().application.get<T>(key: string): Promise<T | null>
297
+ store().application.subscribe(key: string, handler: (value: any) => void): () => void
298
+ store().application.delete(key: string): Promise<boolean>
299
+ store().application.keys(): Promise<string[]>
300
+
301
+ // Same methods for instance, device, shared(namespace)
302
+ ```
303
+
304
+ **Four Scopes:**
305
+
306
+ 1. **application** - Shared across all instances of app in account
307
+ ```typescript
308
+ await store().application.set('companyLogo', 'https://...');
309
+ const logo = await store().application.get<string>('companyLogo');
310
+ ```
311
+
312
+ 2. **instance** - This specific app instance (Settings ↔ Render communication)
313
+ ```typescript
314
+ // Settings saves
315
+ await store().instance.set('config', { city: 'NYC' });
316
+
317
+ // Render subscribes
318
+ const unsubscribe = store().instance.subscribe('config', (newConfig) => {
319
+ updateDisplay(newConfig);
320
+ });
321
+ // Later: unsubscribe();
322
+ ```
323
+
324
+ 3. **device** - This physical device only (NOT available in Settings)
325
+ ```typescript
326
+ // Only in Render mount point
327
+ await store().device.set('cache', data);
328
+ const cached = await store().device.get<CacheType>('cache');
329
+ ```
330
+
331
+ 4. **shared(namespace)** - Inter-app communication
332
+ ```typescript
333
+ // App A publishes
334
+ await store().shared('weather').set('temp', '72°F');
335
+
336
+ // App B subscribes
337
+ store().shared('weather').subscribe('temp', (temp) => console.log(temp));
338
+ ```
339
+
340
+ **Constraints:**
341
+ - All operations timeout after 30 seconds (throws Error)
342
+ - Returns Promise<boolean> for set/delete (true = success)
343
+ - Returns Promise<T | null> for get
344
+ - subscribe() returns unsubscribe function
345
+ - Device scope throws Error in Settings mount point
346
+
347
+ ### Proxy API
348
+
349
+ ```typescript
350
+ proxy().fetch(url: string, options?: RequestInit): Promise<Response>
351
+ ```
352
+
353
+ - Same interface as standard fetch()
354
+ - Use for ALL external API calls to avoid CORS errors
355
+ - Returns standard Response object
356
+ - Handles CORS server-side
357
+
358
+ **Example:**
359
+ ```typescript
360
+ import { proxy } from '@telemetryos/sdk';
361
+
362
+ const response = await proxy().fetch('https://api.example.com/data');
363
+ const json = await response.json();
364
+ ```
365
+
366
+ ### Media API
367
+
368
+ ```typescript
369
+ media().getAllByTag(tag: string): Promise<MediaContent[]>
370
+ media().getById(id: string): Promise<MediaContent | null>
371
+
372
+ interface MediaContent {
373
+ id: string;
374
+ url: string;
375
+ type: 'image' | 'video';
376
+ tags: string[];
377
+ metadata: Record<string, any>;
378
+ }
379
+ ```
380
+
381
+ ### Playlist API
382
+
383
+ ```typescript
384
+ playlist().nextPage(): Promise<boolean>
385
+ playlist().previousPage(): Promise<boolean>
386
+ playlist().jumpToPage(index: number): Promise<boolean>
387
+ ```
388
+
389
+ ### Overrides API
390
+
391
+ ```typescript
392
+ overrides().setOverride(id: string): Promise<boolean>
393
+ overrides().clearOverride(): Promise<boolean>
394
+ ```
395
+
396
+ Note: Override IDs must be pre-configured in Freeform Editor.
397
+
398
+ ### Platform Information
399
+
400
+ ```typescript
401
+ accounts().getCurrent(): Promise<Account>
402
+ users().getCurrent(): Promise<User>
403
+ devices().getCurrent(): Promise<Device> // Render only
404
+ ```
405
+
406
+ ## Hard Constraints
407
+
408
+ **These cause runtime errors:**
409
+
410
+ 1. **Device storage in Settings**
411
+ - Settings runs in Studio browser, not on devices
412
+ - `store().device.*` throws Error in Settings
413
+ - Use `store().instance` or `store().application` instead
414
+
415
+ 2. **External API without proxy**
416
+ - Direct `fetch()` to external domains fails with CORS error
417
+ - Must use `proxy().fetch()` for all external requests
418
+
419
+ 3. **Missing configure()**
420
+ - SDK methods throw "SDK not configured" Error
421
+ - Call `configure()` once in main.tsx before React renders
422
+
423
+ 4. **Subscription memory leaks**
424
+ - `subscribe()` returns unsubscribe function
425
+ - Must call unsubscribe on component unmount
426
+ - Return unsubscribe from useEffect cleanup
427
+
428
+ 5. **Timeout errors**
429
+ - All SDK operations timeout after 30 seconds
430
+ - Throws Error with message containing 'timeout'
431
+ - Handle with try/catch
432
+
433
+ ## TypeScript Patterns
434
+
435
+ **Define interfaces for all configs and data:**
436
+ ```typescript
437
+ interface AppConfig {
438
+ city: string;
439
+ units: 'celsius' | 'fahrenheit';
440
+ refreshInterval: number;
441
+ }
442
+
443
+ const config = await store().instance.get<AppConfig>('config');
444
+ if (config) {
445
+ console.log(config.city); // TypeScript knows this exists
446
+ }
447
+ ```
448
+
449
+ **Component with proper types:**
450
+ ```typescript
451
+ interface Props {
452
+ data: WeatherData;
453
+ onRefresh: () => void;
454
+ }
455
+
456
+ export default function WeatherCard({ data, onRefresh }: Props) {
457
+ return <div>{data.temperature}</div>;
458
+ }
459
+ ```
460
+
461
+ ## React Patterns
462
+
463
+ **Error handling:**
464
+ ```typescript
465
+ const [error, setError] = useState<string | null>(null);
466
+
467
+ try {
468
+ await store().instance.set('key', value);
469
+ } catch (err) {
470
+ setError(err instanceof Error ? err.message : 'Unknown error');
471
+ }
472
+ ```
473
+
474
+ **Loading states:**
475
+ ```typescript
476
+ const [loading, setLoading] = useState(false);
477
+
478
+ const handleAction = async () => {
479
+ setLoading(true);
480
+ try {
481
+ await someAsyncOperation();
482
+ } finally {
483
+ setLoading(false);
484
+ }
485
+ };
486
+ ```
487
+
488
+ **Subscription cleanup:**
489
+ ```typescript
490
+ useEffect(() => {
491
+ const unsubscribe = store().instance.subscribe('key', handler);
492
+ return () => unsubscribe();
493
+ }, []);
494
+ ```
495
+
496
+ **Empty deps for mount-only effects:**
497
+ ```typescript
498
+ useEffect(() => {
499
+ // Runs once on mount
500
+ store().instance.get('config').then(setConfig);
501
+ }, []); // Empty deps array
502
+ ```
503
+
504
+ ## Code Style
505
+
506
+ **Naming:**
507
+ - Components: PascalCase (`WeatherCard.tsx`)
508
+ - Functions: camelCase (`fetchWeatherData`)
509
+ - Constants: UPPER_SNAKE_CASE (`API_BASE_URL`)
510
+ - Interfaces: PascalCase (`WeatherData`, `AppConfig`)
511
+
512
+ **Imports order:**
513
+ ```typescript
514
+ // 1. SDK imports
515
+ import { configure, store, proxy } from '@telemetryos/sdk';
516
+
517
+ // 2. React imports
518
+ import { useEffect, useState } from 'react';
519
+
520
+ // 3. Local imports
521
+ import WeatherCard from '@/components/WeatherCard';
522
+ import type { WeatherData } from '@/types';
523
+ ```
524
+
525
+ **TypeScript:**
526
+ - Use strict mode
527
+ - Define interfaces for all configs and data
528
+ - Use generics with storage: `get<Type>(key)`
529
+ - Prefer `interface` over `type` for objects
530
+
531
+ **React:**
532
+ - Functional components only
533
+ - Use hooks (useState, useEffect, useMemo, useCallback)
534
+ - Implement loading, error, empty states
535
+ - Clean up subscriptions in useEffect return
536
+
537
+ ## Development Commands
538
+
539
+ ```bash
540
+ # Install dependencies
541
+ npm install
542
+
543
+ # Start local dev server
544
+ tos serve
545
+ # Or: npm run dev
546
+
547
+ # Build for production
548
+ npm run build
549
+
550
+ # Type check
551
+ tsc --noEmit
552
+ ```
553
+
554
+ **Local testing:**
555
+ - Settings: http://localhost:3000/settings
556
+ - Render: http://localhost:3000/render
557
+
558
+ **Deployment:**
559
+ ```bash
560
+ git add .
561
+ git commit -m "Description"
562
+ git push origin main
563
+ # GitHub integration auto-deploys
564
+ ```
565
+
566
+ ## Common Errors
567
+
568
+ **"SDK not configured"**
569
+ → Call `configure('app-name')` in main.tsx before React renders
570
+
571
+ **"device storage not available"**
572
+ → Using `store().device` in Settings - use `store().instance` instead
573
+
574
+ **CORS error**
575
+ → Using direct `fetch()` - use `proxy().fetch()` instead
576
+
577
+ **"Request timeout"**
578
+ → SDK operation exceeded 30 seconds - handle with try/catch
579
+
580
+ **Render not updating**
581
+ → Missing subscription - use `store().instance.subscribe()` in Render
582
+
583
+ **Memory leak**
584
+ → Not returning unsubscribe from useEffect
585
+
586
+ ## Project-Specific Context
587
+
588
+ [Add your project details here:]
589
+
590
+ **Application Name:** [Your app name]
591
+ **External APIs:**
592
+ - [API name]: [endpoint]
593
+ - Authentication: [method]
594
+ - Rate limits: [limits]
595
+
596
+ **Custom Components:**
597
+ - [ComponentName]: [purpose]
598
+ - Location: [path]
599
+ - Props: [interface]
600
+
601
+ **Business Logic:**
602
+ - [Key algorithms or calculations]
603
+ - [Data transformation rules]
604
+
605
+ ## Technical References
606
+
607
+ **SDK API Documentation:**
608
+ - [Storage API](https://docs.telemetryos.com/docs/storage-methods) - Complete storage scope reference
609
+ - [Platform API](https://docs.telemetryos.com/docs/platform-methods) - Proxy, media, accounts, users, devices
610
+ - [Playlist API](https://docs.telemetryos.com/docs/playlist-methods) - Page navigation methods
611
+ - [Overrides API](https://docs.telemetryos.com/docs/overrides-methods) - Dynamic content control
612
+ - [Client API](https://docs.telemetryos.com/docs/client-methods) - Device client interactions
613
+ - [Media API](https://docs.telemetryos.com/docs/media-methods) - Media content queries
614
+
615
+ **Critical Context:**
616
+ - [CORS Guide](https://docs.telemetryos.com/docs/cors) - Why proxy().fetch() is required
617
+ - [Mount Points](https://docs.telemetryos.com/docs/mount-points) - /render vs /settings execution
618
+ - [Languages Supported](https://docs.telemetryos.com/docs/languages-supported) - Runtime environment constraints
619
+ - [Configuration](https://docs.telemetryos.com/docs/configuration) - telemetry.config.json schema
620
+ - [Workers](https://docs.telemetryos.com/docs/workers) - Background JavaScript patterns
621
+ - [Containers](https://docs.telemetryos.com/docs/containers) - Docker integration patterns
622
+
623
+ **Code Examples:**
624
+ - [Code Examples](https://docs.telemetryos.com/docs/code-examples) - Real-world implementations
625
+ - [LLMS.txt](https://docs.telemetryos.com/llms.txt) - Complete documentation index