howone 0.2.3 → 0.2.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.
Files changed (23) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +8 -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/04-workflow-operations.md +13 -4
  5. package/templates/vite/.howone/skills/howone/04-app-sdk/01-client-setup.md +94 -261
  6. package/templates/vite/.howone/skills/howone/04-app-sdk/02-entity-operations.md +85 -465
  7. package/templates/vite/.howone/skills/howone/04-app-sdk/03-auth.md +11 -7
  8. package/templates/vite/.howone/skills/howone/04-app-sdk/04-react-integration.md +84 -137
  9. package/templates/vite/.howone/skills/howone/04-app-sdk/05-file-upload.md +66 -273
  10. package/templates/vite/.howone/skills/howone/04-app-sdk/06-raw-http.md +72 -249
  11. package/templates/vite/.howone/skills/howone/04-app-sdk/07-ai-action-calls.md +135 -499
  12. package/templates/vite/.howone/skills/howone/04-app-sdk/08-ai-manifest-handoff.md +49 -196
  13. package/templates/vite/.howone/skills/howone/04-app-sdk/09-extension-boundaries.md +4 -4
  14. package/templates/vite/.howone/skills/howone/04-app-sdk/10-workflow-execute-sse.md +94 -61
  15. package/templates/vite/.howone/skills/howone/04-app-sdk/11-entity-data-access-patterns.md +4 -3
  16. package/templates/vite/.howone/skills/howone/SKILL.md +48 -8
  17. package/templates/vite/.howone/skills/howone/references/common-errors.md +27 -0
  18. package/templates/vite/.howone/skills/howone/references/version-evidence.md +47 -0
  19. package/templates/vite/.howone/skills/howone/scripts/verify-project.mjs +151 -0
  20. package/templates/vite/package.json +1 -1
  21. package/templates/vite/src/App.tsx +9 -5
  22. package/templates/vite/src/lib/sdk.ts +7 -5
  23. package/templates/vite/bun.lock +0 -1478
@@ -1,299 +1,122 @@
1
1
  # Raw HTTP
2
2
 
3
- ## When to Use
3
+ Use `howone.raw` only for an app-owned endpoint that has no typed entity, auth, upload, or AI
4
+ binding. Do not use it to bypass manifest access rules or recreate `/data` and workflow calls.
4
5
 
5
- Use `client.raw` when you need to call a custom backend endpoint that is **not** covered by `client.entities` or `client.ai`. The raw client is an Axios-based HTTP client that automatically attaches the HowOne auth token and project ID headers.
6
+ ## Contract
6
7
 
7
- **Do not use `client.raw` to re-implement entity operations or AI workflows** use the typed SDK methods instead.
8
-
9
- ---
10
-
11
- ## The RawHttpClient Interface
8
+ `howone.raw` is a request client whose methods resolve to response **data**, not an AxiosResponse:
12
9
 
13
10
  ```ts
14
- type RawHttpClient = {
15
- instance: AxiosInstance
16
-
17
- // All methods return Promise<AxiosResponse>
18
- request(config: RequestConfig): Promise<AxiosResponse>
19
- get(config: RequestConfig): Promise<AxiosResponse>
20
- post(config: RequestConfig): Promise<AxiosResponse>
21
- put(config: RequestConfig): Promise<AxiosResponse>
22
- patch(config: RequestConfig): Promise<AxiosResponse>
23
- delete(config: RequestConfig): Promise<AxiosResponse>
24
-
25
- // Cancel an in-flight request by URL
26
- cancelRequest(url: string): void
27
-
28
- // Cancel all in-flight requests
29
- cancelAllRequests(): void
30
- }
11
+ const data = await howone.raw.get<Stats>({ url: '/custom/stats' })
12
+ const created = await howone.raw.post<Receipt>({
13
+ url: '/custom/receipts',
14
+ data: { orderId },
15
+ })
31
16
  ```
32
17
 
33
- ### RequestConfig
34
-
35
- `RequestConfig` extends `AxiosRequestConfig` with optional interceptors:
36
-
37
18
  ```ts
38
- type RequestConfig<T = AxiosResponse> = AxiosRequestConfig & {
19
+ type RequestConfig<T = unknown> = AxiosRequestConfig & {
39
20
  interceptors?: {
40
21
  requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
41
- requestInterceptorCatch?: (error: any) => any
42
- responseInterceptor?: (res: T) => T
43
- responseInterceptorCatch?: (error: any) => any
22
+ requestInterceptorCatch?: (error: unknown) => unknown
23
+ responseInterceptor?: (data: T) => T
24
+ responseInterceptorCatch?: (error: unknown) => unknown
44
25
  }
45
- showLoading?: boolean
46
26
  }
47
27
  ```
48
28
 
49
- ---
29
+ The SDK attaches the current client token to authenticated non-public requests. It does not turn
30
+ an app-owned route into a platform contract; the backend must authenticate and authorize it.
50
31
 
51
- ## Basic Usage
32
+ ## URL rules
52
33
 
53
- ```ts
54
- import howone from '@/lib/sdk'
55
-
56
- // GET
57
- const response = await howone.raw.get({
58
- url: '/api/custom/stats',
59
- })
60
- const data = response.data // untyped AxiosResponse.data
61
-
62
- // POST with body
63
- const response = await howone.raw.post({
64
- url: '/api/custom/send-notification',
65
- data: {
66
- userId: '123',
67
- message: 'Hello!',
68
- },
69
- })
70
-
71
- // PUT
72
- const response = await howone.raw.put({
73
- url: `/api/custom/profile/${userId}`,
74
- data: { displayName: 'Alice' },
75
- })
76
-
77
- // PATCH
78
- const response = await howone.raw.patch({
79
- url: `/api/custom/settings`,
80
- data: { theme: 'dark' },
81
- })
82
-
83
- // DELETE
84
- const response = await howone.raw.delete({
85
- url: `/api/custom/sessions/${sessionId}`,
86
- })
87
- ```
88
-
89
- ---
90
-
91
- ## Typed Responses
92
-
93
- Wrap with generics for type safety:
34
+ The configured REST base already ends in `/api` for hosted environments. Use a path relative to that
35
+ base:
94
36
 
95
37
  ```ts
96
- type StatsResponse = {
97
- totalUsers: number
98
- activeToday: number
99
- storageUsed: number
100
- }
101
-
102
- const response = await howone.raw.get<StatsResponse>({
103
- url: '/api/custom/stats',
104
- })
38
+ // Correct: https://api.howone.dev/api/custom/stats
39
+ const stats = await howone.raw.get<Stats>({ url: '/custom/stats' })
105
40
 
106
- const stats = response.data // typed as StatsResponse
41
+ // Wrong: https://api.howone.dev/api/api/custom/stats
42
+ // howone.raw.get({ url: '/api/custom/stats' })
107
43
  ```
108
44
 
109
- ---
45
+ An absolute URL is an explicit app-owned escape hatch and is not how EAX routing is selected. Do not
46
+ put an EAX URL in a raw request; AI actions already use `env`.
110
47
 
111
- ## Query Parameters
48
+ ## Query, headers, and cancellation
112
49
 
113
50
  ```ts
114
- // Pass query params via the `params` field (Axios serializes them automatically)
115
- const response = await howone.raw.get({
116
- url: '/api/custom/search',
117
- params: {
118
- q: 'dragons',
119
- page: 1,
120
- limit: 20,
121
- sort: 'createdAt',
122
- order: 'desc',
123
- },
51
+ const result = await howone.raw.get<SearchResult>({
52
+ url: '/custom/search',
53
+ params: { q, page: 1, limit: 20 },
54
+ headers: { 'X-Request-Source': 'app' },
55
+ signal: controller.signal,
124
56
  })
125
- // Calls: GET /api/custom/search?q=dragons&page=1&limit=20&sort=createdAt&order=desc
126
- ```
127
-
128
- ---
129
-
130
- ## Custom Headers
131
-
132
- ```ts
133
- const response = await howone.raw.post({
134
- url: '/api/custom/webhook',
135
- data: payload,
136
- headers: {
137
- 'X-Webhook-Secret': import.meta.env.VITE_WEBHOOK_SECRET,
138
- 'X-Request-Source': 'app',
139
- },
140
- })
141
- ```
142
57
 
143
- ---
144
-
145
- ## Request Cancellation
146
-
147
- ```ts
148
- // Cancel a specific request by URL
149
- howone.raw.cancelRequest('/api/custom/long-running')
150
-
151
- // Cancel all in-flight requests (e.g. on page unmount)
58
+ // Cancels every in-flight request with this exact method-independent URL.
59
+ howone.raw.cancelRequest('/custom/search')
152
60
  howone.raw.cancelAllRequests()
61
+ ```
153
62
 
154
- // Pattern: cancel on component unmount
155
- import { useEffect } from 'react'
156
- import howone from '@/lib/sdk'
157
-
158
- function DataComponent() {
159
- useEffect(() => {
160
- howone.raw.get({ url: '/api/custom/data' })
161
- .then(res => setData(res.data))
63
+ Prefer an `AbortController` per component operation so a new search does not cancel an unrelated
64
+ request. The SDK links the caller's signal to its internal controller and also supports concurrent
65
+ requests with the same method and URL.
162
66
 
163
- return () => {
164
- howone.raw.cancelRequest('/api/custom/data')
165
- }
166
- }, [])
167
- }
67
+ ```tsx
68
+ useEffect(() => {
69
+ const controller = new AbortController()
70
+ void howone.raw
71
+ .get<SearchResult>({ url: '/custom/search', params: { q }, signal: controller.signal })
72
+ .then(setResult)
73
+ .catch((error) => {
74
+ if (!controller.signal.aborted) setError(error instanceof Error ? error.message : String(error))
75
+ })
76
+ return () => controller.abort()
77
+ }, [q])
168
78
  ```
169
79
 
170
- ---
171
-
172
- ## Per-Request Interceptors
80
+ ## Per-request interceptors
173
81
 
174
- For one-off request/response transforms without modifying the global client:
82
+ The response interceptor sees the already-unwrapped data:
175
83
 
176
84
  ```ts
177
- const response = await howone.raw.post({
178
- url: '/api/custom/transform',
179
- data: payload,
85
+ const data = await howone.raw.get<RawStats>({
86
+ url: '/custom/stats',
180
87
  interceptors: {
181
88
  requestInterceptor: (config) => {
182
- // Modify request config (e.g. add timestamp)
183
- config.headers['X-Timestamp'] = Date.now().toString()
89
+ config.headers.set('X-Request-Id', crypto.randomUUID())
184
90
  return config
185
91
  },
186
- responseInterceptor: (res) => {
187
- // Log response time or transform data
188
- console.log('Response status:', res.status)
189
- return res
190
- },
191
- responseInterceptorCatch: (error) => {
192
- // Handle specific error codes
193
- if (error.response?.status === 503) {
194
- console.error('Service temporarily unavailable')
195
- }
196
- return Promise.reject(error)
197
- },
92
+ responseInterceptor: (value) => ({ ...value, loadedAt: Date.now() }),
93
+ responseInterceptorCatch: (error) => Promise.reject(error),
198
94
  },
199
95
  })
200
96
  ```
201
97
 
202
- ---
203
-
204
- ## Direct Axios Instance Access
205
-
206
- For maximum control (multipart forms, streaming responses, etc.):
207
-
208
- ```ts
209
- const instance = howone.raw.instance // Axios instance
210
-
211
- // Multipart form data
212
- const formData = new FormData()
213
- formData.append('report', file)
214
- formData.append('meta', JSON.stringify({ type: 'monthly' }))
215
-
216
- const response = await instance.post('/api/custom/reports', formData, {
217
- headers: { 'Content-Type': 'multipart/form-data' },
218
- onUploadProgress: (e) => {
219
- const percent = Math.round((e.loaded * 100) / (e.total ?? 1))
220
- setProgress(percent)
221
- },
222
- })
223
- ```
224
-
225
- ---
226
-
227
- ## React Pattern: Data Fetching with Raw HTTP
228
-
229
- ```tsx
230
- import { useEffect, useState } from 'react'
231
- import howone from '@/lib/sdk'
232
-
233
- type AnalyticsData = {
234
- views: number
235
- clicks: number
236
- conversions: number
237
- period: string
238
- }
239
-
240
- function Analytics({ projectId }: { projectId: string }) {
241
- const [data, setData] = useState<AnalyticsData | null>(null)
242
- const [loading, setLoading] = useState(true)
243
- const [error, setError] = useState<string | null>(null)
244
-
245
- useEffect(() => {
246
- let cancelled = false
247
-
248
- howone.raw.get<AnalyticsData>({
249
- url: '/api/custom/analytics',
250
- params: { projectId, period: '30d' },
251
- })
252
- .then(res => { if (!cancelled) setData(res.data) })
253
- .catch(err => { if (!cancelled) setError(err.message) })
254
- .finally(() => { if (!cancelled) setLoading(false) })
255
-
256
- return () => {
257
- cancelled = true
258
- howone.raw.cancelRequest('/api/custom/analytics')
259
- }
260
- }, [projectId])
261
-
262
- if (loading) return <div>Loading analytics...</div>
263
- if (error) return <div>Error: {error}</div>
264
- if (!data) return null
265
-
266
- return (
267
- <div>
268
- <p>Views: {data.views}</p>
269
- <p>Clicks: {data.clicks}</p>
270
- <p>Conversions: {data.conversions}</p>
271
- </div>
272
- )
273
- }
274
- ```
275
-
276
- ---
98
+ Do not expect `value.status` or `value.data` in `responseInterceptor`; use the direct Axios instance
99
+ only when the installed SDK typings explicitly require a transport-level operation.
277
100
 
278
- ## client.raw vs client.entities
101
+ ## Choosing the correct surface
279
102
 
280
- | Use Case | Recommended API |
103
+ | Need | Use |
281
104
  |---|---|
282
- | CRUD on a HowOne entity | `howone.entities.<Entity>.*` |
283
- | Querying with pagination/filter/sort | `howone.entities.<Entity>.query()` |
284
- | Running an AI workflow | `howone.ai.<action>.run()` |
285
- | Calling a custom backend route | `howone.raw.get/post/...` |
286
- | Sending webhooks or notifications | `howone.raw.post()` |
287
- | Fetching analytics or aggregated data not in entities | `howone.raw.get()` |
288
- | Uploading files | `howone.upload.*` |
105
+ | Manifest entity CRUD/query | `howone.entities.Entity.*` |
106
+ | Manifest-approved public data | `howone.public.entities.Entity.*` |
107
+ | EAX workflow | `howone.ai.action.run/stream/events` |
108
+ | HowOne storage | `howone.upload.*` |
109
+ | App-owned backend route | `howone.raw.*` |
289
110
 
290
- ---
111
+ If the endpoint is missing from the platform contract, classify it as an explicit app-owned
112
+ integration. Do not call an internal platform route because it happens to work today.
291
113
 
292
- ## Common Mistakes
114
+ ## Common mistakes
293
115
 
294
- | Mistake | Correct Pattern |
116
+ | Mistake | Fix |
295
117
  |---|---|
296
- | `howone.raw.get({ url: '/entities/Story' })` to read entities | Use `howone.entities.Story.query()` |
297
- | Not handling Axios errors (`.response.status`) | Wrap in try/catch and check `error.response?.status` |
298
- | Calling `cancelAllRequests()` too broadly | Use `cancelRequest(url)` for surgical cancellation |
299
- | Forgetting that `response.data` is untyped by default | Pass the type parameter: `howone.raw.get<MyType>(...)` |
118
+ | Reading `response.data` | Raw methods already return data. |
119
+ | Prefixing `/api` in a raw path | The REST base already includes `/api`. |
120
+ | Calling raw `/data/...` | Use generated entity bindings. |
121
+ | Passing `'*'` as a cross-window origin | Use explicit origins in the devtools Provider. |
122
+ | Cancelling by a broad substring | Use the exact request path or the caller's AbortSignal. |