howone 0.2.2 → 0.2.5

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 (30) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +36 -7
  3. package/templates/vite/.howone/skills/howone/01-architect/02-manifest-codegen.md +121 -436
  4. package/templates/vite/.howone/skills/howone/03-ai-capabilities/03-service-capability-catalog.md +37 -19
  5. package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +14 -5
  6. package/templates/vite/.howone/skills/howone/04-app-sdk/01-client-setup.md +94 -261
  7. package/templates/vite/.howone/skills/howone/04-app-sdk/02-entity-operations.md +85 -465
  8. package/templates/vite/.howone/skills/howone/04-app-sdk/03-auth.md +11 -7
  9. package/templates/vite/.howone/skills/howone/04-app-sdk/04-react-integration.md +84 -137
  10. package/templates/vite/.howone/skills/howone/04-app-sdk/05-file-upload.md +66 -273
  11. package/templates/vite/.howone/skills/howone/04-app-sdk/06-raw-http.md +72 -249
  12. package/templates/vite/.howone/skills/howone/04-app-sdk/07-ai-action-calls.md +135 -499
  13. package/templates/vite/.howone/skills/howone/04-app-sdk/08-ai-manifest-handoff.md +49 -196
  14. package/templates/vite/.howone/skills/howone/04-app-sdk/09-extension-boundaries.md +4 -4
  15. package/templates/vite/.howone/skills/howone/04-app-sdk/10-workflow-execute-sse.md +94 -61
  16. package/templates/vite/.howone/skills/howone/04-app-sdk/11-entity-data-access-patterns.md +4 -3
  17. package/templates/vite/.howone/skills/howone/SKILL.md +110 -4
  18. package/templates/vite/.howone/skills/howone/references/audio-generation.md +30 -0
  19. package/templates/vite/.howone/skills/howone/references/audio-recognition.md +30 -0
  20. package/templates/vite/.howone/skills/howone/references/common-errors.md +27 -0
  21. package/templates/vite/.howone/skills/howone/references/finance.md +28 -0
  22. package/templates/vite/.howone/skills/howone/references/image-editing.md +30 -0
  23. package/templates/vite/.howone/skills/howone/references/image-generation.md +30 -0
  24. package/templates/vite/.howone/skills/howone/references/version-evidence.md +47 -0
  25. package/templates/vite/.howone/skills/howone/references/video-generation.md +35 -0
  26. package/templates/vite/.howone/skills/howone/scripts/verify-project.mjs +151 -0
  27. package/templates/vite/package.json +1 -1
  28. package/templates/vite/src/App.tsx +9 -5
  29. package/templates/vite/src/lib/sdk.ts +7 -5
  30. package/templates/vite/bun.lock +0 -1478
@@ -1,319 +1,112 @@
1
1
  # File Upload
2
2
 
3
- ## Overview
3
+ Use `howone.upload` for HowOne storage. Upload first, then persist the returned CDN URL in an
4
+ entity. Uploads require a real authenticated token unless the backend explicitly exposes a public
5
+ upload contract.
4
6
 
5
- `client.upload` provides three methods for uploading files to the HowOne storage backend:
6
- - `upload.file(file, options?)` — general-purpose single file upload with progress and abort support
7
- - `upload.image(file)` — convenience wrapper for image uploads
8
- - `upload.batch(options)` — upload multiple files with concurrency control
9
-
10
- All upload methods are accessed from the `client` (or `howone`) object directly — they are **not** part of entities or AI.
11
-
12
- ---
13
-
14
- ## Types
7
+ ## Contract
15
8
 
16
9
  ```ts
17
- // ── Input ─────────────────────────────────────────────────────
18
- type UploadableFile = File | Blob | string // string = URL or base64
10
+ type UploadableFile = File | Blob | string // string = data URL or source URL
19
11
 
20
- // ── Options ───────────────────────────────────────────────────
21
12
  type UploadOptions = {
22
- onProgress?: (percent: number) => void // 0–100
23
- signal?: AbortSignal // for cancellation
24
- metadata?: Record<string, any> // custom metadata to attach
13
+ onProgress?: (percent: number) => void
14
+ signal?: AbortSignal
15
+ metadata?: Record<string, unknown>
25
16
  }
26
17
 
27
- // ── Single upload result ──────────────────────────────────────
28
18
  type UploadResponse = {
29
- url: string // CDN URL of the uploaded file
30
- thumbnailUrl?: string // thumbnail URL (for images/videos)
31
- id?: string // storage file ID
32
- size?: number // file size in bytes
33
- mimeType?: string // detected MIME type
19
+ url: string
20
+ thumbnailUrl?: string
21
+ id?: string
22
+ size?: number
23
+ mimeType?: string
34
24
  }
35
25
 
36
- // ── Batch upload options ──────────────────────────────────────
37
26
  type BatchUploadOptions = {
38
- files: (File | Blob)[]
39
- concurrent?: number // default: 3
27
+ files: UploadableFile[]
28
+ concurrent?: number // positive integer; default 3
40
29
  onProgress?: (completed: number, total: number) => void
41
30
  onFileComplete?: (result: UploadResponse | Error, index: number) => void
42
31
  signal?: AbortSignal
43
32
  }
44
-
45
- // ── Batch upload result ───────────────────────────────────────
46
- type BatchUploadResponse = {
47
- success: UploadResponse[]
48
- failed: Array<{ index: number; error: string }>
49
- total: number
50
- }
51
33
  ```
52
34
 
53
- ---
35
+ Both data-only responses and `{ data: ... }` envelopes are normalized. The response must contain
36
+ `publicUrl` or `url`; malformed responses throw instead of returning an undefined URL.
54
37
 
55
- ## upload.file Single File Upload
38
+ ## Single and image upload
56
39
 
57
40
  ```ts
58
- import howone from '@/lib/sdk'
59
-
60
- // Basic upload
61
- const result = await howone.upload.file(file)
62
- console.log(result.url) // 'https://cdn.howone.app/...'
63
- console.log(result.size) // 12345 (bytes)
64
- console.log(result.mimeType) // 'image/jpeg'
65
-
66
- // With progress callback
67
- const result = await howone.upload.file(file, {
68
- onProgress: (percent) => {
69
- console.log(`Upload progress: ${percent}%`)
70
- setProgress(percent)
71
- },
72
- })
73
-
74
- // With cancellation
75
- const controller = new AbortController()
76
- const promise = howone.upload.file(file, {
77
- signal: controller.signal,
41
+ const uploaded = await howone.upload.file(file, {
42
+ metadata: { category: 'cover' },
78
43
  onProgress: setProgress,
44
+ signal: controller.signal,
79
45
  })
80
46
 
81
- // Cancel after 5 seconds
82
- setTimeout(() => controller.abort(), 5000)
83
-
84
- const result = await promise
47
+ await howone.entities.Story.update(storyId, { coverUrl: uploaded.url })
85
48
 
86
- // With metadata
87
- const result = await howone.upload.file(file, {
88
- metadata: {
89
- entityId: story.id,
90
- uploadedBy: user.id,
91
- category: 'cover',
92
- },
93
- })
49
+ // image is a convenience alias and returns the same full UploadResponse shape.
50
+ const image = await howone.upload.image(imageFile, { onProgress: setProgress })
51
+ console.log(image.url, image.thumbnailUrl, image.id)
94
52
  ```
95
53
 
96
- ---
54
+ `onProgress` fires only when the transport reports a total byte count. Keep a separate loading
55
+ state so an upload without a computable total does not appear stuck.
97
56
 
98
- ## upload.image — Image Shorthand
99
-
100
- ```ts
101
- import howone from '@/lib/sdk'
102
-
103
- // Accepts File, Blob, or a URL/base64 string
104
- const { url } = await howone.upload.image(imageFile)
105
- console.log(url) // 'https://cdn.howone.app/images/...'
106
-
107
- // Use the URL directly in an img tag or save to an entity
108
- await howone.entities.Story.update(storyId, { coverUrl: url })
57
+ ```tsx
58
+ const controller = new AbortController()
59
+ try {
60
+ setUploading(true)
61
+ const { url } = await howone.upload.file(file, {
62
+ signal: controller.signal,
63
+ onProgress: setProgress,
64
+ })
65
+ await howone.entities.Asset.create({ url, kind: 'image' })
66
+ } catch (error) {
67
+ if (!controller.signal.aborted) setError(error instanceof Error ? error.message : String(error))
68
+ } finally {
69
+ setUploading(false)
70
+ }
109
71
  ```
110
72
 
111
- ---
112
-
113
- ## upload.batch — Multiple Files
73
+ ## Batch upload
114
74
 
115
75
  ```ts
116
- import howone from '@/lib/sdk'
117
-
118
- const files: File[] = Array.from(fileInput.files ?? [])
119
-
120
76
  const result = await howone.upload.batch({
121
- files,
122
- concurrent: 3, // upload 3 at a time
123
-
124
- onProgress: (completed, total) => {
125
- console.log(`${completed} / ${total} files uploaded`)
126
- setProgress(Math.round((completed / total) * 100))
127
- },
128
-
129
- onFileComplete: (result, index) => {
130
- if (result instanceof Error) {
131
- console.error(`File ${index} failed:`, result.message)
132
- } else {
133
- console.log(`File ${index} URL:`, result.url)
134
- }
77
+ files: Array.from(input.files ?? []),
78
+ concurrent: 3,
79
+ onProgress: (completed, total) => setProgress(Math.round((completed / total) * 100)),
80
+ onFileComplete: (value, index) => {
81
+ if (value instanceof Error) console.error('file failed', index, value.message)
135
82
  },
136
83
  })
137
84
 
138
- console.log('Uploaded:', result.success.length)
139
- console.log('Failed:', result.failed.length)
140
-
141
- // Collect all URLs
142
- const urls = result.success.map(r => r.url)
143
-
144
- // With cancellation
145
- const controller = new AbortController()
146
- const resultPromise = howone.upload.batch({
147
- files,
148
- signal: controller.signal,
149
- onProgress: (c, t) => console.log(c, '/', t),
150
- })
151
- // controller.abort() to cancel
85
+ const urls = result.success.map((file) => file.url)
86
+ if (result.failed.length) showPartialFailure(result.failed)
152
87
  ```
153
88
 
154
- ---
89
+ `concurrent` must be a positive integer. The result is intentionally partial: successful files are
90
+ kept even when others fail. An aborted batch stops starting later batches and in-flight uploads
91
+ observe the same signal; do not assume every requested file has a corresponding failure entry.
155
92
 
156
- ## React Patterns
93
+ ## AI image workflow pattern
157
94
 
158
- ### Single image upload component
95
+ AI output URLs are not automatically HowOne storage records. If the product needs durable storage:
159
96
 
160
- ```tsx
161
- import { useRef, useState } from 'react'
162
- import howone from '@/lib/sdk'
163
-
164
- export function ImageUploader({
165
- onUpload,
166
- }: {
167
- onUpload: (url: string) => void
168
- }) {
169
- const [uploading, setUploading] = useState(false)
170
- const [progress, setProgress] = useState(0)
171
- const [error, setError] = useState<string | null>(null)
172
- const abortRef = useRef<AbortController | null>(null)
173
-
174
- async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
175
- const file = e.target.files?.[0]
176
- if (!file) return
177
-
178
- setUploading(true)
179
- setProgress(0)
180
- setError(null)
181
- abortRef.current = new AbortController()
182
-
183
- try {
184
- const result = await howone.upload.file(file, {
185
- signal: abortRef.current.signal,
186
- onProgress: setProgress,
187
- })
188
- onUpload(result.url)
189
- } catch (err) {
190
- if ((err as Error).name !== 'AbortError') {
191
- setError(err instanceof Error ? err.message : 'Upload failed')
192
- }
193
- } finally {
194
- setUploading(false)
195
- abortRef.current = null
196
- }
197
- }
198
-
199
- function handleCancel() {
200
- abortRef.current?.abort()
201
- }
202
-
203
- return (
204
- <div>
205
- <input
206
- type="file"
207
- accept="image/*"
208
- onChange={handleChange}
209
- disabled={uploading}
210
- />
211
- {uploading && (
212
- <div>
213
- <progress value={progress} max={100} />
214
- <span>{progress}%</span>
215
- <button onClick={handleCancel}>Cancel</button>
216
- </div>
217
- )}
218
- {error && <p className="error">{error}</p>}
219
- </div>
220
- )
221
- }
222
- ```
223
-
224
- ### Multi-file upload with gallery preview
225
-
226
- ```tsx
227
- import { useState } from 'react'
228
- import howone from '@/lib/sdk'
229
-
230
- export function MultiFileUploader() {
231
- const [files, setFiles] = useState<File[]>([])
232
- const [uploading, setUploading] = useState(false)
233
- const [progress, setProgress] = useState({ completed: 0, total: 0 })
234
- const [uploadedUrls, setUploadedUrls] = useState<string[]>([])
235
- const [failedCount, setFailedCount] = useState(0)
236
-
237
- function handleSelect(e: React.ChangeEvent<HTMLInputElement>) {
238
- setFiles(Array.from(e.target.files ?? []))
239
- setUploadedUrls([])
240
- setFailedCount(0)
241
- }
242
-
243
- async function handleUpload() {
244
- if (!files.length) return
245
- setUploading(true)
246
- setProgress({ completed: 0, total: files.length })
247
-
248
- const result = await howone.upload.batch({
249
- files,
250
- concurrent: 3,
251
- onProgress: (completed, total) => setProgress({ completed, total }),
252
- })
253
-
254
- setUploadedUrls(result.success.map(r => r.url))
255
- setFailedCount(result.failed.length)
256
- setUploading(false)
257
- }
258
-
259
- return (
260
- <div>
261
- <input type="file" multiple onChange={handleSelect} disabled={uploading} />
262
- <button onClick={handleUpload} disabled={uploading || !files.length}>
263
- {uploading
264
- ? `Uploading ${progress.completed}/${progress.total}...`
265
- : `Upload ${files.length} files`}
266
- </button>
267
- {failedCount > 0 && <p>{failedCount} files failed to upload</p>}
268
- <div className="gallery">
269
- {uploadedUrls.map((url, i) => (
270
- <img key={i} src={url} alt={`Upload ${i}`} />
271
- ))}
272
- </div>
273
- </div>
274
- )
275
- }
276
- ```
277
-
278
- ### Upload and save to entity
279
-
280
- ```tsx
281
- import howone, { type StoryUpdate } from '@/lib/sdk'
282
-
283
- async function uploadCoverAndUpdate(storyId: string, coverFile: File) {
284
- // 1. Upload image
285
- const { url } = await howone.upload.image(coverFile)
286
-
287
- // 2. Update entity with the uploaded URL
288
- const updated = await howone.entities.Story.update(storyId, {
289
- coverUrl: url,
290
- })
291
-
292
- return updated
293
- }
294
- ```
295
-
296
- ### Upload from AI output (AI-generated image URL → storage)
297
-
298
- ```tsx
299
- import howone from '@/lib/sdk'
300
-
301
- async function saveGeneratedImage(aiImageUrl: string, storyId: string) {
302
- // Upload by URL (download + re-upload to HowOne storage)
303
- const { url } = await howone.upload.image(aiImageUrl)
304
-
305
- await howone.entities.Story.update(storyId, { coverUrl: url })
306
- return url
307
- }
97
+ ```ts
98
+ const output = await howone.ai.generateImage.run(input)
99
+ const stored = await howone.upload.image(output.imageUrl)
100
+ await howone.entities.Generation.update(generationId, { imageUrl: stored.url })
308
101
  ```
309
102
 
310
- ---
311
-
312
- ## Common Mistakes
103
+ ## Common mistakes
313
104
 
314
- | Mistake | Correct Pattern |
105
+ | Mistake | Fix |
315
106
  |---|---|
316
- | Assuming `upload.image` returns the same shape as `upload.file` | `upload.image` returns `{ url: string }` only; `upload.file` returns full `UploadResponse` |
317
- | Not handling partial batch failures | Always check `result.failed.length` and `result.success` separately |
318
- | Leaking upload after component unmount | Store `AbortController` in a ref and abort on cleanup |
319
- | Saving a raw blob URL (`blob://...`) to an entity | Always await the upload first, then save the returned CDN `url` |
107
+ | Saving a `blob:` URL | Persist the returned `UploadResponse.url`. |
108
+ | Assuming `upload.image` returns only `{ url }` | It returns the full upload response; destructuring `{ url }` remains valid. |
109
+ | Passing `metadata` with owner/system fields | Keep metadata app-owned; entity ownership is derived by the backend. |
110
+ | Using `concurrent: 0` | Use a positive integer. |
111
+ | Ignoring `failed` | Batch uploads are partial; handle both arrays. |
112
+ | Forgetting cleanup on unmount | Abort with a controller owned by the component. |