@telemetryos/cli 1.7.4 → 1.8.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.
Files changed (25) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/commands/init.js +11 -0
  3. package/dist/services/generate-application.d.ts +1 -0
  4. package/dist/services/generate-application.js +127 -4
  5. package/dist/utils/validate-project-name.d.ts +19 -0
  6. package/dist/utils/validate-project-name.js +44 -0
  7. package/package.json +2 -2
  8. package/templates/vite-react-typescript/CLAUDE.md +68 -1244
  9. package/templates/vite-react-typescript/_claude/settings.local.json +17 -0
  10. package/templates/vite-react-typescript/_claude/skills/tos-architecture/SKILL.md +313 -0
  11. package/templates/vite-react-typescript/_claude/skills/tos-debugging/SKILL.md +299 -0
  12. package/templates/vite-react-typescript/_claude/skills/tos-media-api/SKILL.md +335 -0
  13. package/templates/vite-react-typescript/_claude/skills/tos-proxy-fetch/SKILL.md +319 -0
  14. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +332 -0
  15. package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +252 -0
  16. package/templates/vite-react-typescript/_claude/skills/tos-settings-ui/SKILL.md +636 -0
  17. package/templates/vite-react-typescript/_claude/skills/tos-store-sync/SKILL.md +359 -0
  18. package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +384 -0
  19. package/templates/vite-react-typescript/src/hooks/store.ts +3 -3
  20. package/templates/vite-react-typescript/src/index.css +0 -1
  21. package/templates/vite-react-typescript/src/views/Render.css +2 -1
  22. package/templates/vite-react-typescript/src/views/Render.tsx +2 -3
  23. package/templates/vite-react-typescript/src/views/Settings.tsx +2 -3
  24. package/templates/vite-react-typescript/AGENTS.md +0 -7
  25. /package/templates/vite-react-typescript/{gitignore → _gitignore} +0 -0
@@ -0,0 +1,335 @@
1
+ ---
2
+ name: tos-media-api
3
+ description: Access TelemetryOS Media Library for images, videos, and files. Use when building apps that display user-uploaded media content.
4
+ ---
5
+
6
+ # TelemetryOS Media API
7
+
8
+ The Media API provides access to content uploaded to the TelemetryOS Media Library. Users manage their media through Studio and your app can display it.
9
+
10
+ ## Quick Reference
11
+
12
+ ```typescript
13
+ import { media } from '@telemetryos/sdk'
14
+
15
+ // Get all folders
16
+ const folders = await media().getAllFolders()
17
+
18
+ // Get content in a folder
19
+ const content = await media().getAllByFolderId('folder-id')
20
+
21
+ // Get content by tag
22
+ const tagged = await media().getAllByTag('banner')
23
+
24
+ // Get single item by ID
25
+ const item = await media().getById('content-id')
26
+ ```
27
+
28
+ ## Response Types
29
+
30
+ ### MediaFolder
31
+
32
+ ```typescript
33
+ interface MediaFolder {
34
+ id: string
35
+ parentId: string // Parent folder ID (empty for root)
36
+ name: string // Folder name
37
+ size: number // Number of items
38
+ default: boolean // Is this the default folder?
39
+ createdAt: Date
40
+ updatedAt: Date
41
+ }
42
+ ```
43
+
44
+ ### MediaContent
45
+
46
+ ```typescript
47
+ interface MediaContent {
48
+ id: string
49
+ contentFolderId: string // Parent folder
50
+ contentType: string // MIME type (image/jpeg, video/mp4, etc.)
51
+ name: string // File name
52
+ description: string // User description
53
+ thumbnailUrl: string // Thumbnail for preview
54
+ keys: string[] // Storage keys
55
+ publicUrls: string[] // CDN URLs for display
56
+ hidden: boolean // Hidden from listings
57
+ validFrom?: Date // Content scheduling
58
+ validTo?: Date // Content scheduling
59
+ createdAt: Date
60
+ updatedAt: Date
61
+ }
62
+ ```
63
+
64
+ ## Common Patterns
65
+
66
+ ### Folder Picker in Settings
67
+
68
+ Let users select a folder, then display its contents in Render.
69
+
70
+ ```typescript
71
+ // hooks/store.ts
72
+ import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
73
+
74
+ export const useFolderIdState = createUseInstanceStoreState<string>('folderId', '')
75
+ ```
76
+
77
+ ```typescript
78
+ // views/Settings.tsx
79
+ import { useEffect, useState } from 'react'
80
+ import { media } from '@telemetryos/sdk'
81
+ import {
82
+ SettingsContainer,
83
+ SettingsField,
84
+ SettingsLabel,
85
+ SettingsSelectFrame,
86
+ } from '@telemetryos/sdk/react'
87
+ import { useFolderIdState } from '../hooks/store'
88
+
89
+ interface Folder {
90
+ id: string
91
+ name: string
92
+ }
93
+
94
+ export default function Settings() {
95
+ const [isLoading, folderId, setFolderId] = useFolderIdState()
96
+ const [folders, setFolders] = useState<Folder[]>([])
97
+
98
+ useEffect(() => {
99
+ media().getAllFolders().then(setFolders)
100
+ }, [])
101
+
102
+ return (
103
+ <SettingsContainer>
104
+ <SettingsField>
105
+ <SettingsLabel>Media Folder</SettingsLabel>
106
+ <SettingsSelectFrame>
107
+ <select
108
+ disabled={isLoading || folders.length === 0}
109
+ value={folderId}
110
+ onChange={(e) => setFolderId(e.target.value)}
111
+ >
112
+ <option value="">Select a folder...</option>
113
+ {folders.map(folder => (
114
+ <option key={folder.id} value={folder.id}>
115
+ {folder.name}
116
+ </option>
117
+ ))}
118
+ </select>
119
+ </SettingsSelectFrame>
120
+ </SettingsField>
121
+ </SettingsContainer>
122
+ )
123
+ }
124
+ ```
125
+
126
+ ### Image Gallery in Render
127
+
128
+ ```typescript
129
+ // views/Render.tsx
130
+ import { useEffect, useState } from 'react'
131
+ import { media } from '@telemetryos/sdk'
132
+ import { useFolderIdState } from '../hooks/store'
133
+
134
+ interface MediaItem {
135
+ id: string
136
+ name: string
137
+ url: string
138
+ thumbnailUrl: string
139
+ }
140
+
141
+ export default function Render() {
142
+ const [isLoading, folderId] = useFolderIdState()
143
+ const [items, setItems] = useState<MediaItem[]>([])
144
+ const [loading, setLoading] = useState(false)
145
+
146
+ useEffect(() => {
147
+ if (isLoading || !folderId) return
148
+
149
+ const fetchMedia = async () => {
150
+ setLoading(true)
151
+ try {
152
+ const content = await media().getAllByFolderId(folderId)
153
+
154
+ // Filter to images only
155
+ const images = content
156
+ .filter(item => item.contentType.startsWith('image/'))
157
+ .map(item => ({
158
+ id: item.id,
159
+ name: item.name,
160
+ url: item.publicUrls[0],
161
+ thumbnailUrl: item.thumbnailUrl,
162
+ }))
163
+
164
+ setItems(images)
165
+ } catch (err) {
166
+ console.error('Failed to load media:', err)
167
+ } finally {
168
+ setLoading(false)
169
+ }
170
+ }
171
+
172
+ fetchMedia()
173
+ }, [folderId, isLoading])
174
+
175
+ if (isLoading) return <div>Loading config...</div>
176
+ if (!folderId) return <div>Select a folder in Settings</div>
177
+ if (loading) return <div>Loading images...</div>
178
+ if (items.length === 0) return <div>No images in folder</div>
179
+
180
+ return (
181
+ <div className="gallery">
182
+ {items.map(item => (
183
+ <img
184
+ key={item.id}
185
+ src={item.url}
186
+ alt={item.name}
187
+ loading="lazy"
188
+ />
189
+ ))}
190
+ </div>
191
+ )
192
+ }
193
+ ```
194
+
195
+ ### Video Player
196
+
197
+ ```typescript
198
+ // views/Render.tsx
199
+ import { useEffect, useState } from 'react'
200
+ import { media } from '@telemetryos/sdk'
201
+ import { useVideoIdState } from '../hooks/store'
202
+
203
+ export default function Render() {
204
+ const [isLoading, videoId] = useVideoIdState()
205
+ const [videoUrl, setVideoUrl] = useState<string | null>(null)
206
+
207
+ useEffect(() => {
208
+ if (isLoading || !videoId) return
209
+
210
+ media().getById(videoId).then(item => {
211
+ if (item.contentType.startsWith('video/')) {
212
+ setVideoUrl(item.publicUrls[0])
213
+ }
214
+ })
215
+ }, [videoId, isLoading])
216
+
217
+ if (isLoading) return <div>Loading...</div>
218
+ if (!videoUrl) return <div>No video selected</div>
219
+
220
+ return (
221
+ <video
222
+ src={videoUrl}
223
+ autoPlay
224
+ loop
225
+ muted
226
+ playsInline
227
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
228
+ />
229
+ )
230
+ }
231
+ ```
232
+
233
+ ### Content Picker by Tag
234
+
235
+ ```typescript
236
+ // Settings - let user select from tagged content
237
+ const [items, setItems] = useState<MediaContent[]>([])
238
+
239
+ useEffect(() => {
240
+ media().getAllByTag('logo').then(setItems)
241
+ }, [])
242
+ ```
243
+
244
+ ### Slideshow with Auto-Advance
245
+
246
+ ```typescript
247
+ import { useEffect, useState } from 'react'
248
+ import { media } from '@telemetryos/sdk'
249
+ import { useFolderIdState, useIntervalState } from '../hooks/store'
250
+
251
+ export default function Render() {
252
+ const [, folderId] = useFolderIdState()
253
+ const [, interval] = useIntervalState() // seconds
254
+
255
+ const [images, setImages] = useState<string[]>([])
256
+ const [currentIndex, setCurrentIndex] = useState(0)
257
+
258
+ // Load images
259
+ useEffect(() => {
260
+ if (!folderId) return
261
+
262
+ media().getAllByFolderId(folderId).then(content => {
263
+ const urls = content
264
+ .filter(item => item.contentType.startsWith('image/'))
265
+ .map(item => item.publicUrls[0])
266
+ setImages(urls)
267
+ })
268
+ }, [folderId])
269
+
270
+ // Auto-advance
271
+ useEffect(() => {
272
+ if (images.length <= 1) return
273
+
274
+ const timer = setInterval(() => {
275
+ setCurrentIndex(i => (i + 1) % images.length)
276
+ }, interval * 1000)
277
+
278
+ return () => clearInterval(timer)
279
+ }, [images.length, interval])
280
+
281
+ if (images.length === 0) return <div>No images</div>
282
+
283
+ return (
284
+ <img
285
+ src={images[currentIndex]}
286
+ alt={`Slide ${currentIndex + 1}`}
287
+ style={{ width: '100%', height: '100%', objectFit: 'contain' }}
288
+ />
289
+ )
290
+ }
291
+ ```
292
+
293
+ ## Content Type Filtering
294
+
295
+ ```typescript
296
+ const content = await media().getAllByFolderId(folderId)
297
+
298
+ // Images only
299
+ const images = content.filter(item => item.contentType.startsWith('image/'))
300
+
301
+ // Videos only
302
+ const videos = content.filter(item => item.contentType.startsWith('video/'))
303
+
304
+ // PDFs only
305
+ const pdfs = content.filter(item => item.contentType === 'application/pdf')
306
+
307
+ // Specific formats
308
+ const jpegs = content.filter(item => item.contentType === 'image/jpeg')
309
+ const mp4s = content.filter(item => item.contentType === 'video/mp4')
310
+ ```
311
+
312
+ ## Scheduling Support
313
+
314
+ Media items can have valid date ranges:
315
+
316
+ ```typescript
317
+ const content = await media().getAllByFolderId(folderId)
318
+
319
+ const now = new Date()
320
+ const activeContent = content.filter(item => {
321
+ // Check if within valid date range
322
+ if (item.validFrom && new Date(item.validFrom) > now) return false
323
+ if (item.validTo && new Date(item.validTo) < now) return false
324
+ return true
325
+ })
326
+ ```
327
+
328
+ ## Tips
329
+
330
+ 1. **Use publicUrls[0]** - First URL is the primary CDN URL
331
+ 2. **Use thumbnailUrl for previews** - Smaller, faster loading
332
+ 3. **Filter by contentType** - Ensure you're displaying compatible content
333
+ 4. **Handle empty folders** - Show appropriate message when no content
334
+ 5. **Lazy load images** - Use `loading="lazy"` for galleries
335
+ 6. **Respect hidden flag** - Filter out hidden items unless intentional
@@ -0,0 +1,319 @@
1
+ ---
2
+ name: tos-proxy-fetch
3
+ description: REQUIRED for external API calls in TelemetryOS. MUST invoke BEFORE using proxy().fetch() or adding any third-party API integration. Contains CORS workaround patterns, useEffect dependencies, refresh intervals, and error handling.
4
+ ---
5
+
6
+ # TelemetryOS Proxy Fetch
7
+
8
+ When external APIs don't include CORS headers, browsers block requests from your app. The TelemetryOS proxy solves this by routing requests through the platform.
9
+
10
+ ## When to Use
11
+
12
+ ### Use proxy().fetch() when:
13
+ - API returns CORS error in browser console
14
+ - API doesn't include `Access-Control-Allow-Origin` header
15
+ - You need to call APIs that weren't designed for browser use
16
+
17
+ ### Use regular fetch() when:
18
+ - API includes CORS headers
19
+ - API is designed for browser/client-side use
20
+ - You want to use the player's advanced caching (regular fetch has better caching on devices)
21
+
22
+ ## Quick Reference
23
+
24
+ ```typescript
25
+ import { proxy } from '@telemetryos/sdk'
26
+
27
+ // Simple GET
28
+ const response = await proxy().fetch('https://api.example.com/data')
29
+ const data = await response.json()
30
+
31
+ // With headers
32
+ const response = await proxy().fetch('https://api.example.com/data', {
33
+ headers: {
34
+ 'Authorization': 'Bearer YOUR_API_KEY',
35
+ 'Content-Type': 'application/json',
36
+ }
37
+ })
38
+
39
+ // POST request
40
+ const response = await proxy().fetch('https://api.example.com/data', {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ body: JSON.stringify({ key: 'value' })
46
+ })
47
+ ```
48
+
49
+ ## Complete Example
50
+
51
+ ### Settings (API Key + Endpoint Config)
52
+
53
+ ```typescript
54
+ // hooks/store.ts
55
+ import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
56
+
57
+ export const useApiKeyState = createUseInstanceStoreState<string>('apiKey', '')
58
+ export const useEndpointState = createUseInstanceStoreState<string>('endpoint', '')
59
+ ```
60
+
61
+ ```typescript
62
+ // views/Settings.tsx
63
+ import {
64
+ SettingsContainer,
65
+ SettingsField,
66
+ SettingsLabel,
67
+ SettingsInputFrame,
68
+ } from '@telemetryos/sdk/react'
69
+ import { useApiKeyState, useEndpointState } from '../hooks/store'
70
+
71
+ export default function Settings() {
72
+ const [isLoadingKey, apiKey, setApiKey] = useApiKeyState()
73
+ const [isLoadingEndpoint, endpoint, setEndpoint] = useEndpointState()
74
+
75
+ return (
76
+ <SettingsContainer>
77
+ <SettingsField>
78
+ <SettingsLabel>API Key</SettingsLabel>
79
+ <SettingsInputFrame>
80
+ <input
81
+ type="password"
82
+ placeholder="Enter API key..."
83
+ disabled={isLoadingKey}
84
+ value={apiKey}
85
+ onChange={(e) => setApiKey(e.target.value)}
86
+ />
87
+ </SettingsInputFrame>
88
+ </SettingsField>
89
+
90
+ <SettingsField>
91
+ <SettingsLabel>API Endpoint</SettingsLabel>
92
+ <SettingsInputFrame>
93
+ <input
94
+ type="url"
95
+ placeholder="https://api.example.com/data"
96
+ disabled={isLoadingEndpoint}
97
+ value={endpoint}
98
+ onChange={(e) => setEndpoint(e.target.value)}
99
+ />
100
+ </SettingsInputFrame>
101
+ </SettingsField>
102
+ </SettingsContainer>
103
+ )
104
+ }
105
+ ```
106
+
107
+ ### Render (Data Display)
108
+
109
+ ```typescript
110
+ // views/Render.tsx
111
+ import { useEffect, useState } from 'react'
112
+ import { proxy } from '@telemetryos/sdk'
113
+ import { useApiKeyState, useEndpointState } from '../hooks/store'
114
+
115
+ interface ApiData {
116
+ // Define your API response type
117
+ items: Array<{
118
+ id: string
119
+ name: string
120
+ value: number
121
+ }>
122
+ }
123
+
124
+ export default function Render() {
125
+ const [isLoadingKey, apiKey] = useApiKeyState()
126
+ const [isLoadingEndpoint, endpoint] = useEndpointState()
127
+
128
+ const [data, setData] = useState<ApiData | null>(null)
129
+ const [loading, setLoading] = useState(false)
130
+ const [error, setError] = useState<string | null>(null)
131
+
132
+ useEffect(() => {
133
+ if (isLoadingKey || isLoadingEndpoint || !apiKey || !endpoint) return
134
+
135
+ const fetchData = async () => {
136
+ setLoading(true)
137
+ setError(null)
138
+
139
+ try {
140
+ const response = await proxy().fetch(endpoint, {
141
+ headers: {
142
+ 'Authorization': `Bearer ${apiKey}`,
143
+ 'Content-Type': 'application/json',
144
+ }
145
+ })
146
+
147
+ if (!response.ok) {
148
+ throw new Error(`API error: ${response.status} ${response.statusText}`)
149
+ }
150
+
151
+ const json = await response.json()
152
+ setData(json)
153
+ } catch (err) {
154
+ setError(err instanceof Error ? err.message : 'Unknown error')
155
+ } finally {
156
+ setLoading(false)
157
+ }
158
+ }
159
+
160
+ fetchData()
161
+
162
+ // Refresh every 5 minutes
163
+ const interval = setInterval(fetchData, 5 * 60 * 1000)
164
+ return () => clearInterval(interval)
165
+ }, [apiKey, endpoint, isLoadingKey, isLoadingEndpoint])
166
+
167
+ // Loading states
168
+ if (isLoadingKey || isLoadingEndpoint) return <div>Loading config...</div>
169
+ if (!apiKey || !endpoint) return <div>Configure API in Settings</div>
170
+ if (loading && !data) return <div>Loading data...</div>
171
+ if (error && !data) return <div>Error: {error}</div>
172
+
173
+ return (
174
+ <div>
175
+ {data?.items.map(item => (
176
+ <div key={item.id}>
177
+ {item.name}: {item.value}
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )
182
+ }
183
+ ```
184
+
185
+ ## Common API Patterns
186
+
187
+ ### RSS Feed
188
+
189
+ ```typescript
190
+ const response = await proxy().fetch('https://example.com/feed.xml')
191
+ const xml = await response.text()
192
+
193
+ // Parse XML (use DOMParser or a library)
194
+ const parser = new DOMParser()
195
+ const doc = parser.parseFromString(xml, 'text/xml')
196
+ const items = doc.querySelectorAll('item')
197
+ ```
198
+
199
+ ### JSON API with Query Params
200
+
201
+ ```typescript
202
+ const params = new URLSearchParams({
203
+ apiKey: apiKey,
204
+ city: city,
205
+ format: 'json',
206
+ })
207
+
208
+ const response = await proxy().fetch(`https://api.example.com/data?${params}`)
209
+ const data = await response.json()
210
+ ```
211
+
212
+ ### Sports Data API
213
+
214
+ ```typescript
215
+ const response = await proxy().fetch(
216
+ `https://api.sportsdata.io/v3/nfl/scores/json/Games/${season}`,
217
+ {
218
+ headers: {
219
+ 'Ocp-Apim-Subscription-Key': apiKey,
220
+ }
221
+ }
222
+ )
223
+
224
+ const games = await response.json()
225
+ ```
226
+
227
+ ### News/Headlines API
228
+
229
+ ```typescript
230
+ const response = await proxy().fetch(
231
+ `https://newsapi.org/v2/top-headlines?country=us&apiKey=${apiKey}`
232
+ )
233
+
234
+ const news = await response.json()
235
+ ```
236
+
237
+ ## Error Handling
238
+
239
+ ```typescript
240
+ try {
241
+ const response = await proxy().fetch(endpoint)
242
+
243
+ // Check HTTP status
244
+ if (!response.ok) {
245
+ if (response.status === 401) {
246
+ throw new Error('Invalid API key')
247
+ } else if (response.status === 404) {
248
+ throw new Error('Endpoint not found')
249
+ } else if (response.status === 429) {
250
+ throw new Error('Rate limit exceeded')
251
+ } else {
252
+ throw new Error(`API error: ${response.status}`)
253
+ }
254
+ }
255
+
256
+ const data = await response.json()
257
+ } catch (err) {
258
+ if (err instanceof Error) {
259
+ if (err.message.includes('timeout')) {
260
+ // SDK timeout (30 seconds)
261
+ console.error('Request timed out')
262
+ } else if (err.message.includes('network')) {
263
+ // Network error
264
+ console.error('Network error')
265
+ } else {
266
+ console.error(err.message)
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ ## Request Options
273
+
274
+ The proxy supports all standard fetch options:
275
+
276
+ ```typescript
277
+ await proxy().fetch(url, {
278
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
279
+ headers: {
280
+ 'Content-Type': 'application/json',
281
+ 'Authorization': 'Bearer token',
282
+ // Any custom headers
283
+ },
284
+ body: JSON.stringify(data), // For POST/PUT/PATCH
285
+ })
286
+ ```
287
+
288
+ ## Response Handling
289
+
290
+ ```typescript
291
+ const response = await proxy().fetch(url)
292
+
293
+ // JSON
294
+ const json = await response.json()
295
+
296
+ // Text
297
+ const text = await response.text()
298
+
299
+ // Blob (for binary data)
300
+ const blob = await response.blob()
301
+
302
+ // Check content type
303
+ const contentType = response.headers.get('Content-Type')
304
+ if (contentType?.includes('application/json')) {
305
+ return response.json()
306
+ } else {
307
+ return response.text()
308
+ }
309
+ ```
310
+
311
+ ## Tips
312
+
313
+ 1. **Check CORS first** - Try regular `fetch()` first; only use proxy if CORS fails
314
+ 2. **Handle errors** - Always check `response.ok` before parsing
315
+ 3. **Add refresh interval** - External data goes stale; refresh periodically
316
+ 4. **Store API keys securely** - Use instance or application store hooks
317
+ 5. **Show stale data** - Display last known data while refreshing
318
+ 6. **Log errors** - Help users troubleshoot configuration issues
319
+ 7. **Timeout awareness** - SDK times out after 30 seconds